diff --git a/src/services/authFlow/CompleteState.js b/src/services/authFlow/CompleteState.js index 00ac4f2..15ae37e 100644 --- a/src/services/authFlow/CompleteState.js +++ b/src/services/authFlow/CompleteState.js @@ -17,7 +17,7 @@ export default class CompleteState extends AbstractState { } enter(context) { - const {auth = {}, user, accounts} = context.getState(); + const {auth = {}, user} = context.getState(); if (user.isGuest) { context.setState(new LoginState()); @@ -26,67 +26,75 @@ export default class CompleteState extends AbstractState { } else if (user.shouldAcceptRules) { context.setState(new AcceptRulesState()); } else if (auth.oauth && auth.oauth.clientId) { - let isSwitcherEnabled = auth.isSwitcherEnabled; - - if (auth.oauth.loginHint) { - const account = accounts.available.filter((account) => - account.id === auth.oauth.loginHint * 1 - || account.email === auth.oauth.loginHint - || account.username === auth.oauth.loginHint - )[0]; - - if (account) { - // disable switching, because we are know the account, user must be authorized with - context.run('setAccountSwitcher', false); - isSwitcherEnabled = false; - - if (account.id !== accounts.active.id) { - // lets switch user to an account, that is needed for auth - return context.run('authenticate', account) - .then(() => context.setState(new CompleteState())); - } - } - } - - if (isSwitcherEnabled - && (accounts.available.length > 1 - || auth.oauth.prompt.includes(PROMPT_ACCOUNT_CHOOSE) - ) - ) { - context.setState(new ChooseAccountState()); - } else if (auth.oauth.code) { - context.setState(new FinishState()); - } else { - const data = {}; - if (typeof this.isPermissionsAccepted !== 'undefined') { - data.accept = this.isPermissionsAccepted; - } else if (auth.oauth.acceptRequired || auth.oauth.prompt.includes(PROMPT_PERMISSIONS)) { - context.setState(new PermissionsState()); - return; - } - // TODO: it seams that oAuthComplete may be a separate state - return context.run('oAuthComplete', data).then((resp) => { - // TODO: пусть в стейт попадает флаг или тип авторизации - // вместо волшебства над редирект урлой - if (resp.redirectUri.indexOf('static_page') === 0) { - context.setState(new FinishState()); - } else { - return new Promise(() => { - // do not resolve promise to make loader visible and - // overcome app rendering - context.run('redirect', resp.redirectUri); - }); - } - }, (resp) => { - if (resp.unauthorized) { - context.setState(new LoginState()); - } else if (resp.acceptRequired) { - context.setState(new PermissionsState()); - } - }); - } + return this.processOAuth(context); } else { context.navigate('/'); } } + + processOAuth(context) { + const {auth = {}, accounts} = context.getState(); + + let isSwitcherEnabled = auth.isSwitcherEnabled; + const loginHint = auth.oauth.loginHint; + + if (loginHint) { + const account = accounts.available.filter((account) => + account.id === loginHint * 1 + || account.email === loginHint + || account.username === loginHint + )[0]; + + if (account) { + // disable switching, because we are know the account, user must be authorized with + context.run('setAccountSwitcher', false); + isSwitcherEnabled = false; + + if (account.id !== accounts.active.id) { + // lets switch user to an account, that is needed for auth + return context.run('authenticate', account) + .then(() => context.setState(new CompleteState())); + } + } + } + + if (isSwitcherEnabled + && (accounts.available.length > 1 + || auth.oauth.prompt.includes(PROMPT_ACCOUNT_CHOOSE) + ) + ) { + context.setState(new ChooseAccountState()); + } else if (auth.oauth.code) { + context.setState(new FinishState()); + } else { + const data = {}; + if (typeof this.isPermissionsAccepted !== 'undefined') { + data.accept = this.isPermissionsAccepted; + } else if (auth.oauth.acceptRequired || auth.oauth.prompt.includes(PROMPT_PERMISSIONS)) { + context.setState(new PermissionsState()); + return; + } + + // TODO: it seams that oAuthComplete may be a separate state + return context.run('oAuthComplete', data).then((resp) => { + // TODO: пусть в стейт попадает флаг или тип авторизации + // вместо волшебства над редирект урлой + if (resp.redirectUri.indexOf('static_page') === 0) { + context.setState(new FinishState()); + } else { + return new Promise(() => { + // do not resolve promise to make loader visible and + // overcome app rendering + context.run('redirect', resp.redirectUri); + }); + } + }, (resp) => { + if (resp.unauthorized) { + context.setState(new LoginState()); + } else if (resp.acceptRequired) { + context.setState(new PermissionsState()); + } + }); + } + } } diff --git a/tests/services/authFlow/CompleteState.test.js b/tests/services/authFlow/CompleteState.test.js index b8149e5..dbbc468 100644 --- a/tests/services/authFlow/CompleteState.test.js +++ b/tests/services/authFlow/CompleteState.test.js @@ -1,4 +1,5 @@ import expect from 'unexpected'; +import sinon from 'sinon'; import CompleteState from 'services/authFlow/CompleteState'; import LoginState from 'services/authFlow/LoginState'; @@ -135,7 +136,7 @@ describe('CompleteState', () => { }); }); - describe('oAuthComplete', () => { + describe('when user completes oauth', () => { it('should run oAuthComplete', () => { context.getState.returns({ user: { @@ -185,7 +186,7 @@ describe('CompleteState', () => { state.enter(context); }); - it('should transition run redirect by default', () => { + it('should run redirect by default', () => { const expectedUrl = 'foo/bar'; const promise = Promise.resolve({redirectUri: expectedUrl}); @@ -261,6 +262,122 @@ describe('CompleteState', () => { it('should transition to permissions state if rejected with acceptRequired', () => testOAuth('reject', {acceptRequired: true}, PermissionsState) ); + + describe('when loginHint is set', () => { + const testSuccessLoginHint = (field) => { + const account = { + id: 9, + email: 'some@email.com', + username: 'thatUsername' + }; + + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + accounts: { + available: [ + account + ], + active: { + id: 100 + } + }, + auth: { + oauth: { + clientId: 'ely.by', + loginHint: account[field], + prompt: [] + } + } + }); + + expectRun(mock, 'setAccountSwitcher', false); + expectRun(mock, 'authenticate', account) + .returns(Promise.resolve()); + expectState(mock, CompleteState); + + return expect(state.enter(context), 'to be fulfilled'); + }; + + it('should authenticate account if id matches', () => + testSuccessLoginHint('id') + ); + + it('should authenticate account if email matches', () => + testSuccessLoginHint('email') + ); + + it('should authenticate account if username matches', () => + testSuccessLoginHint('username') + ); + + it('should not authenticate if account is already authenticated', () => { + const account = { + id: 9, + email: 'some@email.com', + username: 'thatUsername' + }; + + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + accounts: { + available: [ + account + ], + active: account + }, + auth: { + oauth: { + clientId: 'ely.by', + loginHint: account.id, + prompt: [] + } + } + }); + + expectRun(mock, 'setAccountSwitcher', false); + expectRun(mock, 'oAuthComplete', {}) + .returns({then: () => Promise.resolve()}); + + return expect(state.enter(context), 'to be fulfilled'); + }); + + it('should not authenticate if account was not found and continue auth', () => { + const account = { + id: 9, + email: 'some@email.com', + username: 'thatUsername' + }; + + context.getState.returns({ + user: { + isActive: true, + isGuest: false + }, + accounts: { + available: [{id: 1}], + active: {id: 1} + }, + auth: { + oauth: { + clientId: 'ely.by', + loginHint: account.id, + prompt: [] + } + } + }); + + expectRun(mock, 'oAuthComplete', {}) + .returns({then: () => Promise.resolve()}); + + return expect(state.enter(context), 'to be fulfilled'); + }); + }); }); describe('permissions accept', () => { diff --git a/tests/services/authFlow/helpers.js b/tests/services/authFlow/helpers.js index 7c3ffe3..ec5e30c 100644 --- a/tests/services/authFlow/helpers.js +++ b/tests/services/authFlow/helpers.js @@ -2,6 +2,8 @@ * A helpers for testing states in isolation from AuthFlow */ +import sinon from 'sinon'; + export function bootstrap() { const context = { getState: sinon.stub(), @@ -28,9 +30,9 @@ export function expectState(mock, state) { export function expectNavigate(mock, route, options) { if (options) { return mock.expects('navigate').once().withExactArgs(route, sinon.match(options)); - } else { - return mock.expects('navigate').once().withExactArgs(route); } + + return mock.expects('navigate').once().withExactArgs(route); } export function expectRun(mock, ...args) {