Тесты для стейтов AuthFlow

This commit is contained in:
SleepWalker 2016-03-21 08:16:37 +02:00
parent 1e9b48bd9b
commit 21bbba399f
17 changed files with 985 additions and 17 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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) {

View File

@ -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');
}

11
tests/.eslintrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"globals": {
"sinon": true
}
}

4
tests/index.js Normal file
View File

@ -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);

View File

@ -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));
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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;
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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));
});
});
});

View File

@ -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);
}

View File

@ -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