From 82be227b7f902e882a4175bbf2945170c2a132f7 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 29 Aug 2016 20:44:35 +1200 Subject: [PATCH 01/17] Add tests, starting with CognitoUserPool --- package.json | 12 +++- src/CognitoUserPool.test.js | 130 ++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/CognitoUserPool.test.js diff --git a/package.json b/package.json index ed9aa68a..03d0c66d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "scripts": { "build": "webpack -p", "doc": "jsdoc src -d docs", - "lint": "eslint src" + "lint": "eslint src", + "test": "ava" }, "main": "dist/amazon-cognito-identity.min.js", "dependencies": { @@ -41,14 +42,23 @@ "sjcl": "^1.0.3" }, "devDependencies": { + "ava": "^0.16.0", "babel-core": "^6.13.2", "babel-loader": "^6.2.4", "babel-preset-es2015": "^6.13.2", + "babel-register": "^6.14.0", "eslint": "^3.3.1", "eslint-config-airbnb-base": "^5.0.2", "eslint-import-resolver-webpack": "^0.5.1", "eslint-plugin-import": "^1.13.0", "jsdoc": "^3.4.0", + "mock-require": "^1.3.0", + "sinon": "^1.17.5", "webpack": "^1.13.1" + }, + "ava": { + "require": [ + "babel-register" + ] } } diff --git a/src/CognitoUserPool.test.js b/src/CognitoUserPool.test.js new file mode 100644 index 00000000..e661857e --- /dev/null +++ b/src/CognitoUserPool.test.js @@ -0,0 +1,130 @@ +/* eslint-disable require-jsdoc */ + +import test from 'ava'; +import { stub } from 'sinon'; +import mockRequire from 'mock-require'; + +// Mock dependencies +class MockClient {} +mockRequire('aws-sdk', { + CognitoIdentityServiceProvider: MockClient, +}); + +class MockCognitoUser { + constructor({ Username, Pool }) { + this.username = Username; + this.pool = Pool; + } +} +mockRequire('./CognitoUser', MockCognitoUser); + +// Use with mocked dependencies +const CognitoUserPool = mockRequire.reRequire('./CognitoUserPool').default; + + +function constructorThrowsRequired(t, data) { + t.throws(() => new CognitoUserPool(data), /required/); +} +test(constructorThrowsRequired, null); +test(constructorThrowsRequired, {}); +test(constructorThrowsRequired, { UserPoolId: null, ClientId: null }); +test(constructorThrowsRequired, { UserPoolId: '123', ClientId: null }); +test(constructorThrowsRequired, { UserPoolId: null, ClientId: 'abc' }); +constructorThrowsRequired.title = (originalTitle, data) => ( + `constructor( ${JSON.stringify(data)} ) => throws with "required"` +); +test('constructor => creates instance with expected values', t => { + const data = { UserPoolId: '123', ClientId: 'abc' }; + const pool = new CognitoUserPool(data); + t.truthy(pool); + t.is(pool.getUserPoolId(), data.UserPoolId); + t.is(pool.getClientId(), data.ClientId); + t.is(pool.getParanoia(), 0); + t.true(pool.client instanceof MockClient); +}); + +test('constructor({ Paranoia }) => sets paranoia', t => { + const data = { UserPoolId: '123', ClientId: 'abc', Paranoia: 7 }; + const pool = new CognitoUserPool(data); + t.is(pool.getParanoia(), data.Paranoia); +}); + +function create(...requestConfigs) { + const pool = new CognitoUserPool({ UserPoolId: '123', ClientId: 'abc' }); + const requestStub = stub(); + requestConfigs.forEach((requestConfig, i) => { + const expectation = requestStub.onCall(i); + expectation.yieldsAsync(...requestConfig); + }); + pool.client.makeUnauthenticatedRequest = requestStub; + return pool; +} + +function requestFailsWith(err) { + return [err, null]; +} + +function requestSucceedsWith(result) { + return [null, result]; +} + +test('setParanoia() => sets paranoia', t => { + const pool = create(); + const paranoia = 7; + pool.setParanoia(paranoia); + t.is(pool.getParanoia(), paranoia); +}); + +test.cb('signUp() :: fails => callback gets error', t => { + const expectedError = { code: 'SomeError' }; + const pool = create(requestFailsWith(expectedError)); + pool.signUp('username', 'password', null, null, err => { + t.is(err, expectedError); + t.end(); + }); +}); + +test.cb('signUp() :: success => callback gets user and confirmed', t => { + const expectedUsername = 'username'; + const pool = create(requestSucceedsWith({ UserConfirmed: true })); + pool.signUp(expectedUsername, 'password', null, null, (err, result) => { + t.true(result.user instanceof MockCognitoUser); + t.is(result.user.username, expectedUsername); + t.is(result.user.pool, pool); + t.true(result.userConfirmed); + t.end(); + }); +}); + +test('getCurrentUser() :: no last user => returns null', t => { + const pool = create(); + const localStorage = { + getItem: stub().returns(null), + }; + global.window = { localStorage }; + + t.is(pool.getCurrentUser(), null); + + t.true(localStorage.getItem.calledOnce); + t.true(localStorage.getItem.calledWithExactly( + `CognitoIdentityServiceProvider.${pool.getClientId()}.LastAuthUser` + )); +}); + +test('getCurrentUser() :: with last user => returns user instance', t => { + const pool = create(); + const username = 'username'; + const localStorage = { + getItem: stub().returns(username), + }; + global.window = { localStorage }; + + const currentUser = pool.getCurrentUser(); + t.true(currentUser instanceof MockCognitoUser); + t.is(currentUser.username, username); + + t.true(localStorage.getItem.calledOnce); + t.true(localStorage.getItem.calledWithExactly( + `CognitoIdentityServiceProvider.${pool.getClientId()}.LastAuthUser` + )); +}); From 6e8441912399326a16d44abf980809fb61a66ae6 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Tue, 30 Aug 2016 19:09:47 +1200 Subject: [PATCH 02/17] Factor tests, start CognitoUser tests --- src/CognitoUser.test.js | 60 +++++++++++++++++++++++++++ src/CognitoUserPool.test.js | 82 ++++++++++++++++++------------------- src/_testHelpers.js | 59 ++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 src/CognitoUser.test.js create mode 100644 src/_testHelpers.js diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js new file mode 100644 index 00000000..f8c9b809 --- /dev/null +++ b/src/CognitoUser.test.js @@ -0,0 +1,60 @@ +/* eslint-disable require-jsdoc */ + +import test from 'ava'; +import mockRequire from 'mock-require'; + +import { + requireDefaultWithModuleMocks, + requestFailsWith, + requestSucceedsWith, +} from './_testHelpers'; + +class MockUserPool { +} + +const USERNAME = 'some-username'; + +function createUser(pool = new MockUserPool()) { + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { + // Nothing yet + }); + return new CognitoUser({ Username: USERNAME, Pool: pool }); +} + +function constructorRequiredParams(t, data) { + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); + t.throws(() => new CognitoUser(data), /required/); +} +constructorRequiredParams.title = (_, data) => ( + `constructor(${JSON.stringify(data)}) => throws` +); +test(constructorRequiredParams, null); +test(constructorRequiredParams, {}); +test(constructorRequiredParams, { Username: null, Pool: null }); +test(constructorRequiredParams, { Username: null, Pool: new MockUserPool() }); +test(constructorRequiredParams, { Username: USERNAME, Pool: null }); + +test('constructor() :: valid => creates expected instance', t => { + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); + const pool = new MockUserPool(); + + const user = new CognitoUser({ Username: USERNAME, Pool: pool }); + + t.is(user.getSignInUserSession(), null); + t.is(user.getUsername(), USERNAME); + t.is(user.getAuthenticationFlowType(), 'USER_SRP_AUTH'); + t.is(user.pool, pool); +}); + +test('setAuthenticationFlowType() => sets authentication flow type', t => { + const user = createUser(); + const flowType = 'CUSTOM_AUTH'; + + user.setAuthenticationFlowType(flowType); + + t.is(user.getAuthenticationFlowType(), flowType); +}); + +test.todo('authenticateUser() :: USER_SRP_AUTH, succeeds => creates session'); +test.todo('authenticateUser() :: USER_SRP_AUTH, fails => raises onFailure'); +// ... lots more! diff --git a/src/CognitoUserPool.test.js b/src/CognitoUserPool.test.js index e661857e..38443794 100644 --- a/src/CognitoUserPool.test.js +++ b/src/CognitoUserPool.test.js @@ -2,13 +2,14 @@ import test from 'ava'; import { stub } from 'sinon'; -import mockRequire from 'mock-require'; -// Mock dependencies -class MockClient {} -mockRequire('aws-sdk', { - CognitoIdentityServiceProvider: MockClient, -}); +import { + MockClient, + requireDefaultWithModuleMocks, + stubClient, + requestFailsWith, + requestSucceedsWith, +} from './_testHelpers'; class MockCognitoUser { constructor({ Username, Pool }) { @@ -16,60 +17,55 @@ class MockCognitoUser { this.pool = Pool; } } -mockRequire('./CognitoUser', MockCognitoUser); -// Use with mocked dependencies -const CognitoUserPool = mockRequire.reRequire('./CognitoUserPool').default; +function requireCognitoUserPool() { + return requireDefaultWithModuleMocks('./CognitoUserPool', { + './CognitoUser': MockCognitoUser, + }); +} + +const POOL_DATA = { UserPoolId: '123', ClientId: 'abc' }; + +function createPool(extraData = {}) { + const CognitoUserPool = requireCognitoUserPool(); + return new CognitoUserPool(Object.assign({}, POOL_DATA, extraData)); +} +function createPoolWithClient(...requestConfigs) { + const pool = createPool(); + stubClient(pool.client, ...requestConfigs); + return pool; +} function constructorThrowsRequired(t, data) { + const CognitoUserPool = requireCognitoUserPool(); t.throws(() => new CognitoUserPool(data), /required/); } +constructorThrowsRequired.title = (originalTitle, data) => ( + `constructor( ${JSON.stringify(data)} ) => throws with "required"` +); test(constructorThrowsRequired, null); test(constructorThrowsRequired, {}); test(constructorThrowsRequired, { UserPoolId: null, ClientId: null }); test(constructorThrowsRequired, { UserPoolId: '123', ClientId: null }); test(constructorThrowsRequired, { UserPoolId: null, ClientId: 'abc' }); -constructorThrowsRequired.title = (originalTitle, data) => ( - `constructor( ${JSON.stringify(data)} ) => throws with "required"` -); test('constructor => creates instance with expected values', t => { - const data = { UserPoolId: '123', ClientId: 'abc' }; - const pool = new CognitoUserPool(data); + const pool = createPool(); t.truthy(pool); - t.is(pool.getUserPoolId(), data.UserPoolId); - t.is(pool.getClientId(), data.ClientId); + t.is(pool.getUserPoolId(), POOL_DATA.UserPoolId); + t.is(pool.getClientId(), POOL_DATA.ClientId); t.is(pool.getParanoia(), 0); t.true(pool.client instanceof MockClient); }); test('constructor({ Paranoia }) => sets paranoia', t => { - const data = { UserPoolId: '123', ClientId: 'abc', Paranoia: 7 }; - const pool = new CognitoUserPool(data); - t.is(pool.getParanoia(), data.Paranoia); + const paranoia = 7; + const pool = createPool({ Paranoia: paranoia }); + t.is(pool.getParanoia(), paranoia); }); -function create(...requestConfigs) { - const pool = new CognitoUserPool({ UserPoolId: '123', ClientId: 'abc' }); - const requestStub = stub(); - requestConfigs.forEach((requestConfig, i) => { - const expectation = requestStub.onCall(i); - expectation.yieldsAsync(...requestConfig); - }); - pool.client.makeUnauthenticatedRequest = requestStub; - return pool; -} - -function requestFailsWith(err) { - return [err, null]; -} - -function requestSucceedsWith(result) { - return [null, result]; -} - test('setParanoia() => sets paranoia', t => { - const pool = create(); + const pool = createPoolWithClient(); const paranoia = 7; pool.setParanoia(paranoia); t.is(pool.getParanoia(), paranoia); @@ -77,7 +73,7 @@ test('setParanoia() => sets paranoia', t => { test.cb('signUp() :: fails => callback gets error', t => { const expectedError = { code: 'SomeError' }; - const pool = create(requestFailsWith(expectedError)); + const pool = createPoolWithClient(requestFailsWith(expectedError)); pool.signUp('username', 'password', null, null, err => { t.is(err, expectedError); t.end(); @@ -86,7 +82,7 @@ test.cb('signUp() :: fails => callback gets error', t => { test.cb('signUp() :: success => callback gets user and confirmed', t => { const expectedUsername = 'username'; - const pool = create(requestSucceedsWith({ UserConfirmed: true })); + const pool = createPoolWithClient(requestSucceedsWith({ UserConfirmed: true })); pool.signUp(expectedUsername, 'password', null, null, (err, result) => { t.true(result.user instanceof MockCognitoUser); t.is(result.user.username, expectedUsername); @@ -97,7 +93,7 @@ test.cb('signUp() :: success => callback gets user and confirmed', t => { }); test('getCurrentUser() :: no last user => returns null', t => { - const pool = create(); + const pool = createPoolWithClient(); const localStorage = { getItem: stub().returns(null), }; @@ -112,7 +108,7 @@ test('getCurrentUser() :: no last user => returns null', t => { }); test('getCurrentUser() :: with last user => returns user instance', t => { - const pool = create(); + const pool = createPoolWithClient(); const username = 'username'; const localStorage = { getItem: stub().returns(username), diff --git a/src/_testHelpers.js b/src/_testHelpers.js new file mode 100644 index 00000000..98059902 --- /dev/null +++ b/src/_testHelpers.js @@ -0,0 +1,59 @@ +/* eslint-disable */ + +import test from 'ava'; +import { stub } from 'sinon'; +import mockRequire from 'mock-require'; + +export class MockClient {} + +export function mockAwsSdk() { + mockRequire('aws-sdk', { + CognitoIdentityServiceProvider: MockClient, + }); +} + +export function stubClient(client, ...requestConfigs) { + const requestStub = stub(); + requestConfigs.forEach((requestConfig, i) => { + const expectation = requestStub.onCall(i); + expectation.yieldsAsync(...requestConfig); + }); + client.makeUnauthenticatedRequest = requestStub; // eslint-disable-line no-param-reassign +} + +export function requestFailsWith(err) { + return [err, null]; +} + +export function requestSucceedsWith(result) { + return [null, result]; +} + +test.afterEach.always(() => { + mockRequire.stopAll(); + delete global.window; +}); + +function requireWithModuleMocks(request, moduleMocks = {}) { + function shouldReRequire(path) { + return path.indexOf(__dirname) !== 0; + } + + Object.keys(require.cache).forEach(path => { + if (path.indexOf(__dirname) === 0) { + delete require.cache[path]; + } + }); + + mockAwsSdk(); + + Object.keys(moduleMocks).forEach(name => { + mockRequire(name, moduleMocks[name]); + }); + + return require(request); +} + +export function requireDefaultWithModuleMocks(request, moduleMocks) { + return requireWithModuleMocks(request, moduleMocks).default; +} From 884e53bb380e01a2710b6d3519cffee4124832bf Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 5 Sep 2016 19:04:27 +1200 Subject: [PATCH 03/17] Add more CognitoUser tests, move todo tests for user auth. --- package.json | 3 +- src/CognitoUser.test.js | 179 +++++++++++++++++++++++++++++++++-- src/CognitoUser_auth.test.js | 37 ++++++++ 3 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 src/CognitoUser_auth.test.js diff --git a/package.json b/package.json index 03d0c66d..57586d8d 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "ava": { "require": [ "babel-register" - ] + ], + "timeout": "3s" } } diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index f8c9b809..e5208705 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -7,18 +7,39 @@ import { requireDefaultWithModuleMocks, requestFailsWith, requestSucceedsWith, + stubClient, } from './_testHelpers'; +const USERNAME = 'some-username'; +const CLIENT_ID = 'some-client-id'; +const ACCESS_TOKEN = 'some-access-token'; + class MockUserPool { + getClientId() { return CLIENT_ID; } } -const USERNAME = 'some-username'; +class MockSession { + isValid() { + return true; + } -function createUser(pool = new MockUserPool()) { + getAccessToken() { + return { + getJwtToken() { + return ACCESS_TOKEN; + }, + }; + } +} + +function createUser({ pool = new MockUserPool(), session } = {}, ...requests) { const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { // Nothing yet }); - return new CognitoUser({ Username: USERNAME, Pool: pool }); + const user = new CognitoUser({ Username: USERNAME, Pool: pool }); + stubClient(user.client, ...requests); + user.signInUserSession = session; + return user; } function constructorRequiredParams(t, data) { @@ -55,6 +76,152 @@ test('setAuthenticationFlowType() => sets authentication flow type', t => { t.is(user.getAuthenticationFlowType(), flowType); }); -test.todo('authenticateUser() :: USER_SRP_AUTH, succeeds => creates session'); -test.todo('authenticateUser() :: USER_SRP_AUTH, fails => raises onFailure'); -// ... lots more! +// See CognitoUser_auth.test.js for authenticateUser() and the challenge responses + +function createExpectedErrorFromSuccess(succeeds) { + return succeeds ? null : { code: 'InternalServerException' }; +} + +function requestCalledWithOnCall(t, client, call, ...expectedArgs) { + const actualArgsExceptCallback = client.makeUnauthenticatedRequest.args[call].slice(); + t.true(typeof actualArgsExceptCallback.pop() === 'function'); + t.deepEqual(actualArgsExceptCallback, expectedArgs); +} + +function requestCalledOnceWith(t, client, ...expectedArgs) { + t.true(client.makeUnauthenticatedRequest.callCount === 1); + requestCalledWithOnCall(t, client, 0, ...expectedArgs); +} + +function confirmRegistrationMacro(t, forceAliasCreation, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createUser({}, [expectedError]); + + const confirmationCode = '123456'; + user.confirmRegistration(confirmationCode, forceAliasCreation, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'confirmSignUp', { + ClientId: CLIENT_ID, + ConfirmationCode: confirmationCode, + Username: USERNAME, + ForceAliasCreation: forceAliasCreation, + }); + t.end(); + }); +} +confirmRegistrationMacro.title = (_, forceAliasCreation, succeeds) => ( + `confirmRegistration(forceAliasCreation: ${forceAliasCreation}) :: ${ + succeeds ? 'succeeds' : 'fails' + }` +); +test.cb(confirmRegistrationMacro, false, false); +test.cb(confirmRegistrationMacro, true, false); +test.cb(confirmRegistrationMacro, false, true); +test.cb(confirmRegistrationMacro, true, true); + +function createSignedInUserWithExpectedError(expectedError) { + return createUser({ session: new MockSession() }, [expectedError]); +} + +function changePasswordMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const oldUserPassword = 'swordfish'; + const newUserPassword = 'slaughterfish'; + user.changePassword(oldUserPassword, newUserPassword, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'changePassword', { + PreviousPassword: oldUserPassword, + ProposedPassword: newUserPassword, + AccessToken: ACCESS_TOKEN, + }); + t.end(); + }); +} +changePasswordMacro.title = (_, succeeds) => ( + `changePassword() :: ${succeeds ? 'succeeds' : 'fails'}` +); +test.cb(changePasswordMacro, false); +test.cb(changePasswordMacro, true); + +function enableMFAMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.enableMFA(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'setUserSettings', { + MFAOptions: [ + { DeliveryMedium: 'SMS', AttributeName: 'phone_number' }, + ], + AccessToken: ACCESS_TOKEN, + }); + t.end(); + }); +} +enableMFAMacro.title = (_, succeeds) => ( + `enableMFA() :: ${succeeds ? 'succeeds' : 'fails'}` +); +test.cb(enableMFAMacro, false); +test.cb(enableMFAMacro, true); + +function disableMFAMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.disableMFA(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'setUserSettings', { + MFAOptions: [], + AccessToken: ACCESS_TOKEN, + }); + t.end(); + }); +} +disableMFAMacro.title = (_, succeeds) => ( + `disableMFA() :: ${succeeds ? 'succeeds' : 'fails'}` +); +test.cb(disableMFAMacro, false); +test.cb(disableMFAMacro, true); + +function deleteUserMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.deleteUser(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'deleteUser', { + AccessToken: ACCESS_TOKEN, + }); + t.end(); + }); +} +deleteUserMacro.title = (_, succeeds) => ( + `deleteUser() :: ${succeeds ? 'succeeds' : 'fails'}` +); +test.cb(deleteUserMacro, false); +test.cb(deleteUserMacro, true); + +function updateAttributesMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const attributes = [ + { Name: 'some_name', Value: 'some_value' }, + ]; + + user.updateAttributes(attributes, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'updateUserAttributes', { + UserAttributes: attributes, + AccessToken: ACCESS_TOKEN, + }); + t.end(); + }); +} +updateAttributesMacro.title = (_, succeeds) => ( + `updateAttributes() :: ${succeeds ? 'succeeds' : 'fails'}` +); +test.cb(updateAttributesMacro, false); +test.cb(updateAttributesMacro, true); diff --git a/src/CognitoUser_auth.test.js b/src/CognitoUser_auth.test.js new file mode 100644 index 00000000..6e537828 --- /dev/null +++ b/src/CognitoUser_auth.test.js @@ -0,0 +1,37 @@ +/* eslint-disable require-jsdoc */ + +import test from 'ava'; +import mockRequire from 'mock-require'; + +import { + requireDefaultWithModuleMocks, + requestFailsWith, + requestSucceedsWith, +} from './_testHelpers'; + +class MockUserPool { +} + +const USERNAME = 'some-username'; + +function createUser(pool = new MockUserPool()) { + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { + // Nothing yet + }); + return new CognitoUser({ Username: USERNAME, Pool: pool }); +} + +test.todo('authenticateUser() :: USER_SRP_AUTH, succeeds => creates session'); +test.todo('authenticateUser() :: USER_SRP_AUTH, initiateAuth fails => raises onFailure'); +test.todo('authenticateUser() :: USER_SRP_AUTH, respondToAuthChallenge fails => raises onFailure'); +// ... other flows +test.todo('authenticateUser() :: MFA required => calls mfaRequired'); +test.todo('authenticateUser() :: custom challenge => calls customChallenge'); + +test.todo('getDeviceResponse() :: DEVICE_SRP_AUTH fails => calls onFailure'); +test.todo('getDeviceResponse() :: DEVICE_PASSWORD_VERIFIER fails => calls onFailure'); +test.todo('getDeviceResponse() :: succeeds => signs in and calls onSuccess'); + +test.todo('sendCustomChallengeAnswer() :: fails => calls onFailure'); +test.todo('sendCustomChallengeAnswer() :: succeeds => calls onSuccess'); + From 9064688414de27a888b5dd363ed29165a98c0d19 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Fri, 16 Sep 2016 18:12:42 +1200 Subject: [PATCH 04/17] Get tests working with aws-sdk webpack changes, add coverage --- .babelrc | 10 +++++- .gitignore | 4 +++ package.json | 13 ++++++-- src/CognitoUser.test.js | 12 +++---- src/CognitoUserPool.test.js | 15 +++++---- src/CognitoUser_auth.test.js | 2 +- src/{_testHelpers.js => _helpers.test.js} | 39 ++++++++++++++++------- 7 files changed, 65 insertions(+), 30 deletions(-) rename src/{_testHelpers.js => _helpers.test.js} (56%) diff --git a/.babelrc b/.babelrc index 02f08fb6..562a2e67 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,13 @@ { "presets": [ "es2015" - ] + ], + "env": { + "nyc": { + "sourceMaps": "inline", + "plugins": [ + "istanbul" + ] + } + } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index b4a9ef9b..576ca9e8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ npm-debug.log /docs/ + +/.nyc_output/ +/coverage/ + diff --git a/package.json b/package.json index 57586d8d..044e2b81 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "build": "webpack -p", "doc": "jsdoc src -d docs", "lint": "eslint src", - "test": "ava" + "test": "cross-env BABEL_ENV=nyc nyc ava", + "coverage": "nyc report --reporter=lcov" }, "main": "dist/amazon-cognito-identity.min.js", "dependencies": { @@ -45,14 +46,17 @@ "ava": "^0.16.0", "babel-core": "^6.13.2", "babel-loader": "^6.2.4", + "babel-plugin-istanbul": "^2.0.1", "babel-preset-es2015": "^6.13.2", "babel-register": "^6.14.0", + "cross-env": "^2.0.1", "eslint": "^3.3.1", "eslint-config-airbnb-base": "^5.0.2", "eslint-import-resolver-webpack": "^0.5.1", "eslint-plugin-import": "^1.13.0", "jsdoc": "^3.4.0", "mock-require": "^1.3.0", + "nyc": "^8.3.0", "sinon": "^1.17.5", "webpack": "^1.13.1" }, @@ -60,6 +64,11 @@ "require": [ "babel-register" ], - "timeout": "3s" + "timeout": "30s" + }, + "nyc": { + "cache": true, + "sourceMap": false, + "instrument": false } } diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index e5208705..1f925cfc 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -1,20 +1,18 @@ /* eslint-disable require-jsdoc */ import test from 'ava'; -import mockRequire from 'mock-require'; -import { - requireDefaultWithModuleMocks, - requestFailsWith, - requestSucceedsWith, - stubClient, -} from './_testHelpers'; +import { MockClient, requireDefaultWithModuleMocks, stubClient } from './_helpers.test'; const USERNAME = 'some-username'; const CLIENT_ID = 'some-client-id'; const ACCESS_TOKEN = 'some-access-token'; class MockUserPool { + constructor() { + this.client = new MockClient(); + } + getClientId() { return CLIENT_ID; } } diff --git a/src/CognitoUserPool.test.js b/src/CognitoUserPool.test.js index 38443794..a3c40a79 100644 --- a/src/CognitoUserPool.test.js +++ b/src/CognitoUserPool.test.js @@ -9,7 +9,7 @@ import { stubClient, requestFailsWith, requestSucceedsWith, -} from './_testHelpers'; +} from './_helpers.test'; class MockCognitoUser { constructor({ Username, Pool }) { @@ -24,11 +24,12 @@ function requireCognitoUserPool() { }); } -const POOL_DATA = { UserPoolId: '123', ClientId: 'abc' }; +const UserPoolId = 'xx-nowhere1_SomeUserPool'; // Constructor validates the format. +const ClientId = 'some-client-id'; function createPool(extraData = {}) { const CognitoUserPool = requireCognitoUserPool(); - return new CognitoUserPool(Object.assign({}, POOL_DATA, extraData)); + return new CognitoUserPool(Object.assign({}, { UserPoolId, ClientId }, extraData)); } function createPoolWithClient(...requestConfigs) { @@ -47,13 +48,13 @@ constructorThrowsRequired.title = (originalTitle, data) => ( test(constructorThrowsRequired, null); test(constructorThrowsRequired, {}); test(constructorThrowsRequired, { UserPoolId: null, ClientId: null }); -test(constructorThrowsRequired, { UserPoolId: '123', ClientId: null }); -test(constructorThrowsRequired, { UserPoolId: null, ClientId: 'abc' }); +test(constructorThrowsRequired, { UserPoolId, ClientId: null }); +test(constructorThrowsRequired, { UserPoolId: null, ClientId }); test('constructor => creates instance with expected values', t => { const pool = createPool(); t.truthy(pool); - t.is(pool.getUserPoolId(), POOL_DATA.UserPoolId); - t.is(pool.getClientId(), POOL_DATA.ClientId); + t.is(pool.getUserPoolId(), UserPoolId); + t.is(pool.getClientId(), ClientId); t.is(pool.getParanoia(), 0); t.true(pool.client instanceof MockClient); }); diff --git a/src/CognitoUser_auth.test.js b/src/CognitoUser_auth.test.js index 6e537828..8678ceb7 100644 --- a/src/CognitoUser_auth.test.js +++ b/src/CognitoUser_auth.test.js @@ -7,7 +7,7 @@ import { requireDefaultWithModuleMocks, requestFailsWith, requestSucceedsWith, -} from './_testHelpers'; +} from './_helpers.test'; class MockUserPool { } diff --git a/src/_testHelpers.js b/src/_helpers.test.js similarity index 56% rename from src/_testHelpers.js rename to src/_helpers.test.js index 98059902..f89c0de0 100644 --- a/src/_testHelpers.js +++ b/src/_helpers.test.js @@ -6,13 +6,10 @@ import mockRequire from 'mock-require'; export class MockClient {} -export function mockAwsSdk() { - mockRequire('aws-sdk', { - CognitoIdentityServiceProvider: MockClient, - }); -} - export function stubClient(client, ...requestConfigs) { + if (!(client instanceof MockClient)) { + throw new Error(`'client' is not a MockClient: ${client}`) + } const requestStub = stub(); requestConfigs.forEach((requestConfig, i) => { const expectation = requestStub.onCall(i); @@ -34,24 +31,42 @@ test.afterEach.always(() => { delete global.window; }); +function shouldMock(path) { + return path.startsWith(__dirname) && !path.endsWith('.test.js'); +} + function requireWithModuleMocks(request, moduleMocks = {}) { - function shouldReRequire(path) { - return path.indexOf(__dirname) !== 0; - } + const unmockedCache = Object.create(null); + // Remove require.cache entries that may be using the unmocked modules Object.keys(require.cache).forEach(path => { - if (path.indexOf(__dirname) === 0) { + if (shouldMock(path)) { delete require.cache[path]; } }); - mockAwsSdk(); + // Always mock AWS SDK + mockRequire('aws-sdk/clients/cognitoidentityserviceprovider', MockClient); + // Mock other modules Object.keys(moduleMocks).forEach(name => { mockRequire(name, moduleMocks[name]); }); - return require(request); + const mockedModule = require(request); + + // Restore require.cache to previous state + Object.keys(require.cache).forEach(path => { + if (shouldMock(path)) { + if (Object.prototype.hasOwnProperty.call(unmockedCache, path)) { + require.cache[path] = unmockedCache[path]; + } else { + delete require.cache[path]; + } + } + }); + + return mockedModule; } export function requireDefaultWithModuleMocks(request, moduleMocks) { From e2c09e065660eba0e18e926ca68a3281d9802ce4 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Fri, 16 Sep 2016 19:07:53 +1200 Subject: [PATCH 05/17] Test invalid UserPoolId to hit 100% in CognitoUserPool. --- src/CognitoUserPool.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/CognitoUserPool.test.js b/src/CognitoUserPool.test.js index a3c40a79..82716f3b 100644 --- a/src/CognitoUserPool.test.js +++ b/src/CognitoUserPool.test.js @@ -50,6 +50,13 @@ test(constructorThrowsRequired, {}); test(constructorThrowsRequired, { UserPoolId: null, ClientId: null }); test(constructorThrowsRequired, { UserPoolId, ClientId: null }); test(constructorThrowsRequired, { UserPoolId: null, ClientId }); + +test('constructor :: invalid UserPoolId => throws with "Invalid UserPoolId"', t => { + const CognitoUserPool = requireCognitoUserPool(); + const data = { UserPoolId: 'invalid-user-pool-id', ClientId }; + t.throws(() => new CognitoUserPool(data), /Invalid UserPoolId/); +}); + test('constructor => creates instance with expected values', t => { const pool = createPool(); t.truthy(pool); From 4fe5ae9d6d868111c7d9c37995bae391d9331995 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Fri, 16 Sep 2016 20:50:35 +1200 Subject: [PATCH 06/17] Split up test / coverage npm scripts. --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 044e2b81..a91305b0 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,10 @@ "build": "webpack -p", "doc": "jsdoc src -d docs", "lint": "eslint src", - "test": "cross-env BABEL_ENV=nyc nyc ava", - "coverage": "nyc report --reporter=lcov" + "test": "ava", + "coverage": "cross-env BABEL_ENV=nyc nyc --reporter=text-summary --reporter=text --reporter=lcov ava", + "coverage:test": "cross-env BABEL_ENV=nyc nyc ava", + "coverage:report": "nyc report --reporter=lcov" }, "main": "dist/amazon-cognito-identity.min.js", "dependencies": { From 810eb9bc43eef6eea46a11cfae5fbfb63466763e Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Fri, 16 Sep 2016 20:50:55 +1200 Subject: [PATCH 07/17] More coverage on CognitoUser: 50.77% (33/65) functions. --- src/CognitoUser.test.js | 365 ++++++++++++++++++++++++++++++++-------- 1 file changed, 298 insertions(+), 67 deletions(-) diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index 1f925cfc..aae0cd67 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -4,16 +4,36 @@ import test from 'ava'; import { MockClient, requireDefaultWithModuleMocks, stubClient } from './_helpers.test'; -const USERNAME = 'some-username'; -const CLIENT_ID = 'some-client-id'; -const ACCESS_TOKEN = 'some-access-token'; +// Valid property values: constructor, request props, etc... +const Username = 'some-username'; +const ClientId = 'some-client-id'; +const AccessToken = 'some-access-token'; +const CodeDeliveryDetails = { + Destination: 'some-destination', + DeliveryMedium: 'some-medium', + AttributeName: 'some-attribute', +}; + +// Valid arguments +const attributeName = 'some-attribute-name'; +const confirmationCode = '123456'; +const attributes = [ + { Name: 'some-attribute-name-1', Value: 'some-attribute-value-1' }, + { Name: 'some-attribute-name-2', Value: 'some-attribute-value-2' }, +]; class MockUserPool { constructor() { this.client = new MockClient(); } - getClientId() { return CLIENT_ID; } + getClientId() { + return ClientId; + } + + toJSON() { + return '[mock UserPool]'; + } } class MockSession { @@ -24,7 +44,7 @@ class MockSession { getAccessToken() { return { getJwtToken() { - return ACCESS_TOKEN; + return AccessToken; }, }; } @@ -34,33 +54,80 @@ function createUser({ pool = new MockUserPool(), session } = {}, ...requests) { const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { // Nothing yet }); - const user = new CognitoUser({ Username: USERNAME, Pool: pool }); + const user = new CognitoUser({ Username, Pool: pool }); stubClient(user.client, ...requests); user.signInUserSession = session; return user; } -function constructorRequiredParams(t, data) { +function createSignedInUser(...requests) { + return createUser({ session: new MockSession() }, ...requests); +} + +function createSignedInUserWithExpectedError(expectedError) { + return createSignedInUser([expectedError]); +} + +function createExpectedErrorFromSuccess(succeeds) { + return succeeds ? null : { code: 'InternalServerException' }; +} + +function requestCalledWithOnCall(t, client, call, ...expectedArgs) { + const actualArgsExceptCallback = client.makeUnauthenticatedRequest.args[call].slice(); + t.true(typeof actualArgsExceptCallback.pop() === 'function'); + t.deepEqual(actualArgsExceptCallback, expectedArgs); +} + +function requestCalledOnceWith(t, client, ...expectedArgs) { + t.true(client.makeUnauthenticatedRequest.callCount === 1); + requestCalledWithOnCall(t, client, 0, ...expectedArgs); +} + +function titleMapString(value) { + return value && typeof value === 'object' + ? Object.keys(value).map(key => `${key}: ${JSON.stringify(value[key])}`).join(', ') + : value || ''; +} + +function title(fn, { args, context, succeeds, outcome = succeeds ? 'succeeds' : 'fails' }) { + const fnString = typeof fn === 'function' ? fn.name.replace(/Macro$/, '') : fn; + const contextString = context ? ` :: ${titleMapString(context)}` : ''; + return `${fnString}(${titleMapString(args)})${contextString} => ${outcome}`; +} + +function addSimpleTitle(macro, { args, context } = {}) { + // eslint-disable-next-line no-param-reassign + macro.title = (_, succeeds, ...values) => ( + title(macro, { + succeeds, + args: args && args(...values), + context: context && context(...values), + }) + ); +} + + +function constructorRequiredParamsMacro(t, data) { const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); t.throws(() => new CognitoUser(data), /required/); } -constructorRequiredParams.title = (_, data) => ( - `constructor(${JSON.stringify(data)}) => throws` +constructorRequiredParamsMacro.title = (_, data) => ( + title('constructor', { args: data, outcome: 'throws "required"' }) ); -test(constructorRequiredParams, null); -test(constructorRequiredParams, {}); -test(constructorRequiredParams, { Username: null, Pool: null }); -test(constructorRequiredParams, { Username: null, Pool: new MockUserPool() }); -test(constructorRequiredParams, { Username: USERNAME, Pool: null }); +test(constructorRequiredParamsMacro, null); +test(constructorRequiredParamsMacro, {}); +test(constructorRequiredParamsMacro, { Username: null, Pool: null }); +test(constructorRequiredParamsMacro, { Username: null, Pool: new MockUserPool() }); +test(constructorRequiredParamsMacro, { Username, Pool: null }); test('constructor() :: valid => creates expected instance', t => { const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); const pool = new MockUserPool(); - const user = new CognitoUser({ Username: USERNAME, Pool: pool }); + const user = new CognitoUser({ Username, Pool: pool }); t.is(user.getSignInUserSession(), null); - t.is(user.getUsername(), USERNAME); + t.is(user.getUsername(), Username); t.is(user.getAuthenticationFlowType(), 'USER_SRP_AUTH'); t.is(user.pool, pool); }); @@ -76,51 +143,29 @@ test('setAuthenticationFlowType() => sets authentication flow type', t => { // See CognitoUser_auth.test.js for authenticateUser() and the challenge responses -function createExpectedErrorFromSuccess(succeeds) { - return succeeds ? null : { code: 'InternalServerException' }; -} - -function requestCalledWithOnCall(t, client, call, ...expectedArgs) { - const actualArgsExceptCallback = client.makeUnauthenticatedRequest.args[call].slice(); - t.true(typeof actualArgsExceptCallback.pop() === 'function'); - t.deepEqual(actualArgsExceptCallback, expectedArgs); -} - -function requestCalledOnceWith(t, client, ...expectedArgs) { - t.true(client.makeUnauthenticatedRequest.callCount === 1); - requestCalledWithOnCall(t, client, 0, ...expectedArgs); -} - function confirmRegistrationMacro(t, forceAliasCreation, succeeds) { const expectedError = createExpectedErrorFromSuccess(succeeds); const user = createUser({}, [expectedError]); - const confirmationCode = '123456'; user.confirmRegistration(confirmationCode, forceAliasCreation, err => { t.is(err, expectedError); requestCalledOnceWith(t, user.client, 'confirmSignUp', { - ClientId: CLIENT_ID, + ClientId, ConfirmationCode: confirmationCode, - Username: USERNAME, + Username, ForceAliasCreation: forceAliasCreation, }); t.end(); }); } confirmRegistrationMacro.title = (_, forceAliasCreation, succeeds) => ( - `confirmRegistration(forceAliasCreation: ${forceAliasCreation}) :: ${ - succeeds ? 'succeeds' : 'fails' - }` + title(confirmRegistrationMacro, { succeeds, args: { forceAliasCreation } }) ); test.cb(confirmRegistrationMacro, false, false); test.cb(confirmRegistrationMacro, true, false); test.cb(confirmRegistrationMacro, false, true); test.cb(confirmRegistrationMacro, true, true); -function createSignedInUserWithExpectedError(expectedError) { - return createUser({ session: new MockSession() }, [expectedError]); -} - function changePasswordMacro(t, succeeds) { const expectedError = createExpectedErrorFromSuccess(succeeds); const user = createSignedInUserWithExpectedError(expectedError); @@ -132,14 +177,12 @@ function changePasswordMacro(t, succeeds) { requestCalledOnceWith(t, user.client, 'changePassword', { PreviousPassword: oldUserPassword, ProposedPassword: newUserPassword, - AccessToken: ACCESS_TOKEN, + AccessToken, }); t.end(); }); } -changePasswordMacro.title = (_, succeeds) => ( - `changePassword() :: ${succeeds ? 'succeeds' : 'fails'}` -); +addSimpleTitle(changePasswordMacro); test.cb(changePasswordMacro, false); test.cb(changePasswordMacro, true); @@ -153,14 +196,12 @@ function enableMFAMacro(t, succeeds) { MFAOptions: [ { DeliveryMedium: 'SMS', AttributeName: 'phone_number' }, ], - AccessToken: ACCESS_TOKEN, + AccessToken, }); t.end(); }); } -enableMFAMacro.title = (_, succeeds) => ( - `enableMFA() :: ${succeeds ? 'succeeds' : 'fails'}` -); +addSimpleTitle(enableMFAMacro); test.cb(enableMFAMacro, false); test.cb(enableMFAMacro, true); @@ -172,14 +213,12 @@ function disableMFAMacro(t, succeeds) { t.is(err, expectedError); requestCalledOnceWith(t, user.client, 'setUserSettings', { MFAOptions: [], - AccessToken: ACCESS_TOKEN, + AccessToken, }); t.end(); }); } -disableMFAMacro.title = (_, succeeds) => ( - `disableMFA() :: ${succeeds ? 'succeeds' : 'fails'}` -); +addSimpleTitle(disableMFAMacro); test.cb(disableMFAMacro, false); test.cb(disableMFAMacro, true); @@ -189,15 +228,11 @@ function deleteUserMacro(t, succeeds) { user.deleteUser(err => { t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'deleteUser', { - AccessToken: ACCESS_TOKEN, - }); + requestCalledOnceWith(t, user.client, 'deleteUser', { AccessToken }); t.end(); }); } -deleteUserMacro.title = (_, succeeds) => ( - `deleteUser() :: ${succeeds ? 'succeeds' : 'fails'}` -); +addSimpleTitle(deleteUserMacro); test.cb(deleteUserMacro, false); test.cb(deleteUserMacro, true); @@ -205,21 +240,217 @@ function updateAttributesMacro(t, succeeds) { const expectedError = createExpectedErrorFromSuccess(succeeds); const user = createSignedInUserWithExpectedError(expectedError); - const attributes = [ - { Name: 'some_name', Value: 'some_value' }, - ]; - user.updateAttributes(attributes, err => { t.is(err, expectedError); requestCalledOnceWith(t, user.client, 'updateUserAttributes', { UserAttributes: attributes, - AccessToken: ACCESS_TOKEN, + AccessToken, }); t.end(); }); } -updateAttributesMacro.title = (_, succeeds) => ( - `updateAttributes() :: ${succeeds ? 'succeeds' : 'fails'}` -); +addSimpleTitle(updateAttributesMacro); test.cb(updateAttributesMacro, false); test.cb(updateAttributesMacro, true); + +function getUserAttributesMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const responseResult = succeeds ? { UserAttributes: attributes } : null; + const user = createSignedInUser([expectedError, responseResult]); + + user.getUserAttributes((err, result) => { + t.is(err, expectedError); + if (succeeds) { + // Only check for Name, Value properties, don't assert the results are CognitoUserAttributes. + t.deepEqual(result.map(({ Name, Value }) => ({ Name, Value })), attributes); + } else { + t.falsy(result); + } + requestCalledOnceWith(t, user.client, 'getUser', { AccessToken }); + t.end(); + }); +} +addSimpleTitle(getUserAttributesMacro); +test.cb(getUserAttributesMacro, false); +test.cb(getUserAttributesMacro, true); + +function deleteAttributesMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const attributeList = attributes.map(a => a.Name); + + user.deleteAttributes(attributeList, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'deleteUserAttributes', { + UserAttributeNames: attributeList, + AccessToken, + }); + t.end(); + }); +} +addSimpleTitle(deleteAttributesMacro); +test.cb(deleteAttributesMacro, false); +test.cb(deleteAttributesMacro, true); + +function resendConfirmationCodeMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.resendConfirmationCode(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'resendConfirmationCode', { ClientId, Username }); + t.end(); + }); +} +addSimpleTitle(resendConfirmationCodeMacro); +test.cb(resendConfirmationCodeMacro, false); +test.cb(resendConfirmationCodeMacro, true); + +function createCallback(t, done, callbackTests) { + const callback = {}; + Object.keys(callbackTests).forEach(key => { + callback[key] = function testCallbackMethod(...args) { + t.is(this, callback); + callbackTests[key](...args); + done(); + }; + }); + return callback; +} + +function createBasicCallback(t, succeeds, expectedError, done) { + return createCallback(t, done, { + onFailure(err) { + t.false(succeeds); + t.is(err, expectedError); + }, + onSuccess() { + t.true(succeeds); + }, + }); +} + +function forgotPasswordMacro(t, succeeds, usingInputVerificationCode) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const expectedData = !succeeds ? null : { CodeDeliveryDetails }; + const request = [expectedError, expectedData]; + const user = createSignedInUser(request); + + function done() { + requestCalledOnceWith(t, user.client, 'forgotPassword', { ClientId, Username }); + t.end(); + } + + const callback = !usingInputVerificationCode ? + createBasicCallback(t, succeeds, expectedError, done) : + createCallback(t, done, { + onFailure(err) { + t.false(succeeds); + t.is(err, expectedError); + }, + inputVerificationCode(data) { + t.true(succeeds); + t.is(data, expectedData); + }, + }); + + user.forgotPassword(callback); +} +addSimpleTitle(forgotPasswordMacro, { + context(usingInputVerificationCode) { + return { usingInputVerificationCode }; + }, +}); +test.cb(forgotPasswordMacro, false, false); +test.cb(forgotPasswordMacro, true, false); +test.cb(forgotPasswordMacro, false, true); +test.cb(forgotPasswordMacro, true, true); + +function confirmPasswordMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const confirmationCode = '123456'; + const newPassword = 'swordfish'; + const callback = createBasicCallback(t, succeeds, expectedError, () => { + requestCalledOnceWith(t, user.client, 'confirmForgotPassword', { + ClientId, + Username, + ConfirmationCode: confirmationCode, + Password: newPassword, + }); + t.end(); + }); + user.confirmPassword(confirmationCode, newPassword, callback); +} +addSimpleTitle(confirmPasswordMacro); +test.cb(confirmPasswordMacro, false); +test.cb(confirmPasswordMacro, true); + +function getAttributeVerificationCodeMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const expectedData = !succeeds ? null : { CodeDeliveryDetails }; + const user = createSignedInUser([expectedError, expectedData]); + + const attributeName = 'some-attribute-name'; + function done() { + requestCalledOnceWith(t, user.client, 'getUserAttributeVerificationCode', { + AttributeName: attributeName, + AccessToken, + }); + t.end(); + } + user.getAttributeVerificationCode( + attributeName, + createCallback(t, done, { + onFailure(err) { + t.false(succeeds); + t.is(err, expectedError); + }, + inputVerificationCode(data) { + t.true(succeeds); + t.is(data, expectedData); + }, + })); +} +addSimpleTitle(getAttributeVerificationCodeMacro); +test.cb(getAttributeVerificationCodeMacro, false); +test.cb(getAttributeVerificationCodeMacro, true); + +function verifyAttributeMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const callback = createBasicCallback(t, succeeds, expectedError, () => { + requestCalledOnceWith(t, user.client, 'verifyUserAttribute', { + AttributeName: attributeName, + Code: confirmationCode, + AccessToken, + }); + t.end(); + }); + user.verifyAttribute(attributeName, confirmationCode, callback); +} +addSimpleTitle(verifyAttributeMacro); +test.cb(verifyAttributeMacro, false); +test.cb(verifyAttributeMacro, true); + +function getDeviceMacro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + const expectedDeviceKey = 'some-device-key'; + user.deviceKey = expectedDeviceKey; + + const callback = createBasicCallback(t, succeeds, expectedError, () => { + requestCalledOnceWith(t, user.client, 'getDevice', { + AccessToken, + DeviceKey: expectedDeviceKey, + }); + t.end(); + }); + user.getDevice(callback); +} +addSimpleTitle(getDeviceMacro); +test.cb(getDeviceMacro, false); +test.cb(getDeviceMacro, true); From ca73e3e959fac82c01a444dbb983078fb89c486a Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Wed, 21 Sep 2016 21:36:01 +1200 Subject: [PATCH 08/17] Initial authenticateUser test! --- src/CognitoUser.test.js | 49 +++---------- src/CognitoUserPool.test.js | 3 +- src/CognitoUser_auth.test.js | 137 +++++++++++++++++++++++++++++++++-- src/_helpers.test.js | 61 +++++++++++++--- 4 files changed, 190 insertions(+), 60 deletions(-) diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index aae0cd67..faf8e0bd 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -2,7 +2,13 @@ import test from 'ava'; -import { MockClient, requireDefaultWithModuleMocks, stubClient } from './_helpers.test'; +import { + MockClient, + requireDefaultWithModuleMocks, + requestCalledOnceWith, + createCallback, + createBasicCallback, +} from './_helpers.test'; // Valid property values: constructor, request props, etc... const Username = 'some-username'; @@ -50,12 +56,12 @@ class MockSession { } } -function createUser({ pool = new MockUserPool(), session } = {}, ...requests) { +function createUser({ pool = new MockUserPool(), session } = {}, ...requestConfigs) { + pool.client = new MockClient(...requestConfigs); // eslint-disable-line no-param-reassign const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { // Nothing yet }); const user = new CognitoUser({ Username, Pool: pool }); - stubClient(user.client, ...requests); user.signInUserSession = session; return user; } @@ -72,17 +78,6 @@ function createExpectedErrorFromSuccess(succeeds) { return succeeds ? null : { code: 'InternalServerException' }; } -function requestCalledWithOnCall(t, client, call, ...expectedArgs) { - const actualArgsExceptCallback = client.makeUnauthenticatedRequest.args[call].slice(); - t.true(typeof actualArgsExceptCallback.pop() === 'function'); - t.deepEqual(actualArgsExceptCallback, expectedArgs); -} - -function requestCalledOnceWith(t, client, ...expectedArgs) { - t.true(client.makeUnauthenticatedRequest.callCount === 1); - requestCalledWithOnCall(t, client, 0, ...expectedArgs); -} - function titleMapString(value) { return value && typeof value === 'object' ? Object.keys(value).map(key => `${key}: ${JSON.stringify(value[key])}`).join(', ') @@ -307,30 +302,6 @@ addSimpleTitle(resendConfirmationCodeMacro); test.cb(resendConfirmationCodeMacro, false); test.cb(resendConfirmationCodeMacro, true); -function createCallback(t, done, callbackTests) { - const callback = {}; - Object.keys(callbackTests).forEach(key => { - callback[key] = function testCallbackMethod(...args) { - t.is(this, callback); - callbackTests[key](...args); - done(); - }; - }); - return callback; -} - -function createBasicCallback(t, succeeds, expectedError, done) { - return createCallback(t, done, { - onFailure(err) { - t.false(succeeds); - t.is(err, expectedError); - }, - onSuccess() { - t.true(succeeds); - }, - }); -} - function forgotPasswordMacro(t, succeeds, usingInputVerificationCode) { const expectedError = createExpectedErrorFromSuccess(succeeds); const expectedData = !succeeds ? null : { CodeDeliveryDetails }; @@ -371,7 +342,6 @@ function confirmPasswordMacro(t, succeeds) { const expectedError = createExpectedErrorFromSuccess(succeeds); const user = createSignedInUserWithExpectedError(expectedError); - const confirmationCode = '123456'; const newPassword = 'swordfish'; const callback = createBasicCallback(t, succeeds, expectedError, () => { requestCalledOnceWith(t, user.client, 'confirmForgotPassword', { @@ -393,7 +363,6 @@ function getAttributeVerificationCodeMacro(t, succeeds) { const expectedData = !succeeds ? null : { CodeDeliveryDetails }; const user = createSignedInUser([expectedError, expectedData]); - const attributeName = 'some-attribute-name'; function done() { requestCalledOnceWith(t, user.client, 'getUserAttributeVerificationCode', { AttributeName: attributeName, diff --git a/src/CognitoUserPool.test.js b/src/CognitoUserPool.test.js index 82716f3b..625938e4 100644 --- a/src/CognitoUserPool.test.js +++ b/src/CognitoUserPool.test.js @@ -6,7 +6,6 @@ import { stub } from 'sinon'; import { MockClient, requireDefaultWithModuleMocks, - stubClient, requestFailsWith, requestSucceedsWith, } from './_helpers.test'; @@ -34,7 +33,7 @@ function createPool(extraData = {}) { function createPoolWithClient(...requestConfigs) { const pool = createPool(); - stubClient(pool.client, ...requestConfigs); + pool.client = new MockClient(...requestConfigs); return pool; } diff --git a/src/CognitoUser_auth.test.js b/src/CognitoUser_auth.test.js index 8678ceb7..7406d898 100644 --- a/src/CognitoUser_auth.test.js +++ b/src/CognitoUser_auth.test.js @@ -1,27 +1,150 @@ /* eslint-disable require-jsdoc */ import test from 'ava'; -import mockRequire from 'mock-require'; +import { stub } from 'sinon'; +import { BigInteger } from 'jsbn'; +import * as sjcl from 'sjcl'; import { + MockClient, requireDefaultWithModuleMocks, - requestFailsWith, requestSucceedsWith, + createCallback, + requestCalledWithOnCall, } from './_helpers.test'; +import AuthenticationDetails from './AuthenticationDetails'; + +const UserPoolId = 'xx-nowhere1_SomeUserPool'; // Constructor validates the format. +const ClientId = 'some-client-id'; +const Username = 'some-username'; +const Password = 'swordfish'; +const IdToken = 'some-id-token'; +const RefreshToken = 'some-refresh-token'; +const AccessToken = 'some-access-token'; +const SrpLargeAHex = '1a'.repeat(32); +const ValidationData = [ + { Name: 'some-name-1', Value: 'some-value-1' }, + { Name: 'some-name-2', Value: 'some-value-2' }, +]; + +const dateNow = 'Wed Sep 21 07:36:54 UTC 2016'; + class MockUserPool { + constructor() { + this.client = new MockClient(); + } + + getUserPoolId() { + return UserPoolId; + } + + getClientId() { + return ClientId; + } + + getParanoia() { + return 0; + } + + toJSON() { + return '[mock UserPool]'; + } } -const USERNAME = 'some-username'; +class MockAuthenticationHelper { + getLargeAValue() { + return new BigInteger(SrpLargeAHex, 16); + } + + getPasswordAuthenticationKey() { + return sjcl.codec.hex.toBits('a4'.repeat(32)); + } +} -function createUser(pool = new MockUserPool()) { +class MockDateHelper { + getNowString() { + return dateNow; + } +} + +function createUser({ pool = new MockUserPool() } = {}, ...requestConfigs) { + pool.client = new MockClient(...requestConfigs); // eslint-disable-line no-param-reassign const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { - // Nothing yet + './AuthenticationHelper': MockAuthenticationHelper, + './DateHelper': MockDateHelper, }); - return new CognitoUser({ Username: USERNAME, Pool: pool }); + return new CognitoUser({ Username, Pool: pool }); } -test.todo('authenticateUser() :: USER_SRP_AUTH, succeeds => creates session'); +test.cb('authenticateUser() :: USER_SRP_AUTH, succeeds => creates session', t => { + const expectedUsername = 'some-other-username'; + const expectedSrpBHex = 'cb'.repeat(16); + const expectedSaltHex = 'a7'.repeat(16); + const expectedSecretBlockHex = '0c'.repeat(16); + + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const initiateAuthResponse = { + ChallengeParameters: { + USER_ID_FOR_SRP: expectedUsername, + SRP_B: expectedSrpBHex, + SALT: expectedSaltHex, + SECRET_BLOCK: expectedSecretBlockHex, + }, + Session: 'initiateAuth-session', + }; + + const respondToAuthChallengeResponse = { + ChallengeName: 'USER_SRP_AUTH', + AuthenticationResult: { IdToken, AccessToken, RefreshToken }, + Session: 'respondToAuthChallenge-session', + }; + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(respondToAuthChallengeResponse)); + + user.authenticateUser( + new AuthenticationDetails({ Username, Password, ValidationData }), + createCallback(t, t.end, { + onSuccess() { + t.is(user.client.requestCallArgs.length, 2); + requestCalledWithOnCall(t, user.client, 0, 'initiateAuth', { + AuthFlow: 'USER_SRP_AUTH', + ClientId, + AuthParameters: { + USERNAME: Username, + SRP_A: SrpLargeAHex, + }, + ClientMetadata: ValidationData, + }); + requestCalledWithOnCall(t, user.client, 1, 'respondToAuthChallenge', { + ChallengeName: 'PASSWORD_VERIFIER', + ClientId, + ChallengeResponses: { + USERNAME: expectedUsername, + PASSWORD_CLAIM_SECRET_BLOCK: initiateAuthResponse.ChallengeParameters.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: 'fSJS+J84N4iLwR5UPrqVeXSp9XwuG8NtVHHS9srOEcQ=', + }, + Session: initiateAuthResponse.Session, + }); + t.is(user.username, expectedUsername); + t.is(user.Session, null); + const userSession = user.getSignInUserSession(); + t.is(userSession.getIdToken().getJwtToken(), IdToken); + t.is(userSession.getAccessToken().getJwtToken(), AccessToken); + t.is(userSession.getRefreshToken().getToken(), RefreshToken); + }, + })); +}); + test.todo('authenticateUser() :: USER_SRP_AUTH, initiateAuth fails => raises onFailure'); test.todo('authenticateUser() :: USER_SRP_AUTH, respondToAuthChallenge fails => raises onFailure'); // ... other flows diff --git a/src/_helpers.test.js b/src/_helpers.test.js index f89c0de0..a5442d55 100644 --- a/src/_helpers.test.js +++ b/src/_helpers.test.js @@ -4,18 +4,21 @@ import test from 'ava'; import { stub } from 'sinon'; import mockRequire from 'mock-require'; -export class MockClient {} +export class MockClient { + constructor(...requestConfigs) { + this.requestConfigs = requestConfigs; + this.nextRequestIndex = 0; + this.requestCallArgs = []; + } -export function stubClient(client, ...requestConfigs) { - if (!(client instanceof MockClient)) { - throw new Error(`'client' is not a MockClient: ${client}`) + makeUnauthenticatedRequest(name, args, cb) { + this.requestCallArgs.push([name, args, cb]); + if (this.nextRequestIndex >= this.requestConfigs.length) { + throw new Error(`No config for request ${this.nextRequestIndex}: '${name}'(${JSON.stringify(args)}).`); + } + const requestConfig = this.requestConfigs[this.nextRequestIndex++]; + cb(...requestConfig); } - const requestStub = stub(); - requestConfigs.forEach((requestConfig, i) => { - const expectation = requestStub.onCall(i); - expectation.yieldsAsync(...requestConfig); - }); - client.makeUnauthenticatedRequest = requestStub; // eslint-disable-line no-param-reassign } export function requestFailsWith(err) { @@ -26,7 +29,43 @@ export function requestSucceedsWith(result) { return [null, result]; } -test.afterEach.always(() => { +export function requestCalledWithOnCall(t, client, call, expectedName, expectedArgs) { + const [name, args, cb] = client.requestCallArgs[call]; + t.is(name, expectedName); + t.deepEqual(args, expectedArgs); + t.true(typeof cb === 'function'); +} + +export function requestCalledOnceWith(t, client, ...expectedArgs) { + t.true(client.requestCallArgs.length === 1); + requestCalledWithOnCall(t, client, 0, ...expectedArgs); +} + +export function createCallback(t, done, callbackTests) { + const callback = {}; + Object.keys(callbackTests).forEach(key => { + callback[key] = function testCallbackMethod(...args) { + t.is(this, callback); + callbackTests[key](...args); + done(); + }; + }); + return callback; +} + +export function createBasicCallback(t, succeeds, expectedError, done) { + return createCallback(t, done, { + onFailure(err) { + t.false(succeeds); + t.is(err, expectedError); + }, + onSuccess() { + t.true(succeeds); + }, + }); +} + +test.afterEach.always(t => { mockRequire.stopAll(); delete global.window; }); From d008ed57f9a26147c31b3b06734cf8fe5d49bb51 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 26 Sep 2016 20:42:37 +1300 Subject: [PATCH 09/17] Update authenticateUser() tests to cover device key states. --- src/CognitoUser.authenticateUser.test.js | 362 +++++++++++++++++++++++ src/CognitoUser.test.js | 2 +- src/CognitoUser_auth.test.js | 160 ---------- src/_helpers.test.js | 24 +- 4 files changed, 380 insertions(+), 168 deletions(-) create mode 100644 src/CognitoUser.authenticateUser.test.js delete mode 100644 src/CognitoUser_auth.test.js diff --git a/src/CognitoUser.authenticateUser.test.js b/src/CognitoUser.authenticateUser.test.js new file mode 100644 index 00000000..fe89f7f7 --- /dev/null +++ b/src/CognitoUser.authenticateUser.test.js @@ -0,0 +1,362 @@ +/* eslint-disable require-jsdoc */ + +import test from 'ava'; +import { stub } from 'sinon'; +import { BigInteger } from 'jsbn'; +import * as sjcl from 'sjcl'; + +import { + MockClient, + requireDefaultWithModuleMocks, + requestSucceedsWith, + requestFailsWith, + createCallback, + requestCalledWithOnCall, +} from './_helpers.test'; + +const UserPoolId = 'xx-nowhere1_SomeUserPool'; // Constructor validates the format. +const ClientId = 'some-client-id'; +const constructorUsername = 'constructor-username'; +const aliasUsername = 'initiateAuthResponse-username'; +const Password = 'swordfish'; +const IdToken = 'some-id-token'; +const RefreshToken = 'some-refresh-token'; +const AccessToken = 'some-access-token'; +const SrpLargeAHex = '1a'.repeat(32); +const SaltDevicesHex = '5d'.repeat(32); +const VerifierDevicesHex = 'ed'.repeat(32); +const RandomPasswordHex = 'a0'.repeat(32); +const ValidationData = [ + { Name: 'some-name-1', Value: 'some-value-1' }, + { Name: 'some-name-2', Value: 'some-value-2' }, +]; + +const dateNow = 'Wed Sep 21 07:36:54 UTC 2016'; + +const initiateAuthResponse = { + ChallengeParameters: { + USER_ID_FOR_SRP: aliasUsername, + SRP_B: 'cb'.repeat(16), + SALT: 'a7'.repeat(16), + SECRET_BLOCK: '0c'.repeat(16), + }, + Session: 'initiateAuth-session', +}; + +const keyPrefix = `CognitoIdentityServiceProvider.${ClientId}`; +const idTokenKey = `${keyPrefix}.${aliasUsername}.idToken`; +const accessTokenKey = `${keyPrefix}.${aliasUsername}.accessToken`; +const refreshTokenKey = `${keyPrefix}.${aliasUsername}.refreshToken`; +const lastAuthUserKey = `${keyPrefix}.LastAuthUser`; +const deviceKeyKey = `${keyPrefix}.${aliasUsername}.deviceKey`; +const randomPasswordKey = `${keyPrefix}.${aliasUsername}.randomPasswordKey`; +const deviceGroupKeyKey = `${keyPrefix}.${aliasUsername}.deviceGroupKey`; + +class MockUserPool { + constructor() { + this.client = new MockClient(); + } + + getUserPoolId() { + return UserPoolId; + } + + getClientId() { + return ClientId; + } + + getParanoia() { + return 0; + } + + toJSON() { + return '[mock UserPool]'; + } +} + +class MockAuthenticationHelper { + getLargeAValue() { + return new BigInteger(SrpLargeAHex, 16); + } + + getPasswordAuthenticationKey() { + return sjcl.codec.hex.toBits('a4'.repeat(32)); + } + + generateHashDevice() { + // Can't test this nicely as the instance is local to authenticateUser() + } + + getSaltDevices() { + return SaltDevicesHex; + } + + getVerifierDevices() { + return VerifierDevicesHex; + } + + getRandomPassword() { + return RandomPasswordHex; + } +} + +class MockDateHelper { + getNowString() { + return dateNow; + } +} + +class MockAuthenticationDetails { + getPassword() { + return Password; + } + + getValidationData() { + return ValidationData; + } +} + +function hexToBase64(hex) { + return sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(hex)); +} + +function createUser({ pool = new MockUserPool() } = {}, ...requestConfigs) { + pool.client = new MockClient(...requestConfigs); // eslint-disable-line no-param-reassign + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { + './AuthenticationHelper': MockAuthenticationHelper, + './DateHelper': MockDateHelper, + }); + return new CognitoUser({ Username: constructorUsername, Pool: pool }); +} + +test.cb('initiateAuth fails => raises onFailure', t => { + const expectedError = { code: 'InternalServerError' }; + + const user = createUser({}, requestFailsWith(expectedError)); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + }, + })); +}); + +// .serial for global.window stompage +test.serial.cb('respondToAuthChallenge fails => raises onFailure', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const expectedError = { code: 'InternalServerError' }; + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestFailsWith(expectedError)); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + t.is(user.getUsername(), initiateAuthResponse.ChallengeParameters.USER_ID_FOR_SRP); + t.is(user.Session, null); + }, + })); +}); + +function completesMacroTitle(t, { flow, hasOldDevice, hasCachedDevice, hasNewDevice }) { + return [ + `${flow} flow`, + `${hasOldDevice ? 'with' : 'no'} old`, + `${hasCachedDevice ? 'with' : 'no'} cached`, + `${hasNewDevice ? 'with' : 'no'} new device`, + 'completes => creates session', + ].join(', '); +} +function completesMacro(t, { flow, hasOldDevice, hasCachedDevice, hasNewDevice }) { + const oldDeviceKey = 'old-deviceKey'; + + const cachedDeviceKey = 'cached-deviceKey'; + const cachedRandomPassword = 'cached-randomPassword'; + const cachedDeviceGroupKey = 'cached-deviceGroup'; + + const newDeviceKey = 'new-deviceKey'; + const newDeviceGroupKey = 'new-deviceGroup'; + const deviceName = 'some-device-name'; + + const expectedInitiateAuthArgs = { + AuthFlow: flow, + ClientId, + AuthParameters: { + USERNAME: constructorUsername, + SRP_A: SrpLargeAHex, + }, + ClientMetadata: ValidationData, + }; + const expectedRespondToAuthChallengeArgs = { + ChallengeName: 'PASSWORD_VERIFIER', + ClientId, + ChallengeResponses: { + USERNAME: aliasUsername, + PASSWORD_CLAIM_SECRET_BLOCK: initiateAuthResponse.ChallengeParameters.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: 'duUyEsqUdolAO+/KMVp9lS/sxTozKH6rNZ2HlWnfLp4=', + }, + Session: initiateAuthResponse.Session, + }; + const expectedConfirmDeviceArgs = { + DeviceKey: newDeviceKey, + AccessToken, + DeviceSecretVerifierConfig: { + Salt: hexToBase64(SaltDevicesHex), + PasswordVerifier: hexToBase64(VerifierDevicesHex), + }, + DeviceName: deviceName, + }; + + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + if (hasNewDevice) { + global.navigator = { + userAgent: deviceName, + }; + } + + const respondToAuthChallengeResponse = { + ChallengeName: 'PASSWORD_VERIFIER', + AuthenticationResult: { IdToken, AccessToken, RefreshToken }, + Session: 'respondToAuthChallenge-session', + }; + + const requests = [ + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(respondToAuthChallengeResponse), + ]; + + if (hasNewDevice) { + respondToAuthChallengeResponse.AuthenticationResult.NewDeviceMetadata = { + DeviceGroupKey: newDeviceGroupKey, + DeviceKey: newDeviceKey, + }; + requests.push( + requestSucceedsWith({ + AuthenticationResult: { + NewDeviceMetadata: { + DeviceKey: newDeviceKey, + }, + }, + })); + } + + const user = createUser({}, ...requests); + user.setAuthenticationFlowType(flow); + + let expectedDeviceKey; + let expectedRandomPassword; + let expectedDeviceGroupKey; + + if (hasOldDevice) { + expectedDeviceKey = oldDeviceKey; + user.deviceKey = oldDeviceKey; + + expectedInitiateAuthArgs.AuthParameters.DEVICE_KEY = oldDeviceKey; + expectedRespondToAuthChallengeArgs.ChallengeResponses.DEVICE_KEY = oldDeviceKey; + } + + if (hasCachedDevice) { + expectedDeviceKey = cachedDeviceKey; + expectedRandomPassword = cachedRandomPassword; + expectedDeviceGroupKey = cachedDeviceGroupKey; + + localStorage.getItem.withArgs(deviceKeyKey).returns(cachedDeviceKey); + localStorage.getItem.withArgs(randomPasswordKey).returns(cachedRandomPassword); + localStorage.getItem.withArgs(deviceGroupKeyKey).returns(cachedDeviceGroupKey); + + expectedRespondToAuthChallengeArgs.ChallengeResponses.DEVICE_KEY = cachedDeviceKey; + } + + if (hasNewDevice) { + expectedDeviceKey = newDeviceKey; + expectedRandomPassword = RandomPasswordHex; + expectedDeviceGroupKey = newDeviceGroupKey; + } + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onSuccess() { + // check client requests (expanded due to assert string depth limit) + t.is(user.client.requestCallCount, hasNewDevice ? 3 : 2); + t.is(user.client.getRequestCallArgs(0).name, 'initiateAuth'); + t.deepEqual(user.client.getRequestCallArgs(0).args, expectedInitiateAuthArgs); + t.is(user.client.getRequestCallArgs(1).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(1).args, expectedRespondToAuthChallengeArgs); + if (hasNewDevice) { + t.is(user.client.getRequestCallArgs(2).name, 'confirmDevice'); + t.deepEqual(user.client.getRequestCallArgs(2).args, expectedConfirmDeviceArgs); + } + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, null); + t.is(user.deviceKey, expectedDeviceKey); + t.is(user.randomPassword, expectedRandomPassword); + t.is(user.deviceGroupKey, expectedDeviceGroupKey); + + // Check sign-in session + const userSession = user.getSignInUserSession(); + t.is(userSession.getIdToken().getJwtToken(), IdToken); + t.is(userSession.getAccessToken().getJwtToken(), AccessToken); + t.is(userSession.getRefreshToken().getToken(), RefreshToken); + + // check cacheTokens() + t.is(localStorage.setItem.withArgs(idTokenKey, IdToken).callCount, 1); + t.is(localStorage.setItem.withArgs(accessTokenKey, AccessToken).callCount, 1); + t.is(localStorage.setItem.withArgs(refreshTokenKey, RefreshToken).callCount, 1); + t.is(localStorage.setItem.withArgs(lastAuthUserKey, aliasUsername).callCount, 1); + + // check cacheDeviceKeyAndPassword() + if (hasNewDevice) { + t.is(localStorage.setItem.withArgs(deviceKeyKey, newDeviceKey).callCount, 1); + t.is(localStorage.setItem.withArgs(randomPasswordKey, RandomPasswordHex).callCount, 1); + t.is(localStorage.setItem.withArgs(deviceGroupKeyKey, newDeviceGroupKey).callCount, 1); + } else { + t.is(localStorage.setItem.withArgs(deviceKeyKey).callCount, 0); + t.is(localStorage.setItem.withArgs(randomPasswordKey).callCount, 0); + t.is(localStorage.setItem.withArgs(deviceGroupKeyKey).callCount, 0); + } + }, + })); +} +completesMacro.title = completesMacroTitle; + +for (const flow of ['USER_SRP_AUTH']) { + for (const hasOldDevice of [false, true]) { + for (const hasCachedDevice of [false, true]) { + for (const hasNewDevice of [false, true]) { + test.serial.cb(completesMacro, { flow, hasOldDevice, hasCachedDevice, hasNewDevice }); + } + } + } +} + +test.todo('USER_SRP_AUTH flow, MFA required => calls mfaRequired'); +test.todo('USER_SRP_AUTH flow, custom challenge => calls customChallenge'); +// ... other flows + +test.todo('getDeviceResponse() :: DEVICE_SRP_AUTH fails => calls onFailure'); +test.todo('getDeviceResponse() :: DEVICE_PASSWORD_VERIFIER fails => calls onFailure'); +test.todo('getDeviceResponse() :: succeeds => signs in and calls onSuccess'); + +test.todo('sendCustomChallengeAnswer() :: fails => calls onFailure'); +test.todo('sendCustomChallengeAnswer() :: succeeds => calls onSuccess'); + diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index faf8e0bd..08c4d2b6 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -136,7 +136,7 @@ test('setAuthenticationFlowType() => sets authentication flow type', t => { t.is(user.getAuthenticationFlowType(), flowType); }); -// See CognitoUser_auth.test.js for authenticateUser() and the challenge responses +// See CognitoUser.authenticateUser.test.js for authenticateUser() and the challenge responses function confirmRegistrationMacro(t, forceAliasCreation, succeeds) { const expectedError = createExpectedErrorFromSuccess(succeeds); diff --git a/src/CognitoUser_auth.test.js b/src/CognitoUser_auth.test.js deleted file mode 100644 index 7406d898..00000000 --- a/src/CognitoUser_auth.test.js +++ /dev/null @@ -1,160 +0,0 @@ -/* eslint-disable require-jsdoc */ - -import test from 'ava'; -import { stub } from 'sinon'; -import { BigInteger } from 'jsbn'; -import * as sjcl from 'sjcl'; - -import { - MockClient, - requireDefaultWithModuleMocks, - requestSucceedsWith, - createCallback, - requestCalledWithOnCall, -} from './_helpers.test'; - -import AuthenticationDetails from './AuthenticationDetails'; - -const UserPoolId = 'xx-nowhere1_SomeUserPool'; // Constructor validates the format. -const ClientId = 'some-client-id'; -const Username = 'some-username'; -const Password = 'swordfish'; -const IdToken = 'some-id-token'; -const RefreshToken = 'some-refresh-token'; -const AccessToken = 'some-access-token'; -const SrpLargeAHex = '1a'.repeat(32); -const ValidationData = [ - { Name: 'some-name-1', Value: 'some-value-1' }, - { Name: 'some-name-2', Value: 'some-value-2' }, -]; - -const dateNow = 'Wed Sep 21 07:36:54 UTC 2016'; - -class MockUserPool { - constructor() { - this.client = new MockClient(); - } - - getUserPoolId() { - return UserPoolId; - } - - getClientId() { - return ClientId; - } - - getParanoia() { - return 0; - } - - toJSON() { - return '[mock UserPool]'; - } -} - -class MockAuthenticationHelper { - getLargeAValue() { - return new BigInteger(SrpLargeAHex, 16); - } - - getPasswordAuthenticationKey() { - return sjcl.codec.hex.toBits('a4'.repeat(32)); - } -} - -class MockDateHelper { - getNowString() { - return dateNow; - } -} - -function createUser({ pool = new MockUserPool() } = {}, ...requestConfigs) { - pool.client = new MockClient(...requestConfigs); // eslint-disable-line no-param-reassign - const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { - './AuthenticationHelper': MockAuthenticationHelper, - './DateHelper': MockDateHelper, - }); - return new CognitoUser({ Username, Pool: pool }); -} - -test.cb('authenticateUser() :: USER_SRP_AUTH, succeeds => creates session', t => { - const expectedUsername = 'some-other-username'; - const expectedSrpBHex = 'cb'.repeat(16); - const expectedSaltHex = 'a7'.repeat(16); - const expectedSecretBlockHex = '0c'.repeat(16); - - const localStorage = { - getItem: stub().returns(null), - setItem: stub(), - }; - global.window = { localStorage }; - - const initiateAuthResponse = { - ChallengeParameters: { - USER_ID_FOR_SRP: expectedUsername, - SRP_B: expectedSrpBHex, - SALT: expectedSaltHex, - SECRET_BLOCK: expectedSecretBlockHex, - }, - Session: 'initiateAuth-session', - }; - - const respondToAuthChallengeResponse = { - ChallengeName: 'USER_SRP_AUTH', - AuthenticationResult: { IdToken, AccessToken, RefreshToken }, - Session: 'respondToAuthChallenge-session', - }; - - const user = createUser( - {}, - requestSucceedsWith(initiateAuthResponse), - requestSucceedsWith(respondToAuthChallengeResponse)); - - user.authenticateUser( - new AuthenticationDetails({ Username, Password, ValidationData }), - createCallback(t, t.end, { - onSuccess() { - t.is(user.client.requestCallArgs.length, 2); - requestCalledWithOnCall(t, user.client, 0, 'initiateAuth', { - AuthFlow: 'USER_SRP_AUTH', - ClientId, - AuthParameters: { - USERNAME: Username, - SRP_A: SrpLargeAHex, - }, - ClientMetadata: ValidationData, - }); - requestCalledWithOnCall(t, user.client, 1, 'respondToAuthChallenge', { - ChallengeName: 'PASSWORD_VERIFIER', - ClientId, - ChallengeResponses: { - USERNAME: expectedUsername, - PASSWORD_CLAIM_SECRET_BLOCK: initiateAuthResponse.ChallengeParameters.SECRET_BLOCK, - TIMESTAMP: dateNow, - PASSWORD_CLAIM_SIGNATURE: 'fSJS+J84N4iLwR5UPrqVeXSp9XwuG8NtVHHS9srOEcQ=', - }, - Session: initiateAuthResponse.Session, - }); - t.is(user.username, expectedUsername); - t.is(user.Session, null); - const userSession = user.getSignInUserSession(); - t.is(userSession.getIdToken().getJwtToken(), IdToken); - t.is(userSession.getAccessToken().getJwtToken(), AccessToken); - t.is(userSession.getRefreshToken().getToken(), RefreshToken); - }, - })); -}); - -test.todo('authenticateUser() :: USER_SRP_AUTH, initiateAuth fails => raises onFailure'); -test.todo('authenticateUser() :: USER_SRP_AUTH, respondToAuthChallenge fails => raises onFailure'); -// ... other flows -test.todo('authenticateUser() :: MFA required => calls mfaRequired'); -test.todo('authenticateUser() :: custom challenge => calls customChallenge'); - -test.todo('getDeviceResponse() :: DEVICE_SRP_AUTH fails => calls onFailure'); -test.todo('getDeviceResponse() :: DEVICE_PASSWORD_VERIFIER fails => calls onFailure'); -test.todo('getDeviceResponse() :: succeeds => signs in and calls onSuccess'); - -test.todo('sendCustomChallengeAnswer() :: fails => calls onFailure'); -test.todo('sendCustomChallengeAnswer() :: succeeds => calls onSuccess'); - diff --git a/src/_helpers.test.js b/src/_helpers.test.js index a5442d55..ebe17658 100644 --- a/src/_helpers.test.js +++ b/src/_helpers.test.js @@ -11,8 +11,19 @@ export class MockClient { this.requestCallArgs = []; } + get requestCallCount() { + return this.requestCallArgs.length; + } + + getRequestCallArgs(call) { + return this.requestCallArgs[call]; + } + makeUnauthenticatedRequest(name, args, cb) { - this.requestCallArgs.push([name, args, cb]); + if (typeof cb !== 'function') { + throw new TypeError('MockClient requires cb arg.') + } + this.requestCallArgs.push({ name, args }); if (this.nextRequestIndex >= this.requestConfigs.length) { throw new Error(`No config for request ${this.nextRequestIndex}: '${name}'(${JSON.stringify(args)}).`); } @@ -29,16 +40,15 @@ export function requestSucceedsWith(result) { return [null, result]; } -export function requestCalledWithOnCall(t, client, call, expectedName, expectedArgs) { - const [name, args, cb] = client.requestCallArgs[call]; +export function requestCalledWithOnCall(t, client, callIndex, expectedName, expectedArgs) { + const { name, args } = client.getRequestCallArgs(callIndex); t.is(name, expectedName); t.deepEqual(args, expectedArgs); - t.true(typeof cb === 'function'); } -export function requestCalledOnceWith(t, client, ...expectedArgs) { - t.true(client.requestCallArgs.length === 1); - requestCalledWithOnCall(t, client, 0, ...expectedArgs); +export function requestCalledOnceWith(t, client, expectedName, expectedArgs) { + t.true(client.requestCallCount === 1); + requestCalledWithOnCall(t, client, 0, expectedName, expectedArgs); } export function createCallback(t, done, callbackTests) { From 19d939bf3708094c271204b15d683f3b1f9c128e Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Tue, 11 Oct 2016 20:10:32 +1300 Subject: [PATCH 10/17] Complete coverage for CognitoUser.authenticateUser(). --- src/CognitoUser.authenticateUser.test.js | 566 +++++++++++++++++++---- 1 file changed, 470 insertions(+), 96 deletions(-) diff --git a/src/CognitoUser.authenticateUser.test.js b/src/CognitoUser.authenticateUser.test.js index fe89f7f7..c4dfc5f7 100644 --- a/src/CognitoUser.authenticateUser.test.js +++ b/src/CognitoUser.authenticateUser.test.js @@ -11,7 +11,6 @@ import { requestSucceedsWith, requestFailsWith, createCallback, - requestCalledWithOnCall, } from './_helpers.test'; const UserPoolId = 'xx-nowhere1_SomeUserPool'; // Constructor validates the format. @@ -33,6 +32,27 @@ const ValidationData = [ const dateNow = 'Wed Sep 21 07:36:54 UTC 2016'; +function createExpectedInitiateAuthArgs({ + AuthFlow = 'USER_SRP_AUTH', + extraAuthParameters, +} = {}) { + const args = { + AuthFlow, + ClientId, + AuthParameters: { + USERNAME: constructorUsername, + SRP_A: SrpLargeAHex, + }, + ClientMetadata: ValidationData, + }; + + if (extraAuthParameters) { + Object.assign(args.AuthParameters, extraAuthParameters); + } + + return args; +} + const initiateAuthResponse = { ChallengeParameters: { USER_ID_FOR_SRP: aliasUsername, @@ -43,6 +63,49 @@ const initiateAuthResponse = { Session: 'initiateAuth-session', }; +function createSrpChallengeResponses(extra) { + const result = { + USERNAME: aliasUsername, + PASSWORD_CLAIM_SECRET_BLOCK: initiateAuthResponse.ChallengeParameters.SECRET_BLOCK, + TIMESTAMP: dateNow, + PASSWORD_CLAIM_SIGNATURE: 'duUyEsqUdolAO+/KMVp9lS/sxTozKH6rNZ2HlWnfLp4=', + }; + + if (extra) { + Object.assign(result, extra); + } + + return result; +} + +function createExpectedRespondToAuthChallengePasswordVerifierArgs( + { extraChallengeResponses } = {} +) { + return { + ChallengeName: 'PASSWORD_VERIFIER', + ClientId, + ChallengeResponses: createSrpChallengeResponses(extraChallengeResponses), + Session: initiateAuthResponse.Session, + }; +} + +function createRespondToAuthChallengeCompleteResponse({ NewDeviceMetadata } = {}) { + return { + AuthenticationResult: { IdToken, AccessToken, RefreshToken, NewDeviceMetadata }, + }; +} + +function createRespondToAuthChallengeChallengeResponse({ + ChallengeName, + ChallengeParameters, +} = {}) { + return { + ChallengeName, + Session: `respondToAuthChallenge-${ChallengeName}-session`, + ChallengeParameters, + }; +} + const keyPrefix = `CognitoIdentityServiceProvider.${ClientId}`; const idTokenKey = `${keyPrefix}.${aliasUsername}.idToken`; const accessTokenKey = `${keyPrefix}.${aliasUsername}.accessToken`; @@ -129,7 +192,7 @@ function createUser({ pool = new MockUserPool() } = {}, ...requestConfigs) { return new CognitoUser({ Username: constructorUsername, Pool: pool }); } -test.cb('initiateAuth fails => raises onFailure', t => { +test.cb('fails on initiateAuth => raises onFailure', t => { const expectedError = { code: 'InternalServerError' }; const user = createUser({}, requestFailsWith(expectedError)); @@ -144,7 +207,7 @@ test.cb('initiateAuth fails => raises onFailure', t => { }); // .serial for global.window stompage -test.serial.cb('respondToAuthChallenge fails => raises onFailure', t => { +test.serial.cb('fails on respondToAuthChallenge => raises onFailure', t => { const localStorage = { getItem: stub().returns(null), setItem: stub(), @@ -163,22 +226,65 @@ test.serial.cb('respondToAuthChallenge fails => raises onFailure', t => { createCallback(t, t.end, { onFailure(err) { t.is(err, expectedError); - t.is(user.getUsername(), initiateAuthResponse.ChallengeParameters.USER_ID_FOR_SRP); + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, null); + }, + })); +}); + +test.serial.cb('with new device state, fails on confirmDevice => raises onFailure', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + global.navigator = { userAgent: 'some-device-name' }; + const expectedError = { code: 'InternalServerError' }; + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(createRespondToAuthChallengeCompleteResponse({ + NewDeviceMetadata: { + DeviceGroupKey: 'new-deviceGroup', + DeviceKey: 'new-deviceKey', + }, + })), + requestFailsWith(expectedError) + ); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + t.is(user.getUsername(), aliasUsername); t.is(user.Session, null); }, })); }); -function completesMacroTitle(t, { flow, hasOldDevice, hasCachedDevice, hasNewDevice }) { - return [ - `${flow} flow`, +function deviceStateSuccessMacroTitle( + t, + { hasOldDevice, hasCachedDevice, hasNewDevice, userConfirmationNecessary } +) { + const context = [ `${hasOldDevice ? 'with' : 'no'} old`, `${hasCachedDevice ? 'with' : 'no'} cached`, - `${hasNewDevice ? 'with' : 'no'} new device`, - 'completes => creates session', - ].join(', '); + `${hasNewDevice ? 'with' : 'no'} new device state`, + ]; + + if (userConfirmationNecessary) { + context.push('user confirmation necessary'); + } + + return `${context.join(', ')} => creates session`; } -function completesMacro(t, { flow, hasOldDevice, hasCachedDevice, hasNewDevice }) { + +function deviceStateSuccessMacro( + t, + { hasOldDevice, hasCachedDevice, hasNewDevice, userConfirmationNecessary } +) { const oldDeviceKey = 'old-deviceKey'; const cachedDeviceKey = 'cached-deviceKey'; @@ -189,111 +295,112 @@ function completesMacro(t, { flow, hasOldDevice, hasCachedDevice, hasNewDevice } const newDeviceGroupKey = 'new-deviceGroup'; const deviceName = 'some-device-name'; - const expectedInitiateAuthArgs = { - AuthFlow: flow, - ClientId, - AuthParameters: { - USERNAME: constructorUsername, - SRP_A: SrpLargeAHex, - }, - ClientMetadata: ValidationData, - }; - const expectedRespondToAuthChallengeArgs = { - ChallengeName: 'PASSWORD_VERIFIER', - ClientId, - ChallengeResponses: { - USERNAME: aliasUsername, - PASSWORD_CLAIM_SECRET_BLOCK: initiateAuthResponse.ChallengeParameters.SECRET_BLOCK, - TIMESTAMP: dateNow, - PASSWORD_CLAIM_SIGNATURE: 'duUyEsqUdolAO+/KMVp9lS/sxTozKH6rNZ2HlWnfLp4=', - }, - Session: initiateAuthResponse.Session, - }; - const expectedConfirmDeviceArgs = { - DeviceKey: newDeviceKey, - AccessToken, - DeviceSecretVerifierConfig: { - Salt: hexToBase64(SaltDevicesHex), - PasswordVerifier: hexToBase64(VerifierDevicesHex), - }, - DeviceName: deviceName, - }; - const localStorage = { getItem: stub().returns(null), setItem: stub(), }; global.window = { localStorage }; + let expectedDeviceKey; + let expectedRandomPassword; + let expectedDeviceGroupKey; + let extraExpectedChallengeResponses; + + if (hasOldDevice) { + expectedDeviceKey = oldDeviceKey; + + extraExpectedChallengeResponses = { + DEVICE_KEY: oldDeviceKey, + }; + } + + if (hasCachedDevice) { + expectedDeviceKey = cachedDeviceKey; + expectedRandomPassword = cachedRandomPassword; + expectedDeviceGroupKey = cachedDeviceGroupKey; + + localStorage.getItem.withArgs(deviceKeyKey).returns(cachedDeviceKey); + localStorage.getItem.withArgs(randomPasswordKey).returns(cachedRandomPassword); + localStorage.getItem.withArgs(deviceGroupKeyKey).returns(cachedDeviceGroupKey); + + extraExpectedChallengeResponses = { + DEVICE_KEY: cachedDeviceKey, + }; + } + if (hasNewDevice) { + expectedDeviceKey = newDeviceKey; + expectedRandomPassword = RandomPasswordHex; + expectedDeviceGroupKey = newDeviceGroupKey; + global.navigator = { userAgent: deviceName, }; } - const respondToAuthChallengeResponse = { - ChallengeName: 'PASSWORD_VERIFIER', - AuthenticationResult: { IdToken, AccessToken, RefreshToken }, - Session: 'respondToAuthChallenge-session', + const expectedInitiateAuthArgs = createExpectedInitiateAuthArgs({ + extraAuthParameters: hasOldDevice ? { DEVICE_KEY: oldDeviceKey } : {}, + }); + + const expectedRespondToAuthChallengeArgs = + createExpectedRespondToAuthChallengePasswordVerifierArgs({ + extraChallengeResponses: extraExpectedChallengeResponses, + }); + + const expectedConfirmDeviceArgs = { + DeviceKey: newDeviceKey, + AccessToken, + DeviceSecretVerifierConfig: { + Salt: hexToBase64(SaltDevicesHex), + PasswordVerifier: hexToBase64(VerifierDevicesHex), + }, + DeviceName: deviceName, }; const requests = [ requestSucceedsWith(initiateAuthResponse), - requestSucceedsWith(respondToAuthChallengeResponse), ]; - if (hasNewDevice) { - respondToAuthChallengeResponse.AuthenticationResult.NewDeviceMetadata = { - DeviceGroupKey: newDeviceGroupKey, - DeviceKey: newDeviceKey, - }; + if (!hasNewDevice) { + requests.push( + requestSucceedsWith(createRespondToAuthChallengeCompleteResponse()) + ); + } else { requests.push( + requestSucceedsWith(createRespondToAuthChallengeCompleteResponse({ + NewDeviceMetadata: { + DeviceGroupKey: newDeviceGroupKey, + DeviceKey: newDeviceKey, + }, + })), requestSucceedsWith({ AuthenticationResult: { NewDeviceMetadata: { DeviceKey: newDeviceKey, }, }, - })); + UserConfirmationNecessary: userConfirmationNecessary, + }) + ); } const user = createUser({}, ...requests); - user.setAuthenticationFlowType(flow); - - let expectedDeviceKey; - let expectedRandomPassword; - let expectedDeviceGroupKey; if (hasOldDevice) { - expectedDeviceKey = oldDeviceKey; user.deviceKey = oldDeviceKey; - - expectedInitiateAuthArgs.AuthParameters.DEVICE_KEY = oldDeviceKey; - expectedRespondToAuthChallengeArgs.ChallengeResponses.DEVICE_KEY = oldDeviceKey; - } - - if (hasCachedDevice) { - expectedDeviceKey = cachedDeviceKey; - expectedRandomPassword = cachedRandomPassword; - expectedDeviceGroupKey = cachedDeviceGroupKey; - - localStorage.getItem.withArgs(deviceKeyKey).returns(cachedDeviceKey); - localStorage.getItem.withArgs(randomPasswordKey).returns(cachedRandomPassword); - localStorage.getItem.withArgs(deviceGroupKeyKey).returns(cachedDeviceGroupKey); - - expectedRespondToAuthChallengeArgs.ChallengeResponses.DEVICE_KEY = cachedDeviceKey; - } - - if (hasNewDevice) { - expectedDeviceKey = newDeviceKey; - expectedRandomPassword = RandomPasswordHex; - expectedDeviceGroupKey = newDeviceGroupKey; } user.authenticateUser( new MockAuthenticationDetails(), createCallback(t, t.end, { - onSuccess() { + onSuccess(signInUserSessionArg, userConfirmationNecessaryArg) { + t.is(signInUserSessionArg, user.signInUserSession); + if (userConfirmationNecessary) { + t.true(userConfirmationNecessaryArg); + } else { + t.falsy(userConfirmationNecessaryArg); + } + // check client requests (expanded due to assert string depth limit) t.is(user.client.requestCallCount, hasNewDevice ? 3 : 2); t.is(user.client.getRequestCallArgs(0).name, 'initiateAuth'); @@ -337,26 +444,293 @@ function completesMacro(t, { flow, hasOldDevice, hasCachedDevice, hasNewDevice } }, })); } -completesMacro.title = completesMacroTitle; - -for (const flow of ['USER_SRP_AUTH']) { - for (const hasOldDevice of [false, true]) { - for (const hasCachedDevice of [false, true]) { - for (const hasNewDevice of [false, true]) { - test.serial.cb(completesMacro, { flow, hasOldDevice, hasCachedDevice, hasNewDevice }); - } +deviceStateSuccessMacro.title = deviceStateSuccessMacroTitle; + +for (const hasOldDevice of [false, true]) { + for (const hasCachedDevice of [false, true]) { + for (const hasNewDevice of [false, true]) { + test.serial.cb(deviceStateSuccessMacro, { hasOldDevice, hasCachedDevice, hasNewDevice }); } } } -test.todo('USER_SRP_AUTH flow, MFA required => calls mfaRequired'); -test.todo('USER_SRP_AUTH flow, custom challenge => calls customChallenge'); -// ... other flows +test.serial.cb(deviceStateSuccessMacro, { hasNewDevice: true, userConfirmationNecessary: true }); + +test.serial.cb('CUSTOM_AUTH flow, CUSTOM_CHALLENGE challenge => raises customChallenge', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const expectedChallengeParameters = { + Name: 'some-custom-challenge-parameter', + }; + + const expectedInitiateAuthArgs = createExpectedInitiateAuthArgs({ + AuthFlow: 'CUSTOM_AUTH', + extraAuthParameters: { + CHALLENGE_NAME: 'SRP_A', + }, + }); + + const expectedRespondToAuthChallengeArgs = + createExpectedRespondToAuthChallengePasswordVerifierArgs(); + + const respondToAuthChallengeResponse = createRespondToAuthChallengeChallengeResponse({ + ChallengeName: 'CUSTOM_CHALLENGE', + ChallengeParameters: expectedChallengeParameters, + }); + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(respondToAuthChallengeResponse)); + + user.setAuthenticationFlowType('CUSTOM_AUTH'); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + customChallenge(parameters) { + t.deepEqual(parameters, expectedChallengeParameters); + + // check client requests (expanded due to assert string depth limit) + t.is(user.client.requestCallCount, 2); + t.is(user.client.getRequestCallArgs(0).name, 'initiateAuth'); + t.deepEqual(user.client.getRequestCallArgs(0).args, expectedInitiateAuthArgs); + t.is(user.client.getRequestCallArgs(1).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(1).args, expectedRespondToAuthChallengeArgs); + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, respondToAuthChallengeResponse.Session); + }, + })); +}); + +test.serial.cb('SMS_MFA challenge => raises mfaRequired', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const expectedInitiateAuthArgs = createExpectedInitiateAuthArgs(); + + const expectedRespondToAuthChallengeArgs = + createExpectedRespondToAuthChallengePasswordVerifierArgs(); + + const respondToAuthChallengeResponse = createRespondToAuthChallengeChallengeResponse({ + ChallengeName: 'SMS_MFA', + }); + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(respondToAuthChallengeResponse)); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + mfaRequired() { + // check client requests (expanded due to assert string depth limit) + t.is(user.client.requestCallCount, 2); + t.is(user.client.getRequestCallArgs(0).name, 'initiateAuth'); + t.deepEqual(user.client.getRequestCallArgs(0).args, expectedInitiateAuthArgs); + t.is(user.client.getRequestCallArgs(1).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(1).args, expectedRespondToAuthChallengeArgs); + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, respondToAuthChallengeResponse.Session); + }, + })); +}); + +test.serial.cb('DEVICE_SRP_AUTH challenge, fails on DEVICE_SRP_AUTH => raises onFailure', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const expectedError = { code: 'InternalServerError' }; + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(createRespondToAuthChallengeChallengeResponse({ + ChallengeName: 'DEVICE_SRP_AUTH', + })), + requestFailsWith(expectedError)); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, null); + }, + })); +}); + +test.serial.cb( + 'DEVICE_SRP_AUTH challenge, fails on DEVICE_PASSWORD_VERIFIER fails => raises onFailure', + t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const expectedError = { code: 'InternalServerError' }; + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(createRespondToAuthChallengeChallengeResponse({ + ChallengeName: 'DEVICE_SRP_AUTH', + })), + requestSucceedsWith({ + ChallengeParameters: initiateAuthResponse.ChallengeParameters, + }), + requestFailsWith(expectedError)); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, null); + }, + })); + }); + +test.serial.cb('DEVICE_SRP_AUTH challenge, succeeds => creates session', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + // The PASSWORD_CLAIM_SIGNATURE depends on these values + const deviceGroupKey = 'cached-deviceGroupKey'; + const randomPassword = 'cached-randomPassword'; + const deviceKey = 'cached-deviceKey'; + + localStorage.getItem.withArgs(deviceKeyKey).returns(deviceKey); + localStorage.getItem.withArgs(randomPasswordKey).returns(randomPassword); + localStorage.getItem.withArgs(deviceGroupKeyKey).returns(deviceGroupKey); + + const expectedInitiateAuthArgs = createExpectedInitiateAuthArgs(); + + const expectedRespondToAuthChallengePasswordVerifierArgs = + createExpectedRespondToAuthChallengePasswordVerifierArgs({ + extraChallengeResponses: { + DEVICE_KEY: deviceKey, + }, + }); + + const respondToAuthChallengePasswordVerifierResponse = + createRespondToAuthChallengeChallengeResponse({ + ChallengeName: 'DEVICE_SRP_AUTH', + }); + + // FIXME: should be using a separate set of AuthenticationHelper mock results. + const expectedRespondToAuthChallengeDeviceSrpAuthArgs = { + ChallengeName: 'DEVICE_SRP_AUTH', + ClientId, + ChallengeResponses: { + USERNAME: aliasUsername, + DEVICE_KEY: deviceKey, + SRP_A: SrpLargeAHex, + }, + }; + + const respondToAuthChallengeDeviceSrpAuthResponse = { + ChallengeParameters: initiateAuthResponse.ChallengeParameters, + Session: 'respondToAuthChallenge-DEVICE_SRP_AUTH-session', + }; + + const expectedRespondToAuthChallengeDevicePasswordVerifierArgs = { + ChallengeName: 'DEVICE_PASSWORD_VERIFIER', + ClientId, + ChallengeResponses: createSrpChallengeResponses({ + PASSWORD_CLAIM_SIGNATURE: 'ZkW+a3yZRihjvIXY0pKfKzIozqXvsw/2LaOXGDN3vo8=', + DEVICE_KEY: deviceKey, + }), + Session: respondToAuthChallengeDeviceSrpAuthResponse.Session, + }; + + const user = createUser( + {}, + requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(respondToAuthChallengePasswordVerifierResponse), + requestSucceedsWith(respondToAuthChallengeDeviceSrpAuthResponse), + requestSucceedsWith(createRespondToAuthChallengeCompleteResponse()) + ); + + user.authenticateUser( + new MockAuthenticationDetails(), + createCallback(t, t.end, { + onSuccess() { + // check client requests (expanded due to assert string depth limit) + t.is(user.client.requestCallCount, 4); + t.is(user.client.getRequestCallArgs(0).name, 'initiateAuth'); + t.deepEqual(user.client.getRequestCallArgs(0).args, expectedInitiateAuthArgs); + t.is(user.client.getRequestCallArgs(1).name, 'respondToAuthChallenge'); + t.deepEqual( + user.client.getRequestCallArgs(1).args, + expectedRespondToAuthChallengePasswordVerifierArgs + ); + t.is(user.client.getRequestCallArgs(2).name, 'respondToAuthChallenge'); + t.deepEqual( + user.client.getRequestCallArgs(2).args, + expectedRespondToAuthChallengeDeviceSrpAuthArgs + ); + t.is(user.client.getRequestCallArgs(3).name, 'respondToAuthChallenge'); + t.deepEqual( + user.client.getRequestCallArgs(3).args, + expectedRespondToAuthChallengeDevicePasswordVerifierArgs + ); + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, null); + t.is(user.deviceKey, deviceKey); + t.is(user.randomPassword, randomPassword); + t.is(user.deviceGroupKey, deviceGroupKey); + + // Check sign-in session + const userSession = user.getSignInUserSession(); + t.is(userSession.getIdToken().getJwtToken(), IdToken); + t.is(userSession.getAccessToken().getJwtToken(), AccessToken); + t.is(userSession.getRefreshToken().getToken(), RefreshToken); + + // check cacheTokens() + t.is(localStorage.setItem.withArgs(idTokenKey, IdToken).callCount, 1); + t.is(localStorage.setItem.withArgs(accessTokenKey, AccessToken).callCount, 1); + t.is(localStorage.setItem.withArgs(refreshTokenKey, RefreshToken).callCount, 1); + t.is(localStorage.setItem.withArgs(lastAuthUserKey, aliasUsername).callCount, 1); + + // check cacheDeviceKeyAndPassword() + t.is(localStorage.setItem.withArgs(deviceKeyKey).callCount, 0); + t.is(localStorage.setItem.withArgs(randomPasswordKey).callCount, 0); + t.is(localStorage.setItem.withArgs(deviceGroupKeyKey).callCount, 0); + }, + })); +}); -test.todo('getDeviceResponse() :: DEVICE_SRP_AUTH fails => calls onFailure'); -test.todo('getDeviceResponse() :: DEVICE_PASSWORD_VERIFIER fails => calls onFailure'); -test.todo('getDeviceResponse() :: succeeds => signs in and calls onSuccess'); +test.todo('sendCustomChallengeAnswer() :: fails => raises onFailure'); +test.todo('sendCustomChallengeAnswer() :: succeeds => raises onSuccess'); -test.todo('sendCustomChallengeAnswer() :: fails => calls onFailure'); -test.todo('sendCustomChallengeAnswer() :: succeeds => calls onSuccess'); +test.todo('sendMFACode() :: fails => raises onFailure'); +test.todo('sendMFACode() :: succeeds => raises onSuccess'); From b864b3b7d3e9816bb950a9edd81c87a00e48e32d Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Tue, 11 Oct 2016 20:27:14 +1300 Subject: [PATCH 11/17] Remove text-summary coverage reporter, same as "All Files" line. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a91305b0..48267ece 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "doc": "jsdoc src -d docs", "lint": "eslint src", "test": "ava", - "coverage": "cross-env BABEL_ENV=nyc nyc --reporter=text-summary --reporter=text --reporter=lcov ava", + "coverage": "cross-env BABEL_ENV=nyc nyc --reporter=text --reporter=lcov ava", "coverage:test": "cross-env BABEL_ENV=nyc nyc ava", "coverage:report": "nyc report --reporter=lcov" }, From b61eacf03678703b0ca31138a2410790dac2c812 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Wed, 12 Oct 2016 18:48:45 +1300 Subject: [PATCH 12/17] Mock out project deps for more trustworthy coverage And simplify module mocking. --- src/CognitoUser.authenticateUser.test.js | 40 +++++++++++++++++++ src/CognitoUser.test.js | 27 +++++++++---- src/_helpers.test.js | 51 +++++++++--------------- 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/src/CognitoUser.authenticateUser.test.js b/src/CognitoUser.authenticateUser.test.js index c4dfc5f7..f6444c99 100644 --- a/src/CognitoUser.authenticateUser.test.js +++ b/src/CognitoUser.authenticateUser.test.js @@ -183,10 +183,50 @@ function hexToBase64(hex) { return sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(hex)); } +class MockResultBase { + constructor(...params) { + this.params = params; + } +} + +class MockCognitoTokenBase extends MockResultBase {} +class MockCognitoAccessToken extends MockCognitoTokenBase { + getJwtToken() { + return this.params[0].AccessToken; + } +} +class MockCognitoIdToken extends MockCognitoTokenBase { + getJwtToken() { + return this.params[0].IdToken; + } +} +class MockCognitoRefreshToken extends MockCognitoTokenBase { + getToken() { + return this.params[0].RefreshToken; + } +} +class MockCognitoUserSession extends MockResultBase { + getAccessToken() { + return this.params[0].AccessToken; + } + + getIdToken() { + return this.params[0].IdToken; + } + + getRefreshToken() { + return this.params[0].RefreshToken; + } +} + function createUser({ pool = new MockUserPool() } = {}, ...requestConfigs) { pool.client = new MockClient(...requestConfigs); // eslint-disable-line no-param-reassign const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { './AuthenticationHelper': MockAuthenticationHelper, + './CognitoAccessToken': MockCognitoAccessToken, + './CognitoIdToken': MockCognitoIdToken, + './CognitoRefreshToken': MockCognitoRefreshToken, + './CognitoUserSession': MockCognitoUserSession, './DateHelper': MockDateHelper, }); return new CognitoUser({ Username: constructorUsername, Pool: pool }); diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index 08c4d2b6..969e6740 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -56,16 +56,18 @@ class MockSession { } } -function createUser({ pool = new MockUserPool(), session } = {}, ...requestConfigs) { +function createUser({ pool = new MockUserPool(), session, mocks } = {}, ...requestConfigs) { pool.client = new MockClient(...requestConfigs); // eslint-disable-line no-param-reassign - const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', { - // Nothing yet - }); + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser', mocks); const user = new CognitoUser({ Username, Pool: pool }); user.signInUserSession = session; return user; } +function createSignedInUserWithMocks(mocks, ...requests) { + return createUser({ session: new MockSession(), mocks }, ...requests); +} + function createSignedInUser(...requests) { return createUser({ session: new MockSession() }, ...requests); } @@ -249,15 +251,26 @@ test.cb(updateAttributesMacro, false); test.cb(updateAttributesMacro, true); function getUserAttributesMacro(t, succeeds) { + class MockCognitoUserAttribute { + constructor(...params) { + this.params = params; + } + } + const expectedError = createExpectedErrorFromSuccess(succeeds); const responseResult = succeeds ? { UserAttributes: attributes } : null; - const user = createSignedInUser([expectedError, responseResult]); + + const user = createSignedInUserWithMocks( + { + './CognitoUserAttribute': MockCognitoUserAttribute, + }, + [expectedError, responseResult]); user.getUserAttributes((err, result) => { t.is(err, expectedError); if (succeeds) { - // Only check for Name, Value properties, don't assert the results are CognitoUserAttributes. - t.deepEqual(result.map(({ Name, Value }) => ({ Name, Value })), attributes); + t.true(Array.isArray(result) && result.every(i => i instanceof MockCognitoUserAttribute)); + t.deepEqual(result.map(i => i.params[0]), attributes); } else { t.falsy(result); } diff --git a/src/_helpers.test.js b/src/_helpers.test.js index ebe17658..37bbb62f 100644 --- a/src/_helpers.test.js +++ b/src/_helpers.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { stub } from 'sinon'; -import mockRequire from 'mock-require'; +import mockRequire, { reRequire } from 'mock-require'; export class MockClient { constructor(...requestConfigs) { @@ -80,42 +80,29 @@ test.afterEach.always(t => { delete global.window; }); -function shouldMock(path) { - return path.startsWith(__dirname) && !path.endsWith('.test.js'); -} +const defaultMocks = { + 'aws-sdk/clients/cognitoidentityserviceprovider': MockClient, + './AuthenticationDetails': null, + './AuthenticationHelper': null, + './CognitoAccessToken': null, + './CognitoIdToken': null, + './CognitoRefreshToken': null, + './CognitoUser': null, + './CognitoUserAttribute': null, + './CognitoUserPool': null, + './CognitoUserSession': null, + './DateHelper': null, +}; function requireWithModuleMocks(request, moduleMocks = {}) { - const unmockedCache = Object.create(null); - - // Remove require.cache entries that may be using the unmocked modules - Object.keys(require.cache).forEach(path => { - if (shouldMock(path)) { - delete require.cache[path]; - } - }); - - // Always mock AWS SDK - mockRequire('aws-sdk/clients/cognitoidentityserviceprovider', MockClient); - - // Mock other modules - Object.keys(moduleMocks).forEach(name => { - mockRequire(name, moduleMocks[name]); - }); - - const mockedModule = require(request); - - // Restore require.cache to previous state - Object.keys(require.cache).forEach(path => { - if (shouldMock(path)) { - if (Object.prototype.hasOwnProperty.call(unmockedCache, path)) { - require.cache[path] = unmockedCache[path]; - } else { - delete require.cache[path]; - } + const allModuleMocks = Object.assign({}, defaultMocks, moduleMocks); + Object.keys(allModuleMocks).forEach(mockRequest => { + if (mockRequest !== request) { + mockRequire(mockRequest, allModuleMocks[mockRequest]); } }); - return mockedModule; + return reRequire(request); } export function requireDefaultWithModuleMocks(request, moduleMocks) { From 478c8a0eb6c58993be9e06db6d04600814dcb712 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Wed, 12 Oct 2016 21:32:06 +1300 Subject: [PATCH 13/17] Refactor authenticateUser tests, implement send*() tests --- src/CognitoUser.authenticateUser.test.js | 548 ++++++++++++++++------- src/CognitoUser.test.js | 25 +- src/_helpers.test.js | 30 ++ 3 files changed, 419 insertions(+), 184 deletions(-) diff --git a/src/CognitoUser.authenticateUser.test.js b/src/CognitoUser.authenticateUser.test.js index f6444c99..bc3b5110 100644 --- a/src/CognitoUser.authenticateUser.test.js +++ b/src/CognitoUser.authenticateUser.test.js @@ -3,7 +3,7 @@ import test from 'ava'; import { stub } from 'sinon'; import { BigInteger } from 'jsbn'; -import * as sjcl from 'sjcl'; +import { codec } from 'sjcl'; import { MockClient, @@ -11,8 +11,13 @@ import { requestSucceedsWith, requestFailsWith, createCallback, + title, } from './_helpers.test'; +function hexToBase64(hex) { + return codec.base64.fromBits(codec.hex.toBits(hex)); +} + const UserPoolId = 'xx-nowhere1_SomeUserPool'; // Constructor validates the format. const ClientId = 'some-client-id'; const constructorUsername = 'constructor-username'; @@ -23,7 +28,9 @@ const RefreshToken = 'some-refresh-token'; const AccessToken = 'some-access-token'; const SrpLargeAHex = '1a'.repeat(32); const SaltDevicesHex = '5d'.repeat(32); +const SaltDevicesBase64 = hexToBase64(SaltDevicesHex); const VerifierDevicesHex = 'ed'.repeat(32); +const VerifierDevicesBase64 = hexToBase64(VerifierDevicesHex); const RandomPasswordHex = 'a0'.repeat(32); const ValidationData = [ { Name: 'some-name-1', Value: 'some-value-1' }, @@ -32,6 +39,25 @@ const ValidationData = [ const dateNow = 'Wed Sep 21 07:36:54 UTC 2016'; +const keyPrefix = `CognitoIdentityServiceProvider.${ClientId}`; +const idTokenKey = `${keyPrefix}.${aliasUsername}.idToken`; +const accessTokenKey = `${keyPrefix}.${aliasUsername}.accessToken`; +const refreshTokenKey = `${keyPrefix}.${aliasUsername}.refreshToken`; +const lastAuthUserKey = `${keyPrefix}.LastAuthUser`; +const deviceKeyKey = `${keyPrefix}.${aliasUsername}.deviceKey`; +const randomPasswordKey = `${keyPrefix}.${aliasUsername}.randomPasswordKey`; +const deviceGroupKeyKey = `${keyPrefix}.${aliasUsername}.deviceGroupKey`; + +const oldDeviceKey = 'old-deviceKey'; + +const cachedDeviceKey = 'cached-deviceKey'; +const cachedRandomPassword = 'cached-randomPassword'; +const cachedDeviceGroupKey = 'cached-deviceGroup'; + +const newDeviceKey = 'new-deviceKey'; +const newDeviceGroupKey = 'new-deviceGroup'; +const deviceName = 'some-device-name'; + function createExpectedInitiateAuthArgs({ AuthFlow = 'USER_SRP_AUTH', extraAuthParameters, @@ -89,31 +115,96 @@ function createExpectedRespondToAuthChallengePasswordVerifierArgs( }; } -function createRespondToAuthChallengeCompleteResponse({ NewDeviceMetadata } = {}) { - return { - AuthenticationResult: { IdToken, AccessToken, RefreshToken, NewDeviceMetadata }, +function createRespondToAuthChallengeResponseForSuccess({ hasNewDevice } = {}) { + const response = { + AuthenticationResult: { IdToken, AccessToken, RefreshToken }, }; + + if (hasNewDevice) { + response.AuthenticationResult.NewDeviceMetadata = { + DeviceGroupKey: newDeviceGroupKey, + DeviceKey: newDeviceKey, + }; + } + + return response; } -function createRespondToAuthChallengeChallengeResponse({ - ChallengeName, - ChallengeParameters, -} = {}) { +function createRespondToAuthChallengeResponseForChallenge(challengeName) { return { - ChallengeName, - Session: `respondToAuthChallenge-${ChallengeName}-session`, - ChallengeParameters, + ChallengeName: challengeName, + Session: `respondToAuthChallenge-${challengeName}-session`, }; } -const keyPrefix = `CognitoIdentityServiceProvider.${ClientId}`; -const idTokenKey = `${keyPrefix}.${aliasUsername}.idToken`; -const accessTokenKey = `${keyPrefix}.${aliasUsername}.accessToken`; -const refreshTokenKey = `${keyPrefix}.${aliasUsername}.refreshToken`; -const lastAuthUserKey = `${keyPrefix}.LastAuthUser`; -const deviceKeyKey = `${keyPrefix}.${aliasUsername}.deviceKey`; -const randomPasswordKey = `${keyPrefix}.${aliasUsername}.randomPasswordKey`; -const deviceGroupKeyKey = `${keyPrefix}.${aliasUsername}.deviceGroupKey`; +function createRespondToAuthChallengeResponseForCustomChallenge() { + return Object.assign( + createRespondToAuthChallengeResponseForChallenge('CUSTOM_CHALLENGE'), + { + ChallengeParameters: { + Name: 'some-custom-challenge-parameter', + }, + } + ); +} + +function assertHasSetSignInSession(t, user) { + const userSession = user.getSignInUserSession(); + t.is(userSession.getIdToken().getJwtToken(), IdToken); + t.is(userSession.getAccessToken().getJwtToken(), AccessToken); + t.is(userSession.getRefreshToken().getToken(), RefreshToken); +} + +function assertHasDeviceState(t, user, { hasOldDevice, hasCachedDevice, hasNewDevice }) { + let expectedDeviceKey; + let expectedRandomPassword; + let expectedDeviceGroupKey; + + if (hasOldDevice) { + expectedDeviceKey = oldDeviceKey; + } + + if (hasCachedDevice) { + expectedDeviceKey = cachedDeviceKey; + expectedRandomPassword = cachedRandomPassword; + expectedDeviceGroupKey = cachedDeviceGroupKey; + } + + if (hasNewDevice) { + expectedDeviceKey = newDeviceKey; + expectedRandomPassword = RandomPasswordHex; + expectedDeviceGroupKey = newDeviceGroupKey; + } + + // FIXME: AuthenticationHelper.getVerifierDevices() returns hex, but CognitoUser expects sjcl bits + t.skip.is(user.verifierDevices, VerifierDevicesBase64); + t.is(user.deviceGroupKey, expectedDeviceGroupKey); + t.is(user.randomPassword, expectedRandomPassword); + t.is(user.deviceKey, expectedDeviceKey); +} + +function assertDidCacheTokens(t, localStorage) { + t.is(localStorage.setItem.withArgs(idTokenKey).callCount, 1); + t.is(localStorage.setItem.withArgs(accessTokenKey).callCount, 1); + t.is(localStorage.setItem.withArgs(refreshTokenKey).callCount, 1); + t.is(localStorage.setItem.withArgs(lastAuthUserKey).callCount, 1); + t.is(localStorage.setItem.withArgs(idTokenKey).args[0][1], IdToken); + t.is(localStorage.setItem.withArgs(accessTokenKey).args[0][1], AccessToken); + t.is(localStorage.setItem.withArgs(refreshTokenKey).args[0][1], RefreshToken); + t.is(localStorage.setItem.withArgs(lastAuthUserKey).args[0][1], aliasUsername); +} + +function assertDidCacheDeviceKeyAndPassword(t, localStorage) { + t.is(localStorage.setItem.withArgs(deviceKeyKey, newDeviceKey).callCount, 1); + t.is(localStorage.setItem.withArgs(randomPasswordKey, RandomPasswordHex).callCount, 1); + t.is(localStorage.setItem.withArgs(deviceGroupKeyKey, newDeviceGroupKey).callCount, 1); +} + +function assertDidNotCacheDeviceKeyAndPassword(t, localStorage) { + t.is(localStorage.setItem.withArgs(deviceKeyKey).callCount, 0); + t.is(localStorage.setItem.withArgs(randomPasswordKey).callCount, 0); + t.is(localStorage.setItem.withArgs(deviceGroupKeyKey).callCount, 0); +} class MockUserPool { constructor() { @@ -143,7 +234,7 @@ class MockAuthenticationHelper { } getPasswordAuthenticationKey() { - return sjcl.codec.hex.toBits('a4'.repeat(32)); + return codec.hex.toBits('a4'.repeat(32)); } generateHashDevice() { @@ -179,10 +270,6 @@ class MockAuthenticationDetails { } } -function hexToBase64(hex) { - return sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(hex)); -} - class MockResultBase { constructor(...params) { this.params = params; @@ -278,20 +365,14 @@ test.serial.cb('with new device state, fails on confirmDevice => raises onFailur setItem: stub(), }; global.window = { localStorage }; - global.navigator = { userAgent: 'some-device-name' }; + global.navigator = { userAgent: deviceName }; const expectedError = { code: 'InternalServerError' }; const user = createUser( {}, requestSucceedsWith(initiateAuthResponse), - requestSucceedsWith(createRespondToAuthChallengeCompleteResponse({ - NewDeviceMetadata: { - DeviceGroupKey: 'new-deviceGroup', - DeviceKey: 'new-deviceKey', - }, - })), - requestFailsWith(expectedError) - ); + requestSucceedsWith(createRespondToAuthChallengeResponseForSuccess({ hasNewDevice: true })), + requestFailsWith(expectedError)); user.authenticateUser( new MockAuthenticationDetails(), @@ -304,61 +385,25 @@ test.serial.cb('with new device state, fails on confirmDevice => raises onFailur })); }); -function deviceStateSuccessMacroTitle( - t, - { hasOldDevice, hasCachedDevice, hasNewDevice, userConfirmationNecessary } -) { - const context = [ - `${hasOldDevice ? 'with' : 'no'} old`, - `${hasCachedDevice ? 'with' : 'no'} cached`, - `${hasNewDevice ? 'with' : 'no'} new device state`, - ]; - - if (userConfirmationNecessary) { - context.push('user confirmation necessary'); - } - - return `${context.join(', ')} => creates session`; -} - function deviceStateSuccessMacro( t, { hasOldDevice, hasCachedDevice, hasNewDevice, userConfirmationNecessary } ) { - const oldDeviceKey = 'old-deviceKey'; - - const cachedDeviceKey = 'cached-deviceKey'; - const cachedRandomPassword = 'cached-randomPassword'; - const cachedDeviceGroupKey = 'cached-deviceGroup'; - - const newDeviceKey = 'new-deviceKey'; - const newDeviceGroupKey = 'new-deviceGroup'; - const deviceName = 'some-device-name'; - const localStorage = { getItem: stub().returns(null), setItem: stub(), }; global.window = { localStorage }; - let expectedDeviceKey; - let expectedRandomPassword; - let expectedDeviceGroupKey; let extraExpectedChallengeResponses; if (hasOldDevice) { - expectedDeviceKey = oldDeviceKey; - extraExpectedChallengeResponses = { DEVICE_KEY: oldDeviceKey, }; } if (hasCachedDevice) { - expectedDeviceKey = cachedDeviceKey; - expectedRandomPassword = cachedRandomPassword; - expectedDeviceGroupKey = cachedDeviceGroupKey; - localStorage.getItem.withArgs(deviceKeyKey).returns(cachedDeviceKey); localStorage.getItem.withArgs(randomPasswordKey).returns(cachedRandomPassword); localStorage.getItem.withArgs(deviceGroupKeyKey).returns(cachedDeviceGroupKey); @@ -369,13 +414,7 @@ function deviceStateSuccessMacro( } if (hasNewDevice) { - expectedDeviceKey = newDeviceKey; - expectedRandomPassword = RandomPasswordHex; - expectedDeviceGroupKey = newDeviceGroupKey; - - global.navigator = { - userAgent: deviceName, - }; + global.navigator = { userAgent: deviceName }; } const expectedInitiateAuthArgs = createExpectedInitiateAuthArgs({ @@ -391,34 +430,20 @@ function deviceStateSuccessMacro( DeviceKey: newDeviceKey, AccessToken, DeviceSecretVerifierConfig: { - Salt: hexToBase64(SaltDevicesHex), - PasswordVerifier: hexToBase64(VerifierDevicesHex), + Salt: SaltDevicesBase64, + PasswordVerifier: VerifierDevicesBase64, }, DeviceName: deviceName, }; const requests = [ requestSucceedsWith(initiateAuthResponse), + requestSucceedsWith(createRespondToAuthChallengeResponseForSuccess({ hasNewDevice })), ]; - if (!hasNewDevice) { - requests.push( - requestSucceedsWith(createRespondToAuthChallengeCompleteResponse()) - ); - } else { + if (hasNewDevice) { requests.push( - requestSucceedsWith(createRespondToAuthChallengeCompleteResponse({ - NewDeviceMetadata: { - DeviceGroupKey: newDeviceGroupKey, - DeviceKey: newDeviceKey, - }, - })), requestSucceedsWith({ - AuthenticationResult: { - NewDeviceMetadata: { - DeviceKey: newDeviceKey, - }, - }, UserConfirmationNecessary: userConfirmationNecessary, }) ); @@ -452,39 +477,25 @@ function deviceStateSuccessMacro( t.deepEqual(user.client.getRequestCallArgs(2).args, expectedConfirmDeviceArgs); } - // Check user state t.is(user.getUsername(), aliasUsername); t.is(user.Session, null); - t.is(user.deviceKey, expectedDeviceKey); - t.is(user.randomPassword, expectedRandomPassword); - t.is(user.deviceGroupKey, expectedDeviceGroupKey); - - // Check sign-in session - const userSession = user.getSignInUserSession(); - t.is(userSession.getIdToken().getJwtToken(), IdToken); - t.is(userSession.getAccessToken().getJwtToken(), AccessToken); - t.is(userSession.getRefreshToken().getToken(), RefreshToken); - - // check cacheTokens() - t.is(localStorage.setItem.withArgs(idTokenKey, IdToken).callCount, 1); - t.is(localStorage.setItem.withArgs(accessTokenKey, AccessToken).callCount, 1); - t.is(localStorage.setItem.withArgs(refreshTokenKey, RefreshToken).callCount, 1); - t.is(localStorage.setItem.withArgs(lastAuthUserKey, aliasUsername).callCount, 1); - - // check cacheDeviceKeyAndPassword() + + assertHasDeviceState(t, user, { hasOldDevice, hasCachedDevice, hasNewDevice }); + + assertHasSetSignInSession(t, user); + assertDidCacheTokens(t, localStorage); + if (hasNewDevice) { - t.is(localStorage.setItem.withArgs(deviceKeyKey, newDeviceKey).callCount, 1); - t.is(localStorage.setItem.withArgs(randomPasswordKey, RandomPasswordHex).callCount, 1); - t.is(localStorage.setItem.withArgs(deviceGroupKeyKey, newDeviceGroupKey).callCount, 1); + assertDidCacheDeviceKeyAndPassword(t, localStorage); } else { - t.is(localStorage.setItem.withArgs(deviceKeyKey).callCount, 0); - t.is(localStorage.setItem.withArgs(randomPasswordKey).callCount, 0); - t.is(localStorage.setItem.withArgs(deviceGroupKeyKey).callCount, 0); + assertDidNotCacheDeviceKeyAndPassword(t, localStorage); } }, })); } -deviceStateSuccessMacro.title = deviceStateSuccessMacroTitle; +deviceStateSuccessMacro.title = (_, context) => ( + title(null, { context, outcome: 'creates session' }) +); for (const hasOldDevice of [false, true]) { for (const hasCachedDevice of [false, true]) { @@ -503,10 +514,6 @@ test.serial.cb('CUSTOM_AUTH flow, CUSTOM_CHALLENGE challenge => raises customCha }; global.window = { localStorage }; - const expectedChallengeParameters = { - Name: 'some-custom-challenge-parameter', - }; - const expectedInitiateAuthArgs = createExpectedInitiateAuthArgs({ AuthFlow: 'CUSTOM_AUTH', extraAuthParameters: { @@ -517,10 +524,8 @@ test.serial.cb('CUSTOM_AUTH flow, CUSTOM_CHALLENGE challenge => raises customCha const expectedRespondToAuthChallengeArgs = createExpectedRespondToAuthChallengePasswordVerifierArgs(); - const respondToAuthChallengeResponse = createRespondToAuthChallengeChallengeResponse({ - ChallengeName: 'CUSTOM_CHALLENGE', - ChallengeParameters: expectedChallengeParameters, - }); + const respondToAuthChallengeResponse = + createRespondToAuthChallengeResponseForCustomChallenge(); const user = createUser( {}, @@ -533,7 +538,7 @@ test.serial.cb('CUSTOM_AUTH flow, CUSTOM_CHALLENGE challenge => raises customCha new MockAuthenticationDetails(), createCallback(t, t.end, { customChallenge(parameters) { - t.deepEqual(parameters, expectedChallengeParameters); + t.deepEqual(parameters, respondToAuthChallengeResponse.ChallengeParameters); // check client requests (expanded due to assert string depth limit) t.is(user.client.requestCallCount, 2); @@ -561,9 +566,8 @@ test.serial.cb('SMS_MFA challenge => raises mfaRequired', t => { const expectedRespondToAuthChallengeArgs = createExpectedRespondToAuthChallengePasswordVerifierArgs(); - const respondToAuthChallengeResponse = createRespondToAuthChallengeChallengeResponse({ - ChallengeName: 'SMS_MFA', - }); + const respondToAuthChallengeResponse = + createRespondToAuthChallengeResponseForChallenge('SMS_MFA'); const user = createUser( {}, @@ -600,9 +604,7 @@ test.serial.cb('DEVICE_SRP_AUTH challenge, fails on DEVICE_SRP_AUTH => raises on const user = createUser( {}, requestSucceedsWith(initiateAuthResponse), - requestSucceedsWith(createRespondToAuthChallengeChallengeResponse({ - ChallengeName: 'DEVICE_SRP_AUTH', - })), + requestSucceedsWith(createRespondToAuthChallengeResponseForChallenge('DEVICE_SRP_AUTH')), requestFailsWith(expectedError)); user.authenticateUser( @@ -632,9 +634,7 @@ test.serial.cb( const user = createUser( {}, requestSucceedsWith(initiateAuthResponse), - requestSucceedsWith(createRespondToAuthChallengeChallengeResponse({ - ChallengeName: 'DEVICE_SRP_AUTH', - })), + requestSucceedsWith(createRespondToAuthChallengeResponseForChallenge('DEVICE_SRP_AUTH')), requestSucceedsWith({ ChallengeParameters: initiateAuthResponse.ChallengeParameters, }), @@ -679,9 +679,7 @@ test.serial.cb('DEVICE_SRP_AUTH challenge, succeeds => creates session', t => { }); const respondToAuthChallengePasswordVerifierResponse = - createRespondToAuthChallengeChallengeResponse({ - ChallengeName: 'DEVICE_SRP_AUTH', - }); + createRespondToAuthChallengeResponseForChallenge('DEVICE_SRP_AUTH'); // FIXME: should be using a separate set of AuthenticationHelper mock results. const expectedRespondToAuthChallengeDeviceSrpAuthArgs = { @@ -714,7 +712,7 @@ test.serial.cb('DEVICE_SRP_AUTH challenge, succeeds => creates session', t => { requestSucceedsWith(initiateAuthResponse), requestSucceedsWith(respondToAuthChallengePasswordVerifierResponse), requestSucceedsWith(respondToAuthChallengeDeviceSrpAuthResponse), - requestSucceedsWith(createRespondToAuthChallengeCompleteResponse()) + requestSucceedsWith(createRespondToAuthChallengeResponseForSuccess()) ); user.authenticateUser( @@ -748,29 +746,257 @@ test.serial.cb('DEVICE_SRP_AUTH challenge, succeeds => creates session', t => { t.is(user.randomPassword, randomPassword); t.is(user.deviceGroupKey, deviceGroupKey); - // Check sign-in session - const userSession = user.getSignInUserSession(); - t.is(userSession.getIdToken().getJwtToken(), IdToken); - t.is(userSession.getAccessToken().getJwtToken(), AccessToken); - t.is(userSession.getRefreshToken().getToken(), RefreshToken); - - // check cacheTokens() - t.is(localStorage.setItem.withArgs(idTokenKey, IdToken).callCount, 1); - t.is(localStorage.setItem.withArgs(accessTokenKey, AccessToken).callCount, 1); - t.is(localStorage.setItem.withArgs(refreshTokenKey, RefreshToken).callCount, 1); - t.is(localStorage.setItem.withArgs(lastAuthUserKey, aliasUsername).callCount, 1); - - // check cacheDeviceKeyAndPassword() - t.is(localStorage.setItem.withArgs(deviceKeyKey).callCount, 0); - t.is(localStorage.setItem.withArgs(randomPasswordKey).callCount, 0); - t.is(localStorage.setItem.withArgs(deviceGroupKeyKey).callCount, 0); + assertHasSetSignInSession(t, user); + assertDidCacheTokens(t, localStorage); + assertDidNotCacheDeviceKeyAndPassword(t, localStorage); + }, + })); +}); + +test.cb('sendCustomChallengeAnswer() :: fails => raises onFailure', t => { + const expectedError = { code: 'InternalServerError' }; + const previousChallengeSession = 'previous-challenge-session'; + + const user = createUser({}, requestFailsWith(expectedError)); + user.Session = previousChallengeSession; + + const answerChallenge = 'some-answer-challenge'; + user.sendCustomChallengeAnswer(answerChallenge, createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + + t.is(user.client.requestCallCount, 1); + t.is(user.client.getRequestCallArgs(0).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(0).args, { + ChallengeName: 'CUSTOM_CHALLENGE', + ChallengeResponses: { + USERNAME: constructorUsername, + ANSWER: answerChallenge, + }, + ClientId, + Session: previousChallengeSession, + }); + + t.is(user.getUsername(), constructorUsername); + t.is(user.Session, previousChallengeSession); + }, + })); +}); + +test.cb( + 'sendCustomChallengeAnswer() :: CUSTOM_CHALLENGE challenge => raises customChallenge', + t => { + const respondToAuthChallengeResponse = + createRespondToAuthChallengeResponseForCustomChallenge(); + const previousChallengeSession = 'previous-challenge-session'; + + const user = createUser({}, requestSucceedsWith(respondToAuthChallengeResponse)); + user.Session = previousChallengeSession; + + const answerChallenge = 'some-answer-challenge'; + user.sendCustomChallengeAnswer(answerChallenge, createCallback(t, t.end, { + customChallenge(challengeParameters) { + // Looks like a bug: Uses response `challengeParameters` not `ChallengeParameters` like + // authenticateUser() does. + t.skip.is(challengeParameters, respondToAuthChallengeResponse.ChallengeParameters); + + t.is(user.client.requestCallCount, 1); + t.is(user.client.getRequestCallArgs(0).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(0).args, { + ChallengeName: 'CUSTOM_CHALLENGE', + ChallengeResponses: { + USERNAME: constructorUsername, + ANSWER: answerChallenge, + }, + ClientId, + Session: previousChallengeSession, + }); + + t.is(user.getUsername(), constructorUsername); + t.is(user.Session, respondToAuthChallengeResponse.Session); }, })); + }); + +test.serial.cb('sendCustomChallengeAnswer() :: succeeds => creates session', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + const respondToAuthChallengeResponse = + createRespondToAuthChallengeResponseForSuccess(); + + const previousChallengeSession = 'previous-challenge-session'; + + const user = createUser({}, requestSucceedsWith(respondToAuthChallengeResponse)); + user.username = aliasUsername; // assertDidCacheTokens() expects this + user.Session = previousChallengeSession; + + const answerChallenge = 'some-answer-challenge'; + + user.sendCustomChallengeAnswer(answerChallenge, createCallback(t, t.end, { + onSuccess(signInSessionArg) { + t.is(signInSessionArg, user.getSignInUserSession()); + + // check client requests (expanded due to assert string depth limit) + t.is(user.client.requestCallCount, 1); + t.is(user.client.getRequestCallArgs(0).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(0).args, { + ChallengeName: 'CUSTOM_CHALLENGE', + ChallengeResponses: { + USERNAME: aliasUsername, + ANSWER: answerChallenge, + }, + ClientId, + Session: previousChallengeSession, + }); + + // Check user state + t.is(user.getUsername(), aliasUsername); + t.skip.is(user.Session, null); // FIXME: should be clearing Session like authenticateUser() + + assertHasSetSignInSession(t, user); + assertDidCacheTokens(t, localStorage); + assertDidNotCacheDeviceKeyAndPassword(t, localStorage); + }, + })); }); -test.todo('sendCustomChallengeAnswer() :: fails => raises onFailure'); -test.todo('sendCustomChallengeAnswer() :: succeeds => raises onSuccess'); +test.cb('sendMFACode() :: fails on respondToAuthChallenge => raises onFailure', t => { + const expectedError = { code: 'InternalServerError' }; + const previousChallengeSession = 'previous-challenge-session'; -test.todo('sendMFACode() :: fails => raises onFailure'); -test.todo('sendMFACode() :: succeeds => raises onSuccess'); + const user = createUser({}, requestFailsWith(expectedError)); + user.Session = previousChallengeSession; + const confirmationCode = 'some-confirmation-code'; + user.sendMFACode(confirmationCode, createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + + t.is(user.getUsername(), constructorUsername); + t.is(user.Session, previousChallengeSession); + }, + })); +}); + +test.serial.cb('sendMFACode() :: fails on confirmDevice => raises onFailure', t => { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const expectedError = { code: 'InternalServerError' }; + + const respondToAuthChallengeResponse = createRespondToAuthChallengeResponseForSuccess({ + hasNewDevice: true, + }); + + const previousChallengeSession = 'previous-challenge-session'; + + const user = createUser( + {}, + requestSucceedsWith(respondToAuthChallengeResponse), + requestFailsWith(expectedError)); + user.Session = previousChallengeSession; + + const confirmationCode = 'some-confirmation-code'; + user.sendMFACode(confirmationCode, createCallback(t, t.end, { + onFailure(err) { + t.is(err, expectedError); + }, + })); +}); + +function sendMFACodeSucceedsMacro(t, { hasOldDevice, hasNewDevice, userConfirmationNecessary }) { + const localStorage = { + getItem: stub().returns(null), + setItem: stub(), + }; + global.window = { localStorage }; + + const previousChallengeSession = 'previous-challenge-session'; + const confirmationCode = 'some-confirmation-code'; + + const expectedRespondToAuthChallengeArgs = { + ChallengeName: 'SMS_MFA', + ChallengeResponses: { + USERNAME: aliasUsername, + SMS_MFA_CODE: confirmationCode, + }, + ClientId, + Session: previousChallengeSession, + }; + + if (hasOldDevice) { + expectedRespondToAuthChallengeArgs.ChallengeResponses.DEVICE_KEY = oldDeviceKey; + } + + const user = createUser( + {}, + requestSucceedsWith(createRespondToAuthChallengeResponseForSuccess({ hasNewDevice })), + requestSucceedsWith({ + UserConfirmationNecessary: userConfirmationNecessary, + })); + + user.username = aliasUsername; // assertDidCacheTokens() expects this + user.Session = previousChallengeSession; + + if (hasOldDevice) { + user.deviceKey = oldDeviceKey; + } + + user.sendMFACode(confirmationCode, createCallback(t, t.end, { + onSuccess(signInUserSessionArg, userConfirmationNecessaryArg) { + t.is(signInUserSessionArg, user.getSignInUserSession()); + if (userConfirmationNecessary) { + t.true(userConfirmationNecessaryArg); + } else { + t.falsy(userConfirmationNecessaryArg); + } + + t.is(user.client.requestCallCount, hasNewDevice ? 2 : 1); + t.is(user.client.getRequestCallArgs(0).name, 'respondToAuthChallenge'); + t.deepEqual(user.client.getRequestCallArgs(0).args, expectedRespondToAuthChallengeArgs); + if (hasNewDevice) { + t.is(user.client.getRequestCallArgs(1).name, 'confirmDevice'); + t.deepEqual(user.client.getRequestCallArgs(1).args, { + DeviceKey: newDeviceKey, + AccessToken, + DeviceSecretVerifierConfig: { + Salt: SaltDevicesBase64, + PasswordVerifier: VerifierDevicesBase64, + }, + DeviceName: deviceName, + }); + } + + t.is(user.getUsername(), aliasUsername); + t.is(user.Session, previousChallengeSession); + + assertHasDeviceState(t, user, { hasOldDevice, hasNewDevice }); + + assertDidCacheTokens(t, localStorage); + if (hasNewDevice) { + assertDidCacheDeviceKeyAndPassword(t, localStorage); + } else { + assertDidNotCacheDeviceKeyAndPassword(t, localStorage); + } + }, + })); +} +sendMFACodeSucceedsMacro.title = (_, context) => ( + title('sendMFACode', { context, outcome: 'creates session' }) +); + +for (const hasOldDevice of [false, true]) { + for (const hasNewDevice of [false, true]) { + test.serial.cb(sendMFACodeSucceedsMacro, { hasOldDevice, hasNewDevice }); + } +} +test.serial.cb( + sendMFACodeSucceedsMacro, + { hasNewDevice: true, userConfirmationNecessary: true } +); diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index 969e6740..0d87ad32 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -8,6 +8,8 @@ import { requestCalledOnceWith, createCallback, createBasicCallback, + title, + addSimpleTitle, } from './_helpers.test'; // Valid property values: constructor, request props, etc... @@ -80,29 +82,6 @@ function createExpectedErrorFromSuccess(succeeds) { return succeeds ? null : { code: 'InternalServerException' }; } -function titleMapString(value) { - return value && typeof value === 'object' - ? Object.keys(value).map(key => `${key}: ${JSON.stringify(value[key])}`).join(', ') - : value || ''; -} - -function title(fn, { args, context, succeeds, outcome = succeeds ? 'succeeds' : 'fails' }) { - const fnString = typeof fn === 'function' ? fn.name.replace(/Macro$/, '') : fn; - const contextString = context ? ` :: ${titleMapString(context)}` : ''; - return `${fnString}(${titleMapString(args)})${contextString} => ${outcome}`; -} - -function addSimpleTitle(macro, { args, context } = {}) { - // eslint-disable-next-line no-param-reassign - macro.title = (_, succeeds, ...values) => ( - title(macro, { - succeeds, - args: args && args(...values), - context: context && context(...values), - }) - ); -} - function constructorRequiredParamsMacro(t, data) { const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); diff --git a/src/_helpers.test.js b/src/_helpers.test.js index 37bbb62f..5515af0a 100644 --- a/src/_helpers.test.js +++ b/src/_helpers.test.js @@ -108,3 +108,33 @@ function requireWithModuleMocks(request, moduleMocks = {}) { export function requireDefaultWithModuleMocks(request, moduleMocks) { return requireWithModuleMocks(request, moduleMocks).default; } + +function titleMapString(value) { + return value && typeof value === 'object' + ? Object.keys(value).map(key => `${key}: ${JSON.stringify(value[key])}`).join(', ') + : value || ''; +} + +export function title( + fn, + { args, context, succeeds, outcome = succeeds ? 'succeeds' : 'fails' } +) { + const fnString = typeof fn === 'function' ? fn.name.replace(/Macro$/, '') : fn; + const callString = fn || args ? `${fnString}(${titleMapString(args)})` : ''; + const contextString = titleMapString(context); + const prefixString = callString && contextString + ? `${callString} :: ${contextString}` + : callString || contextString; + return `${prefixString} => ${outcome}`; +} + +export function addSimpleTitle(macro, { args, context } = {}) { + // eslint-disable-next-line no-param-reassign + macro.title = (_, succeeds, ...values) => ( + title(macro, { + succeeds, + args: args && args(...values), + context: context && context(...values), + }) + ); +} From 247b458681811508fbaa90851e386bd44326b26c Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 31 Oct 2016 09:40:31 +1300 Subject: [PATCH 14/17] Implement `nyc --all`: 52% statements --- .babelrc | 5 ++++- package.json | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.babelrc b/.babelrc index 562a2e67..cf59a3a3 100644 --- a/.babelrc +++ b/.babelrc @@ -2,6 +2,9 @@ "presets": [ "es2015" ], + "only": [ + "src" + ], "env": { "nyc": { "sourceMaps": "inline", @@ -10,4 +13,4 @@ ] } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 48267ece..6824b0d0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "ava": "^0.16.0", "babel-core": "^6.13.2", "babel-loader": "^6.2.4", - "babel-plugin-istanbul": "^2.0.1", + "babel-plugin-istanbul": "^2.0.3", "babel-preset-es2015": "^6.13.2", "babel-register": "^6.14.0", "cross-env": "^2.0.1", @@ -58,7 +58,7 @@ "eslint-plugin-import": "^1.13.0", "jsdoc": "^3.4.0", "mock-require": "^1.3.0", - "nyc": "^8.3.0", + "nyc": "^8.4.0-candidate", "sinon": "^1.17.5", "webpack": "^1.13.1" }, @@ -69,7 +69,12 @@ "timeout": "30s" }, "nyc": { + "all": true, "cache": true, + "include": "src", + "require": [ + "babel-register" + ], "sourceMap": false, "instrument": false } From 6ba9dc9213006581664640264a505b5051e27391 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 5 Dec 2016 17:57:51 +1300 Subject: [PATCH 15/17] Fix deleteUser test. --- src/CognitoUser.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index 0d87ad32..76690fe2 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -1,6 +1,7 @@ /* eslint-disable require-jsdoc */ import test from 'ava'; +import { stub } from 'sinon'; import { MockClient, @@ -199,6 +200,11 @@ test.cb(disableMFAMacro, false); test.cb(disableMFAMacro, true); function deleteUserMacro(t, succeeds) { + const localStorage = { + removeItem: stub(), + }; + global.window = { localStorage }; + const expectedError = createExpectedErrorFromSuccess(succeeds); const user = createSignedInUserWithExpectedError(expectedError); @@ -209,8 +215,8 @@ function deleteUserMacro(t, succeeds) { }); } addSimpleTitle(deleteUserMacro); -test.cb(deleteUserMacro, false); -test.cb(deleteUserMacro, true); +test.serial.cb(deleteUserMacro, false); +test.serial.cb(deleteUserMacro, true); function updateAttributesMacro(t, succeeds) { const expectedError = createExpectedErrorFromSuccess(succeeds); From e519d28efe47cee2386fe9a85b914673b167b120 Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 5 Dec 2016 18:46:19 +1300 Subject: [PATCH 16/17] Use a testSpec() helper in CognitoUser to reduce repetition. --- src/CognitoUser.test.js | 640 ++++++++++++++++++++++------------------ 1 file changed, 347 insertions(+), 293 deletions(-) diff --git a/src/CognitoUser.test.js b/src/CognitoUser.test.js index 76690fe2..0cc0876e 100644 --- a/src/CognitoUser.test.js +++ b/src/CognitoUser.test.js @@ -10,7 +10,6 @@ import { createCallback, createBasicCallback, title, - addSimpleTitle, } from './_helpers.test'; // Valid property values: constructor, request props, etc... @@ -84,18 +83,62 @@ function createExpectedErrorFromSuccess(succeeds) { } -function constructorRequiredParamsMacro(t, data) { - const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); - t.throws(() => new CognitoUser(data), /required/); +function testSpec({ + title: macroTitle, + cb = true, + serial = false, + macro, + cases = [ + [false], + [true], + ], +}) { + if (typeof macroTitle === 'string') { + // eslint-disable-next-line no-param-reassign + macro.title = (_, succeeds) => title(macroTitle, { succeeds }); + } else if (typeof macroTitle === 'function') { + // eslint-disable-next-line no-param-reassign + macro.title = (_, ...args) => macroTitle(...args); + } else { + // eslint-disable-next-line no-param-reassign + macro.title = (_, succeeds, ...values) => ( + title(macroTitle.name, { + succeeds, + args: macroTitle.args && macroTitle.args(...values), + context: macroTitle.context && macroTitle.context(...values), + }) + ); + } + let testMethod = test; + if (cb) { + testMethod = testMethod.cb; + } + if (serial) { + testMethod = testMethod.serial; + } + for (const testCase of cases) { + testMethod(macro, ...testCase); + } } -constructorRequiredParamsMacro.title = (_, data) => ( - title('constructor', { args: data, outcome: 'throws "required"' }) -); -test(constructorRequiredParamsMacro, null); -test(constructorRequiredParamsMacro, {}); -test(constructorRequiredParamsMacro, { Username: null, Pool: null }); -test(constructorRequiredParamsMacro, { Username: null, Pool: new MockUserPool() }); -test(constructorRequiredParamsMacro, { Username, Pool: null }); + + +testSpec({ + title(data) { + return title('constructor', { args: data, outcome: 'throws "required"' }); + }, + cb: false, + macro(t, data) { + const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); + t.throws(() => new CognitoUser(data), /required/); + }, + cases: [ + [null], + [{}], + [{ Username: null, Pool: null }], + [{ Username: null, Pool: new MockUserPool() }], + [{ Username, Pool: null }], + ], +}); test('constructor() :: valid => creates expected instance', t => { const CognitoUser = requireDefaultWithModuleMocks('./CognitoUser'); @@ -120,304 +163,315 @@ test('setAuthenticationFlowType() => sets authentication flow type', t => { // See CognitoUser.authenticateUser.test.js for authenticateUser() and the challenge responses -function confirmRegistrationMacro(t, forceAliasCreation, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createUser({}, [expectedError]); - - user.confirmRegistration(confirmationCode, forceAliasCreation, err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'confirmSignUp', { - ClientId, - ConfirmationCode: confirmationCode, - Username, - ForceAliasCreation: forceAliasCreation, - }); - t.end(); - }); -} -confirmRegistrationMacro.title = (_, forceAliasCreation, succeeds) => ( - title(confirmRegistrationMacro, { succeeds, args: { forceAliasCreation } }) -); -test.cb(confirmRegistrationMacro, false, false); -test.cb(confirmRegistrationMacro, true, false); -test.cb(confirmRegistrationMacro, false, true); -test.cb(confirmRegistrationMacro, true, true); - -function changePasswordMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - const oldUserPassword = 'swordfish'; - const newUserPassword = 'slaughterfish'; - user.changePassword(oldUserPassword, newUserPassword, err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'changePassword', { - PreviousPassword: oldUserPassword, - ProposedPassword: newUserPassword, - AccessToken, +testSpec({ + title(forceAliasCreation, succeeds) { + return title('confirmRegistration', { succeeds, args: { forceAliasCreation } }); + }, + macro(t, forceAliasCreation, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createUser({}, [expectedError]); + + user.confirmRegistration(confirmationCode, forceAliasCreation, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'confirmSignUp', { + ClientId, + ConfirmationCode: confirmationCode, + Username, + ForceAliasCreation: forceAliasCreation, + }); + t.end(); }); - t.end(); - }); -} -addSimpleTitle(changePasswordMacro); -test.cb(changePasswordMacro, false); -test.cb(changePasswordMacro, true); - -function enableMFAMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - user.enableMFA(err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'setUserSettings', { - MFAOptions: [ - { DeliveryMedium: 'SMS', AttributeName: 'phone_number' }, - ], - AccessToken, + }, + cases: [ + [false, false], + [true, false], + [false, true], + [true, true], + ], +}); + +testSpec({ + title: 'changePassword', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const oldUserPassword = 'swordfish'; + const newUserPassword = 'slaughterfish'; + user.changePassword(oldUserPassword, newUserPassword, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'changePassword', { + PreviousPassword: oldUserPassword, + ProposedPassword: newUserPassword, + AccessToken, + }); + t.end(); }); - t.end(); - }); -} -addSimpleTitle(enableMFAMacro); -test.cb(enableMFAMacro, false); -test.cb(enableMFAMacro, true); - -function disableMFAMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - user.disableMFA(err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'setUserSettings', { - MFAOptions: [], - AccessToken, + }, +}); + +testSpec({ + title: 'enableMFA', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.enableMFA(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'setUserSettings', { + MFAOptions: [ + { DeliveryMedium: 'SMS', AttributeName: 'phone_number' }, + ], + AccessToken, + }); + t.end(); }); - t.end(); - }); -} -addSimpleTitle(disableMFAMacro); -test.cb(disableMFAMacro, false); -test.cb(disableMFAMacro, true); - -function deleteUserMacro(t, succeeds) { - const localStorage = { - removeItem: stub(), - }; - global.window = { localStorage }; - - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - user.deleteUser(err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'deleteUser', { AccessToken }); - t.end(); - }); -} -addSimpleTitle(deleteUserMacro); -test.serial.cb(deleteUserMacro, false); -test.serial.cb(deleteUserMacro, true); - -function updateAttributesMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - user.updateAttributes(attributes, err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'updateUserAttributes', { - UserAttributes: attributes, - AccessToken, + }, +}); + +testSpec({ + title: 'disableMFA', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.disableMFA(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'setUserSettings', { + MFAOptions: [], + AccessToken, + }); + t.end(); }); - t.end(); - }); -} -addSimpleTitle(updateAttributesMacro); -test.cb(updateAttributesMacro, false); -test.cb(updateAttributesMacro, true); - -function getUserAttributesMacro(t, succeeds) { - class MockCognitoUserAttribute { - constructor(...params) { - this.params = params; - } - } + }, +}); - const expectedError = createExpectedErrorFromSuccess(succeeds); - const responseResult = succeeds ? { UserAttributes: attributes } : null; - const user = createSignedInUserWithMocks( - { - './CognitoUserAttribute': MockCognitoUserAttribute, - }, - [expectedError, responseResult]); - - user.getUserAttributes((err, result) => { - t.is(err, expectedError); - if (succeeds) { - t.true(Array.isArray(result) && result.every(i => i instanceof MockCognitoUserAttribute)); - t.deepEqual(result.map(i => i.params[0]), attributes); - } else { - t.falsy(result); - } - requestCalledOnceWith(t, user.client, 'getUser', { AccessToken }); - t.end(); - }); -} -addSimpleTitle(getUserAttributesMacro); -test.cb(getUserAttributesMacro, false); -test.cb(getUserAttributesMacro, true); +testSpec({ + title: 'deleteUser', + serial: true, + macro(t, succeeds) { + const localStorage = { + removeItem: stub(), + }; + global.window = { localStorage }; -function deleteAttributesMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); - const attributeList = attributes.map(a => a.Name); + user.deleteUser(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'deleteUser', { AccessToken }); + t.end(); + }); + }, +}); - user.deleteAttributes(attributeList, err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'deleteUserAttributes', { - UserAttributeNames: attributeList, - AccessToken, +testSpec({ + title: 'updateAttributes', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.updateAttributes(attributes, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'updateUserAttributes', { + UserAttributes: attributes, + AccessToken, + }); + t.end(); }); - t.end(); - }); -} -addSimpleTitle(deleteAttributesMacro); -test.cb(deleteAttributesMacro, false); -test.cb(deleteAttributesMacro, true); - -function resendConfirmationCodeMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - user.resendConfirmationCode(err => { - t.is(err, expectedError); - requestCalledOnceWith(t, user.client, 'resendConfirmationCode', { ClientId, Username }); - t.end(); - }); -} -addSimpleTitle(resendConfirmationCodeMacro); -test.cb(resendConfirmationCodeMacro, false); -test.cb(resendConfirmationCodeMacro, true); - -function forgotPasswordMacro(t, succeeds, usingInputVerificationCode) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const expectedData = !succeeds ? null : { CodeDeliveryDetails }; - const request = [expectedError, expectedData]; - const user = createSignedInUser(request); - - function done() { - requestCalledOnceWith(t, user.client, 'forgotPassword', { ClientId, Username }); - t.end(); - } + }, +}); - const callback = !usingInputVerificationCode ? - createBasicCallback(t, succeeds, expectedError, done) : - createCallback(t, done, { - onFailure(err) { - t.false(succeeds); - t.is(err, expectedError); - }, - inputVerificationCode(data) { - t.true(succeeds); - t.is(data, expectedData); +testSpec({ + title: 'getUserAttributes', + macro(t, succeeds) { + class MockCognitoUserAttribute { + constructor(...params) { + this.params = params; + } + } + + const expectedError = createExpectedErrorFromSuccess(succeeds); + const responseResult = succeeds ? { UserAttributes: attributes } : null; + + const user = createSignedInUserWithMocks( + { + './CognitoUserAttribute': MockCognitoUserAttribute, }, + [expectedError, responseResult]); + + user.getUserAttributes((err, result) => { + t.is(err, expectedError); + if (succeeds) { + t.true(Array.isArray(result) && result.every(i => i instanceof MockCognitoUserAttribute)); + t.deepEqual(result.map(i => i.params[0]), attributes); + } else { + t.falsy(result); + } + requestCalledOnceWith(t, user.client, 'getUser', { AccessToken }); + t.end(); }); + }, +}); - user.forgotPassword(callback); -} -addSimpleTitle(forgotPasswordMacro, { - context(usingInputVerificationCode) { - return { usingInputVerificationCode }; +testSpec({ + title: 'deleteUserAttributes', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const attributeList = attributes.map(a => a.Name); + + user.deleteAttributes(attributeList, err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'deleteUserAttributes', { + UserAttributeNames: attributeList, + AccessToken, + }); + t.end(); + }); }, }); -test.cb(forgotPasswordMacro, false, false); -test.cb(forgotPasswordMacro, true, false); -test.cb(forgotPasswordMacro, false, true); -test.cb(forgotPasswordMacro, true, true); - -function confirmPasswordMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - const newPassword = 'swordfish'; - const callback = createBasicCallback(t, succeeds, expectedError, () => { - requestCalledOnceWith(t, user.client, 'confirmForgotPassword', { - ClientId, - Username, - ConfirmationCode: confirmationCode, - Password: newPassword, + +testSpec({ + title: 'resendConfirmationCode', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + user.resendConfirmationCode(err => { + t.is(err, expectedError); + requestCalledOnceWith(t, user.client, 'resendConfirmationCode', { ClientId, Username }); + t.end(); }); - t.end(); - }); - user.confirmPassword(confirmationCode, newPassword, callback); -} -addSimpleTitle(confirmPasswordMacro); -test.cb(confirmPasswordMacro, false); -test.cb(confirmPasswordMacro, true); - -function getAttributeVerificationCodeMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const expectedData = !succeeds ? null : { CodeDeliveryDetails }; - const user = createSignedInUser([expectedError, expectedData]); - - function done() { - requestCalledOnceWith(t, user.client, 'getUserAttributeVerificationCode', { - AttributeName: attributeName, - AccessToken, + }, +}); + +testSpec({ + title: { + name: 'forgotPassword', + context(usingInputVerificationCode) { + return { usingInputVerificationCode }; + }, + }, + macro(t, succeeds, usingInputVerificationCode) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const expectedData = !succeeds ? null : { CodeDeliveryDetails }; + const request = [expectedError, expectedData]; + const user = createSignedInUser(request); + + function done() { + requestCalledOnceWith(t, user.client, 'forgotPassword', { ClientId, Username }); + t.end(); + } + + const callback = !usingInputVerificationCode ? + createBasicCallback(t, succeeds, expectedError, done) : + createCallback(t, done, { + onFailure(err) { + t.false(succeeds); + t.is(err, expectedError); + }, + inputVerificationCode(data) { + t.true(succeeds); + t.is(data, expectedData); + }, + }); + + user.forgotPassword(callback); + }, + cases: [ + [false, false], + [true, false], + [false, true], + [true, true], + ], +}); + +testSpec({ + title: 'confirmPassword', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const newPassword = 'swordfish'; + const callback = createBasicCallback(t, succeeds, expectedError, () => { + requestCalledOnceWith(t, user.client, 'confirmForgotPassword', { + ClientId, + Username, + ConfirmationCode: confirmationCode, + Password: newPassword, + }); + t.end(); }); - t.end(); - } - user.getAttributeVerificationCode( - attributeName, - createCallback(t, done, { - onFailure(err) { - t.false(succeeds); - t.is(err, expectedError); - }, - inputVerificationCode(data) { - t.true(succeeds); - t.is(data, expectedData); - }, - })); -} -addSimpleTitle(getAttributeVerificationCodeMacro); -test.cb(getAttributeVerificationCodeMacro, false); -test.cb(getAttributeVerificationCodeMacro, true); - -function verifyAttributeMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - - const callback = createBasicCallback(t, succeeds, expectedError, () => { - requestCalledOnceWith(t, user.client, 'verifyUserAttribute', { - AttributeName: attributeName, - Code: confirmationCode, - AccessToken, + user.confirmPassword(confirmationCode, newPassword, callback); + }, +}); + +testSpec({ + title: 'getAttributeVerificationCode', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const expectedData = !succeeds ? null : { CodeDeliveryDetails }; + const user = createSignedInUser([expectedError, expectedData]); + + function done() { + requestCalledOnceWith(t, user.client, 'getUserAttributeVerificationCode', { + AttributeName: attributeName, + AccessToken, + }); + t.end(); + } + user.getAttributeVerificationCode( + attributeName, + createCallback(t, done, { + onFailure(err) { + t.false(succeeds); + t.is(err, expectedError); + }, + inputVerificationCode(data) { + t.true(succeeds); + t.is(data, expectedData); + }, + })); + }, +}); + +testSpec({ + title: 'verifyAttribute', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + + const callback = createBasicCallback(t, succeeds, expectedError, () => { + requestCalledOnceWith(t, user.client, 'verifyUserAttribute', { + AttributeName: attributeName, + Code: confirmationCode, + AccessToken, + }); + t.end(); }); - t.end(); - }); - user.verifyAttribute(attributeName, confirmationCode, callback); -} -addSimpleTitle(verifyAttributeMacro); -test.cb(verifyAttributeMacro, false); -test.cb(verifyAttributeMacro, true); - -function getDeviceMacro(t, succeeds) { - const expectedError = createExpectedErrorFromSuccess(succeeds); - const user = createSignedInUserWithExpectedError(expectedError); - const expectedDeviceKey = 'some-device-key'; - user.deviceKey = expectedDeviceKey; - - const callback = createBasicCallback(t, succeeds, expectedError, () => { - requestCalledOnceWith(t, user.client, 'getDevice', { - AccessToken, - DeviceKey: expectedDeviceKey, + user.verifyAttribute(attributeName, confirmationCode, callback); + }, +}); + +testSpec({ + title: 'getDevice', + macro(t, succeeds) { + const expectedError = createExpectedErrorFromSuccess(succeeds); + const user = createSignedInUserWithExpectedError(expectedError); + const expectedDeviceKey = 'some-device-key'; + user.deviceKey = expectedDeviceKey; + + const callback = createBasicCallback(t, succeeds, expectedError, () => { + requestCalledOnceWith(t, user.client, 'getDevice', { + AccessToken, + DeviceKey: expectedDeviceKey, + }); + t.end(); }); - t.end(); - }); - user.getDevice(callback); -} -addSimpleTitle(getDeviceMacro); -test.cb(getDeviceMacro, false); -test.cb(getDeviceMacro, true); + user.getDevice(callback); + }, +}); From 554fe70b506e50c251aa19b994fdb9cf8471adaa Mon Sep 17 00:00:00 2001 From: Simon Buchan Date: Mon, 5 Dec 2016 18:54:42 +1300 Subject: [PATCH 17/17] Update test/coverage packages --- .gitignore | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 576ca9e8..e0ce9776 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/node_modules/ +node_modules/ npm-debug.log /docs/ diff --git a/package.json b/package.json index 6824b0d0..f464e232 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,10 @@ "sjcl": "^1.0.3" }, "devDependencies": { - "ava": "^0.16.0", + "ava": "^0.17.0", "babel-core": "^6.13.2", "babel-loader": "^6.2.4", - "babel-plugin-istanbul": "^2.0.3", + "babel-plugin-istanbul": "^3.0.0", "babel-preset-es2015": "^6.13.2", "babel-register": "^6.14.0", "cross-env": "^2.0.1", @@ -58,7 +58,7 @@ "eslint-plugin-import": "^1.13.0", "jsdoc": "^3.4.0", "mock-require": "^1.3.0", - "nyc": "^8.4.0-candidate", + "nyc": "^10.0.0", "sinon": "^1.17.5", "webpack": "^1.13.1" },