From 21bbba399fe8db5b3d2a114f50d9983493814fb8 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Mon, 21 Mar 2016 08:16:37 +0200 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D1=82=D0=B5=D0=B9=D1=82=D0=BE=D0=B2=20AuthFlow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/services/authFlow/AuthFlow.js | 9 +- src/services/authFlow/CompleteState.js | 18 +- src/services/authFlow/FinishState.js | 2 +- tests/.eslintrc.json | 11 + tests/index.js | 4 + .../services/authFlow/ActivationState.test.js | 86 +++++ .../authFlow/ChangePasswordState.test.js | 86 +++++ tests/services/authFlow/CompleteState.test.js | 305 ++++++++++++++++++ tests/services/authFlow/FinishState.test.js | 29 ++ tests/services/authFlow/LoginState.test.js | 97 ++++++ tests/services/authFlow/OAuthState.test.js | 67 ++++ tests/services/authFlow/PasswordState.test.js | 119 +++++++ .../authFlow/PermissionsState.test.js | 56 ++++ tests/services/authFlow/RegisterState.test.js | 81 +++++ tests/services/authFlow/helpers.js | 29 ++ webpack.config.js | 1 + 17 files changed, 985 insertions(+), 17 deletions(-) create mode 100644 tests/.eslintrc.json create mode 100644 tests/index.js create mode 100644 tests/services/authFlow/ActivationState.test.js create mode 100644 tests/services/authFlow/ChangePasswordState.test.js create mode 100644 tests/services/authFlow/CompleteState.test.js create mode 100644 tests/services/authFlow/FinishState.test.js create mode 100644 tests/services/authFlow/LoginState.test.js create mode 100644 tests/services/authFlow/OAuthState.test.js create mode 100644 tests/services/authFlow/PasswordState.test.js create mode 100644 tests/services/authFlow/PermissionsState.test.js create mode 100644 tests/services/authFlow/RegisterState.test.js create mode 100644 tests/services/authFlow/helpers.js diff --git a/package.json b/package.json index c5d498d..268b202 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "less-loader": "^2.0.0", "mocha": "^2.2.5", "node-sass": "^3.4.2", - "phantomjs": "^1.9.18", + "phantomjs-prebuilt": "^2.0.0", "postcss-loader": "^0.8.0", "react-addons-test-utils": "^0.14.3", "sass-loader": "^3.1.2", diff --git a/src/services/authFlow/AuthFlow.js b/src/services/authFlow/AuthFlow.js index 66d87ae..abc9c55 100644 --- a/src/services/authFlow/AuthFlow.js +++ b/src/services/authFlow/AuthFlow.js @@ -10,14 +10,13 @@ import ForgotPasswordState from './ForgotPasswordState'; const availableActions = { ...actions, - updateUser + updateUser, + redirect(url) { + location.href = url; + } }; export default class AuthFlow { - constructor(states) { - this.states = states; - } - setStore(store) { this.navigate = (route) => { const {routing} = this.getState(); diff --git a/src/services/authFlow/CompleteState.js b/src/services/authFlow/CompleteState.js index f6f83a7..fb74ddd 100644 --- a/src/services/authFlow/CompleteState.js +++ b/src/services/authFlow/CompleteState.js @@ -9,14 +9,11 @@ export default class CompleteState extends AbstractState { constructor(options = {}) { super(options); - if ('accept' in options) { - this.isPermissionsAccepted = options.accept; - this.isUserReviewedPermissions = true; - } + this.isPermissionsAccepted = options.accept; } enter(context) { - const {auth, user} = context.getState(); + const {auth = {}, user} = context.getState(); if (user.isGuest) { context.setState(new LoginState()); @@ -28,18 +25,19 @@ export default class CompleteState extends AbstractState { if (auth.oauth.code) { context.setState(new FinishState()); } else { - let data = {}; - if (this.isUserReviewedPermissions) { + const data = {}; + if (typeof this.isPermissionsAccepted !== 'undefined') { data.accept = this.isPermissionsAccepted; } context.run('oAuthComplete', data).then((resp) => { - if (resp.redirectUri.startsWith('static_page')) { + // TODO: пусть в стейт попадает флаг или тип авторизации + // вместо волшебства над редирект урлой + if (resp.redirectUri.indexOf('static_page') === 0) { context.setState(new FinishState()); } else { - location.href = resp.redirectUri; + context.run('redirect', resp.redirectUri); } }, (resp) => { - // TODO if (resp.unauthorized) { context.setState(new LoginState()); } else if (resp.acceptRequired) { diff --git a/src/services/authFlow/FinishState.js b/src/services/authFlow/FinishState.js index 275a03e..4e30399 100644 --- a/src/services/authFlow/FinishState.js +++ b/src/services/authFlow/FinishState.js @@ -1,6 +1,6 @@ import AbstractState from './AbstractState'; -export default class CompleteState extends AbstractState { +export default class FinishState extends AbstractState { enter(context) { context.navigate('/oauth/finish'); } diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json new file mode 100644 index 0000000..4c8b034 --- /dev/null +++ b/tests/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + + "globals": { + "sinon": true + } +} diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..fded395 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,4 @@ +// require all modules ending in "_test" from the +// current directory and all subdirectories +var testsContext = require.context(".", true, /\.test\.js$/); +testsContext.keys().forEach(testsContext); diff --git a/tests/services/authFlow/ActivationState.test.js b/tests/services/authFlow/ActivationState.test.js new file mode 100644 index 0000000..2238441 --- /dev/null +++ b/tests/services/authFlow/ActivationState.test.js @@ -0,0 +1,86 @@ +import ActivationState from 'services/authFlow/ActivationState'; +import CompleteState from 'services/authFlow/CompleteState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('ActivationState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new ActivationState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /activation', () => { + context.getState.returns({ + user: { + isGuest: false, + isActive: false + } + }); + + expectNavigate(mock, '/activation'); + + state.enter(context); + }); + + it('should transition to complete state if account activated', () => { + context.getState.returns({ + user: { + isGuest: false, + isActive: true + } + }); + + expectState(mock, CompleteState); + + state.enter(context); + }); + }); + + describe('#resolve', () => { + it('should call activate with payload', () => { + const payload = {}; + + expectRun( + mock, + 'activate', + sinon.match.same(payload) + ).returns({then() {}}); + + state.resolve(context, payload); + }); + + it('should transition to complete state on success', () => { + const promise = Promise.resolve(); + + mock.expects('run').returns(promise); + expectState(mock, CompleteState); + + state.resolve(context); + + return promise; + }); + + it('should NOT transition to complete state on fail', () => { + const promise = Promise.reject(); + + mock.expects('run').returns(promise); + mock.expects('setState').never(); + + state.resolve(context); + + return promise.catch(mock.verify.bind(mock)); + }); + }); +}); diff --git a/tests/services/authFlow/ChangePasswordState.test.js b/tests/services/authFlow/ChangePasswordState.test.js new file mode 100644 index 0000000..e1f3fd7 --- /dev/null +++ b/tests/services/authFlow/ChangePasswordState.test.js @@ -0,0 +1,86 @@ +import ChangePasswordState from 'services/authFlow/ChangePasswordState'; +import CompleteState from 'services/authFlow/CompleteState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('ChangePasswordState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new ChangePasswordState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /change-password', () => { + context.getState.returns({ + user: {isGuest: true} + }); + + expectNavigate(mock, '/change-password'); + + state.enter(context); + }); + }); + + describe('#resolve', () => { + it('should call changePassword with payload', () => { + const payload = {}; + + expectRun( + mock, + 'changePassword', + sinon.match.same(payload) + ).returns({then() {}}); + + state.resolve(context, payload); + }); + + it('should transition to complete state on success', () => { + const promise = Promise.resolve(); + + mock.expects('run').returns(promise); + expectState(mock, CompleteState); + + state.resolve(context); + + return promise; + }); + + it('should NOT transition to complete state on fail', () => { + const promise = Promise.reject(); + + mock.expects('run').returns(promise); + mock.expects('setState').never(); + + state.resolve(context); + + return promise.catch(mock.verify.bind(mock)); + }); + }); + + describe('#reject', () => { + it('should transition to complete state and mark that password should not be changed', () => { + expectRun( + mock, + 'updateUser', + sinon.match({ + shouldChangePassword: false + }) + ); + + expectState(mock, CompleteState); + + state.reject(context); + }); + }); +}); diff --git a/tests/services/authFlow/CompleteState.test.js b/tests/services/authFlow/CompleteState.test.js new file mode 100644 index 0000000..c0cb839 --- /dev/null +++ b/tests/services/authFlow/CompleteState.test.js @@ -0,0 +1,305 @@ +import CompleteState from 'services/authFlow/CompleteState'; +import LoginState from 'services/authFlow/LoginState'; +import ActivationState from 'services/authFlow/ActivationState'; +import ChangePasswordState from 'services/authFlow/ChangePasswordState'; +import FinishState from 'services/authFlow/FinishState'; +import PermissionsState from 'services/authFlow/PermissionsState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('CompleteState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new CompleteState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to / for authenticated', () => { + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: {} + }); + + expectNavigate(mock, '/'); + + state.enter(context); + }); + + it('should transition to login for guests', () => { + context.getState.returns({ + user: { + isGuest: true + }, + auth: {} + }); + + expectState(mock, LoginState); + + state.enter(context); + }); + + it('should transition to activation if account is not activated', () => { + context.getState.returns({ + user: { + isGuest: false + }, + auth: {} + }); + + expectState(mock, ActivationState); + + state.enter(context); + }); + + it('should transition to change-password if shouldChangePassword', () => { + context.getState.returns({ + user: { + shouldChangePassword: true, + isActive: true, + isGuest: false + }, + auth: {} + }); + + expectState(mock, ChangePasswordState); + + state.enter(context); + }); + + it('should transition to activation with higher priority than shouldChangePassword', () => { + context.getState.returns({ + user: { + shouldChangePassword: true, + isGuest: false + }, + auth: {} + }); + + expectState(mock, ActivationState); + + state.enter(context); + }); + + it('should transition to finish state if code is present', () => { + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by', + code: 'XXX' + } + } + }); + + expectState(mock, FinishState); + + state.enter(context); + }); + }); + + describe('oAuthComplete', () => { + it('should run oAuthComplete', () => { + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by' + } + } + }); + + expectRun( + mock, + 'oAuthComplete', + sinon.match.object + ).returns({then() {}}); + + state.enter(context); + }); + + it('should listen for auth success/failure', () => { + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by' + } + } + }); + + expectRun( + mock, + 'oAuthComplete', + sinon.match.object + ).returns({then(success, fail) { + expect(success).to.be.a('function'); + expect(fail).to.be.a('function'); + }}); + + state.enter(context); + }); + + it('should transition run redirect by default', () => { + const expectedUrl = 'foo/bar'; + const promise = Promise.resolve({redirectUri: expectedUrl}); + + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by' + } + } + }); + + expectRun( + mock, + 'oAuthComplete', + sinon.match.object + ).returns(promise); + expectRun( + mock, + 'redirect', + expectedUrl + ); + + state.enter(context); + + return promise.catch(mock.verify.bind(mock)); + }); + + const testOAuth = (type, resp, expectedInstance) => { + const promise = Promise[type](resp); + + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by' + } + } + }); + + expectRun( + mock, + 'oAuthComplete', + sinon.match.object + ).returns(promise); + expectState(mock, expectedInstance); + + state.enter(context); + + return promise.catch(mock.verify.bind(mock)); + }; + + it('should transition to finish state if rejected with static_page', () => { + return testOAuth('resolve', {redirectUri: 'static_page'}, FinishState); + }); + + it('should transition to finish state if rejected with static_page_with_code', () => { + return testOAuth('resolve', {redirectUri: 'static_page_with_code'}, FinishState); + }); + + it('should transition to login state if rejected with unauthorized', () => { + return testOAuth('reject', {unauthorized: true}, LoginState); + }); + + it('should transition to permissions state if rejected with acceptRequired', () => { + return testOAuth('reject', {acceptRequired: true}, PermissionsState); + }); + }) + + describe('permissions accept', () => { + it('should set flags, when user accepted permissions', () => { + state = new CompleteState(); + expect(state.isPermissionsAccepted).to.be.undefined; + + state = new CompleteState({accept: undefined}); + expect(state.isPermissionsAccepted).to.be.undefined; + + state = new CompleteState({accept: true}); + expect(state.isPermissionsAccepted).to.be.true; + + state = new CompleteState({accept: false}); + expect(state.isPermissionsAccepted).to.be.false; + }); + + it('should run oAuthComplete passing accept: true', () => { + const expected = {accept: true}; + + state = new CompleteState(expected); + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by' + } + } + }); + + mock.expects('run').once().withExactArgs( + 'oAuthComplete', + sinon.match(expected) + ).returns({then() {}}); + + state.enter(context); + }); + + it('should run oAuthComplete passing accept: false', () => { + const expected = {accept: false}; + + state = new CompleteState(expected); + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + auth: { + oauth: { + clientId: 'ely.by' + } + } + }); + + expectRun( + mock, + 'oAuthComplete', + sinon.match(expected) + ).returns({then() {}}); + + state.enter(context); + }); + }); +}); diff --git a/tests/services/authFlow/FinishState.test.js b/tests/services/authFlow/FinishState.test.js new file mode 100644 index 0000000..556385d --- /dev/null +++ b/tests/services/authFlow/FinishState.test.js @@ -0,0 +1,29 @@ +import FinishState from 'services/authFlow/FinishState'; + +import { bootstrap, expectNavigate } from './helpers'; + +describe('FinishState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new FinishState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /oauth/finish', () => { + expectNavigate(mock, '/oauth/finish'); + + state.enter(context); + }); + }); +}); diff --git a/tests/services/authFlow/LoginState.test.js b/tests/services/authFlow/LoginState.test.js new file mode 100644 index 0000000..4c62e3e --- /dev/null +++ b/tests/services/authFlow/LoginState.test.js @@ -0,0 +1,97 @@ +import LoginState from 'services/authFlow/LoginState'; +import PasswordState from 'services/authFlow/PasswordState'; +import ForgotPasswordState from 'services/authFlow/ForgotPasswordState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('LoginState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new LoginState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /login', () => { + context.getState.returns({ + user: {isGuest: true} + }); + + expectNavigate(mock, '/login'); + + state.enter(context); + }); + + const testTransitionToPassword = (user) => { + context.getState.returns({ + user: user + }); + + expectState(mock, PasswordState); + + state.enter(context); + }; + + it('should transition to password if has email', () => { + testTransitionToPassword({email: 'foo'}); + }); + + it('should transition to password if has username', () => { + testTransitionToPassword({username: 'foo'}); + }); + }); + + describe('#resolve', () => { + it('should call login with email or username', () => { + const payload = {}; + + expectRun( + mock, + 'login', + sinon.match.same(payload) + ).returns({then() {}}); + + state.resolve(context, payload); + }); + + it('should transition to password state on successfull login (first phase)', () => { + const promise = Promise.resolve(); + + mock.expects('run').returns(promise); + expectState(mock, PasswordState); + + state.resolve(context); + + return promise; + }); + + it('should NOT transition to password state on fail', () => { + const promise = Promise.reject(); + + mock.expects('run').returns(promise); + mock.expects('setState').never(); + + state.resolve(context); + + return promise.catch(mock.verify.bind(mock)); + }); + }); + + describe('#reject', () => { + it('should transition to forgot password state', () => { + expectState(mock, ForgotPasswordState); + + state.reject(context); + }); + }); +}); diff --git a/tests/services/authFlow/OAuthState.test.js b/tests/services/authFlow/OAuthState.test.js new file mode 100644 index 0000000..97254d4 --- /dev/null +++ b/tests/services/authFlow/OAuthState.test.js @@ -0,0 +1,67 @@ +import OAuthState from 'services/authFlow/OAuthState'; +import CompleteState from 'services/authFlow/CompleteState'; + +import { bootstrap, expectState, expectRun } from './helpers'; + +describe('OAuthState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new OAuthState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should run oAuthValidate', () => { + const query = { + client_id: 'client_id', + redirect_uri: 'redirect_uri', + response_type: 'response_type', + scope: 'scope', + state: 'state' + }; + + context.getState.returns({ + routing: {location: {query}} + }); + + expectRun( + mock, + 'oAuthValidate', + sinon.match({ + clientId: query.client_id, + redirectUrl: query.redirect_uri, + responseType: query.response_type, + scope: query.scope, + state: query.state + }) + ).returns({then() {}}); + + state.enter(context); + }); + + it('should transition to complete state on success', () => { + const promise = Promise.resolve(); + + context.getState.returns({ + routing: {location: {query: {}}} + }); + + mock.expects('run').returns(promise); + expectState(mock, CompleteState); + + state.enter(context); + + return promise; + }); + }); +}); diff --git a/tests/services/authFlow/PasswordState.test.js b/tests/services/authFlow/PasswordState.test.js new file mode 100644 index 0000000..e55d52d --- /dev/null +++ b/tests/services/authFlow/PasswordState.test.js @@ -0,0 +1,119 @@ +import PasswordState from 'services/authFlow/PasswordState'; +import CompleteState from 'services/authFlow/CompleteState'; +import LoginState from 'services/authFlow/LoginState'; +import ForgotPasswordState from 'services/authFlow/ForgotPasswordState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('PasswordState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new PasswordState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /password', () => { + context.getState.returns({ + user: {isGuest: true} + }); + + expectNavigate(mock, '/password'); + + state.enter(context); + }); + + it('should transition to complete if not guest', () => { + context.getState.returns({ + user: {isGuest: false} + }); + + expectState(mock, CompleteState); + + state.enter(context); + }); + }); + + describe('#resolve', () => { + (() => { + const expectedLogin = 'login'; + const expectedPassword = 'password'; + + const testWith = (user) => { + it(`should call login with email or username and password. User: ${JSON.stringify(user)}`, () => { + context.getState.returns({user}); + + expectRun( + mock, + 'login', + sinon.match({ + login: expectedLogin, + password: expectedPassword + }) + ).returns({then() {}}); + + state.resolve(context, {password: expectedPassword}); + }); + }; + + testWith({ + email: expectedLogin + }); + + testWith({ + username: expectedLogin + }); + + testWith({ + email: expectedLogin, + username: expectedLogin + }); + }); + + it('should transition to complete state on successfull login', () => { + const promise = Promise.resolve(); + const expectedLogin = 'login'; + const expectedPassword = 'password'; + + context.getState.returns({ + user: { + email: expectedLogin + } + }); + + mock.expects('run').returns(promise); + expectState(mock, CompleteState); + + state.resolve(context, {password: expectedPassword}); + + return promise; + }); + }); + + describe('#reject', () => { + it('should transition to forgot password state', () => { + expectState(mock, ForgotPasswordState); + + state.reject(context); + }); + }); + + describe('#goBack', () => { + it('should transition to forgot password state', () => { + expectRun(mock, 'logout'); + expectState(mock, LoginState); + + state.goBack(context); + }); + }); +}); diff --git a/tests/services/authFlow/PermissionsState.test.js b/tests/services/authFlow/PermissionsState.test.js new file mode 100644 index 0000000..2826130 --- /dev/null +++ b/tests/services/authFlow/PermissionsState.test.js @@ -0,0 +1,56 @@ +import PermissionsState from 'services/authFlow/PermissionsState'; +import CompleteState from 'services/authFlow/CompleteState'; + +import { bootstrap, expectNavigate } from './helpers'; + +describe('PermissionsState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new PermissionsState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /oauth/permissions', () => { + expectNavigate(mock, '/oauth/permissions'); + + state.enter(context); + }); + }); + + describe('#resolve', () => { + it('should transition to complete state with acceptance', () => { + mock.expects('setState').once().withExactArgs( + sinon.match.instanceOf(CompleteState) + .and( + sinon.match.has('isPermissionsAccepted', true) + ) + ); + + state.resolve(context); + }); + }); + + describe('#reject', () => { + it('should transition to complete state without acceptance', () => { + mock.expects('setState').once().withExactArgs( + sinon.match.instanceOf(CompleteState) + .and( + sinon.match.has('isPermissionsAccepted', false) + ) + ); + + state.reject(context); + }); + }); +}); diff --git a/tests/services/authFlow/RegisterState.test.js b/tests/services/authFlow/RegisterState.test.js new file mode 100644 index 0000000..2484f68 --- /dev/null +++ b/tests/services/authFlow/RegisterState.test.js @@ -0,0 +1,81 @@ +import RegisterState from 'services/authFlow/RegisterState'; +import CompleteState from 'services/authFlow/CompleteState'; + +import { bootstrap, expectState, expectNavigate, expectRun } from './helpers'; + +describe('RegisterState', () => { + let state; + let context; + let mock; + + beforeEach(() => { + state = new RegisterState(); + + const data = bootstrap(); + context = data.context; + mock = data.mock; + }); + + afterEach(() => { + mock.verify(); + }); + + describe('#enter', () => { + it('should navigate to /register', () => { + context.getState.returns({ + user: {isGuest: true} + }); + + expectNavigate(mock, '/register'); + + state.enter(context); + }); + + it('should transition to complete if not guest', () => { + context.getState.returns({ + user: {isGuest: false} + }); + + expectState(mock, CompleteState); + + state.enter(context); + }); + }); + + describe('#resolve', () => { + it('should register on resolve', () => { + const payload = {}; + + expectRun( + mock, + 'register', + sinon.match.same(payload) + ).returns({then() {}}); + + state.resolve(context, payload); + }); + + it('should transition to complete after register', () => { + const payload = {}; + const promise = Promise.resolve(); + + mock.expects('run').returns(promise); + expectState(mock, CompleteState); + + state.resolve(context, payload); + + return promise; + }); + + it('should NOT transition to complete if register fails', () => { + const promise = Promise.reject(); + + mock.expects('run').returns(promise); + mock.expects('setState').never(); + + state.resolve(context); + + return promise.catch(mock.verify.bind(mock)); + }); + }); +}); diff --git a/tests/services/authFlow/helpers.js b/tests/services/authFlow/helpers.js new file mode 100644 index 0000000..77bebf3 --- /dev/null +++ b/tests/services/authFlow/helpers.js @@ -0,0 +1,29 @@ +export function bootstrap() { + const context = { + getState: sinon.stub(), + run() {}, + setState() {}, + navigate() {} + }; + + const mock = sinon.mock(context); + mock.expects('run').never(); + mock.expects('navigate').never(); + mock.expects('setState').never(); + + return {context, mock}; +} + +export function expectState(mock, state) { + return mock.expects('setState').once().withExactArgs( + sinon.match.instanceOf(state) + ); +} + +export function expectNavigate(mock, route) { + return mock.expects('navigate').once().withExactArgs(route); +} + +export function expectRun(mock, ...args) { + return mock.expects('run').once().withExactArgs(...args); +} diff --git a/webpack.config.js b/webpack.config.js index 4afb668..e096b7a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,6 +15,7 @@ var iconfontImporter = require('./webpack/node-sass-iconfont-importer'); * https://github.com/glenjamin/ultimate-hot-reloading-example ( обратить внимание на плагины babel ) * https://github.com/gajus/react-css-modules ( + BrowserSync) * https://github.com/reactuate/reactuate + * https://github.com/insin/nwb * * Inspiration projects: * https://github.com/davezuko/react-redux-starter-kit