I’ve been using AWS Cognito as the authentication piece to give users access to an Angular web project I’m working on.
Everything worked as advertised and I was happy with the result, until I started testing my authentication service.
That’s when things got interesting and I could not create a spy object on any of the Cognito classes.
Instead, I got…
1 | AuthenticationService when .signInUser is working normally instantiate cognito AuthenticationDetails with correct user credentials FAILED |
Quick Answer
- Turns out you need to use Object.defineProperty and modify the property to be
writable: true
.
1 | import * as AWSCognito from 'amazon-cognito-identity-js'; |
- This is strange because the same code in node.js throws
TypeError: Cannot redefine property: AuthenticationDetails
. - Angular uses
zone.js
to modify, or extend, all your async APIs. - We call this Monkey patching.
- In my case, Angular Monkey patched
amazon-cognito-identity-js
and the result is we can executeObject.defineProperty
withoutTypeError: Cannot redefine property: AuthenticationDetails
. - Mocking AWS Cognito is simple, now that I know :)
- Code examples on how to mock AWS Cognito in Angular.
What is Object.defineProperty
Object.defineProperty
is a function which is natively present in the Javascript runtime environment and takes the following arguments:
1 | Object.defineProperty(obj, prop, descriptor) |
With Object.defineProperty
, one defines a new property directly on an object or modifies an existing property on an object.
I found a very good StackOverflow answer, How to use javascript Object.defineProperty, and here are some points which helped me:
In Javascript, standard properties (data member with getter and setter) are defined by accessor descriptor.
Exclusively, you can use data descriptor (so you can’t use value and set on the same property):
- accessor descriptor = get + set
- get must be a function; its return value is used in reading the property; if not specified, the default is
undefined
, which behaves like a function that returnsundefined
.- set must be a function; its parameter is filled with Right Hand Side in assigning a value to property; if not specified, the default is
undefined
, which behaves like an empty function.- data descriptor = value + writable
- value default
undefined
; if writable, configurable and enumerable (see below) are true, the property behaves like an ordinary data field.- writable - default false; if not true, the property is read only; attempt to write is ignored without error**
Both descriptors can have these members:
- configurable - default false; if not true, the property can’t be deleted; attempt to delete is ignored without error**
- enumerable - default false; if true, it will be iterated in
for(var i in theObject);
if false, it will not be iterated, but it is still accessible as public.(** unless in strict mode - in that case javascript stops execution with TypeError unless it is caught in try-catch block)
(Side note: Also good to explore Object.preventExtensions(), Object.seal(), Object.freeze())
In my case, I actually marked the Cognito.AuthenticationDetails
constructor function as overridable (writable).
But, you can only do that if configurable: true
.
Here is the Mozilla example Object.defineProperty to explore:
1 | ; |
If you want to explore my claim of read-only is the default
, use console.log(Object.getOwnPropertyDescriptor(object1, 'property1'));
.
Expected output:
1 | Object { value: 42, writable: false, enumerable: false, configurable: false } |
Let’s explore the configurable
flag:
1 | ; |
The example above will throw error:
1 | Error: Cannot redefine property: property1 |
Resolve this error with configurable: true
:
1 | ; |
Background
I have an authentication.service.ts and I want to test signInUser
.
I reference amazon-cognito-identity-js for the Cognito functionality.
Here are a few tests I’ve identified to be implemented:
- Cognito.AuthenticationDetails instantiated with correct credentials.
- Cognito.CognitoUserPool instantiated with the correct UserPoolId and ClientId.
- Cognito.CognitoUser instantiated with the correct ICognitoUserData object.
- cognitoUser.authenticateUser was called only once.
- On successful login, username must be returned.
(I’ve identified these tests by the comment: // test - ...
in authentication.service.ts
)
Mocking Cognito
My first mission: Test Cognito.AuthenticationDetails instantiated with correct credentials.
Because I reference amazon-cognito-identity-js
, I thought mocks and spies would be the tools for testing AuthenticationService.signInUser
.
The code below should do the trick…
1 | describe('AuthenticationService', () => { |
But failed with:
1 | AuthenticationService when .signInUser is working normally instantiate cognito AuthenticationDetails with correct user credentials FAILED |
Based on the research here:
- Jasmine - How to write test case for AWS service authenticateUser
- Allow module system that does not rely on getters in Webpack 4
I’ve decided to go with the solution below:
1 | Object.defineProperty(AWSCognito, 'AuthenticationDetails', { |
I’ve updated my test and all was green…
1 | describe('AuthenticationService', () => { |
Life was good and I continued working until the voice inside my head whispered:
“Why is this working…?”
Node behaves different to Angular
Although the chances of using a front-end library (amazon-cognito-identity-js) in a node.js server application is very low, I’ve created a quick test in node.js just to proof a point.
The test is following exactly what I’ve done in Angular, but without the extra package dependencies from Angular.
My investigation revealed Object.getOwnPropertyDescriptor(AWSCognito, 'AuthenticationDetails')
returns configurable: false
.
This is proof that AWS Cognito library is shipped with configurable: false
.
Because it returns false
, one would not be able to execute Object.defineProperty
without an exception.
The AWS team does not want you to fiddle with their implementation of AWSCognito.AuthenticationDetails
.
Below is my node.js test:
1 | const AWSCognito = require('amazon-cognito-identity-js'); |
Output after running the test:
1 | Randomized with seed 26638 |
I’m confused…I’m sure in my Angular test, before the property modification code, I logged the exact same thing and the result was configurable: true
.
How is that possible?!
Angular’s Zone.js and Monkey patching
Angular uses zone.js to handle change detection.
Because we have async code, Angular will Monkey patch all async APIs in order to managed the change detection.
(See resources below for a more in-depth look at zone'js
as it does more than just change detection, it also helps with debugging)
A quick google revealed:
What is Monkey patching?
By definition, Monkey patching is basically extending or modifying the original API.
Now, zone.js re-defines all the async APIs like browser apis which includes set/clearTimeOut, set/clearInterval, alert, XHR apis etc.
amazon-cognito-identity-js
will be re-defined by zone.js
and also define the properties as configurable: true
.
Because it sets configurable: true
, it explains why I can define/modify object properties in the Angular world but not in the node.js world.
Below is the code that changes configurable: false
to configurable: true
from zone-evergreen.js
.
1 | function rewriteDescriptor(obj, prop, desc) { |
Here is the stack trace when I’ve added a conditional breakpoint prop === 'AuthenticationDetails'
:
1 | rewriteDescriptor @ zone-evergreen.js:2227 |
The rewriteDescriptor
function can be found in angular/packages/zone.js/lib/browser/define-property.ts in the Angular repository.
Conclusion
- Be aware that Angular uses
zone.js
to modify, or extend, all your async APIs. - We call this Monkey patching and it is Angular’s way to deal with change detection.
- If you build an Angular app and have unexpected side-effects in one of your async API libraries, start your investigation with
zone.js
.
Hopefully it saved you a few hours of research.
Use it…don’t use it :)
Code Samples
Resources
- Angular deep dive zone-js how does it monkey patches various apis by Manish Bansal.
- Zones video - NG-Conf 2014 by Brian Ford.
- 10 Things Every Angular Developer Should Know About Zone.js by Matthias Junker.
- What the hell is Zone.js and why is it in my Angular 2 by Pete Mertz.
- Jasmine - How to write test case for AWS service authenticateUser
- Allow module system that does not rely on getters in Webpack 4