diff --git a/src/index.js b/src/index.js index 2ef133f..253c677 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,12 @@ const store = applyMiddleware( thunk )(createStore)(reducer); +if (process.env.NODE_ENV !== 'production') { + // some shortcuts for testing on localhost + + window.testOAuth = () => location.href = '/oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session'; +} + ReactDOM.render( diff --git a/src/services/authFlow.js b/src/services/authFlow.js index 1e4226a..f23d115 100644 --- a/src/services/authFlow.js +++ b/src/services/authFlow.js @@ -1,5 +1,14 @@ import AuthFlow from './authFlow/AuthFlow'; -// TODO: a way to unload service (when we are on account page) +import * as actions from 'components/auth/actions'; +import {updateUser} from 'components/user/actions'; -export default new AuthFlow(); +const availableActions = { + ...actions, + updateUser, + redirect(url) { + location.href = url; + } +}; + +export default new AuthFlow(availableActions); diff --git a/src/services/authFlow/AuthFlow.js b/src/services/authFlow/AuthFlow.js index abc9c55..b889372 100644 --- a/src/services/authFlow/AuthFlow.js +++ b/src/services/authFlow/AuthFlow.js @@ -1,22 +1,25 @@ import { routeActions } from 'react-router-redux'; -import * as actions from 'components/auth/actions'; -import {updateUser} from 'components/user/actions'; - import RegisterState from './RegisterState'; import LoginState from './LoginState'; import OAuthState from './OAuthState'; import ForgotPasswordState from './ForgotPasswordState'; -const availableActions = { - ...actions, - updateUser, - redirect(url) { - location.href = url; - } -}; +// TODO: a way to unload service (when we are on account page) export default class AuthFlow { + constructor(actions) { + if (typeof actions !== 'object') { + throw new Error('AuthFlow requires an actions object'); + } + + this.actions = actions; + + if (Object.freeze) { + Object.freeze(this.actions); + } + } + setStore(store) { this.navigate = (route) => { const {routing} = this.getState(); @@ -48,11 +51,11 @@ export default class AuthFlow { } run(actionId, payload) { - if (!availableActions[actionId]) { + if (!this.actions[actionId]) { throw new Error(`Action ${actionId} does not exists`); } - return this.dispatch(availableActions[actionId](payload)); + return this.dispatch(this.actions[actionId](payload)); } setState(state) { @@ -110,8 +113,4 @@ export default class AuthFlow { throw new Error(`Unsupported request: ${path}`); } } - - login() { - this.setState(new LoginState()); - } } diff --git a/tests/services/authFlow/AuthFlow.test.js b/tests/services/authFlow/AuthFlow.test.js new file mode 100644 index 0000000..768a94b --- /dev/null +++ b/tests/services/authFlow/AuthFlow.test.js @@ -0,0 +1,143 @@ +import AuthFlow from 'services/authFlow/AuthFlow'; +import AbstractState from 'services/authFlow/AbstractState'; + +// TODO: navigate and state switching + +describe('AuthFlow', () => { + let flow; + let actions; + + beforeEach(() => { + actions = {test: sinon.stub()}; + actions.test.returns('passed'); + + flow = new AuthFlow(actions); + }); + + it('throws when no actions provided', () => { + expect(() => new AuthFlow()).to.throw('AuthFlow requires an actions object'); + }); + + it('should not allow to mutate actions', () => { + expect(() => flow.actions.foo = 'bar').to.throw(/readonly/); + expect(() => flow.actions.test = 'hacked').to.throw(/readonly/); + }); + + describe('#setState', () => { + it('should change state', () => { + const state = new AbstractState(); + flow.setState(state); + + expect(flow.state).to.be.equal(state); + }); + + it('should call `enter` on new state and pass reference to itself', () => { + const state = new AbstractState(); + const spy = sinon.spy(state, 'enter'); + + flow.setState(state); + + sinon.assert.calledWith(spy, flow); + sinon.assert.calledOnce(spy); + }); + + it('should call `leave` on previous state if any', () => { + const state1 = new AbstractState(); + const state2 = new AbstractState(); + const spy1 = sinon.spy(state1, 'leave'); + const spy2 = sinon.spy(state2, 'leave'); + + flow.setState(state1); + flow.setState(state2); + + sinon.assert.calledWith(spy1, flow); + sinon.assert.calledOnce(spy1); + sinon.assert.notCalled(spy2); + }); + + it('should throw if no state', () => { + expect(() => flow.setState()).to.throw('State is required'); + }); + }); + + describe('#run', () => { + let store; + + beforeEach(() => { + store = { + getState() {}, + dispatch: sinon.stub() + }; + + flow.setStore(store); + }); + + it('should dispatch an action', () => { + flow.run('test'); + + sinon.assert.calledOnce(store.dispatch); + sinon.assert.calledWith(store.dispatch, 'passed'); + }); + + it('should dispatch an action with payload given', () => { + flow.run('test', 'arg'); + + sinon.assert.calledOnce(actions.test); + sinon.assert.calledWith(actions.test, 'arg'); + }); + + it('should return action dispatch result', () => { + const expected = 'dispatch called'; + store.dispatch.returns(expected); + + expect(flow.run('test')).to.be.equal(expected); + }); + + it('throws when running unexisted action', () => { + expect(() => flow.run('123')).to.throw('Action 123 does not exists'); + }); + }); + + describe('#goBack', () => { + it('should call goBack on state passing itself as argument', () => { + const state = new AbstractState(); + sinon.stub(state, 'goBack'); + flow.setState(state); + + flow.goBack(); + + sinon.assert.calledOnce(state.goBack); + sinon.assert.calledWith(state.goBack, flow); + }); + }); + + describe('#resolve', () => { + it('should call resolve on state passing itself and payload as arguments', () => { + const state = new AbstractState(); + sinon.stub(state, 'resolve'); + flow.setState(state); + + const expectedPayload = {foo: 'bar'}; + + flow.resolve(expectedPayload); + + sinon.assert.calledOnce(state.resolve); + sinon.assert.calledWithExactly(state.resolve, flow, expectedPayload); + }); + }); + + describe('#reject', () => { + it('should call reject on state passing itself and payload as arguments', () => { + const state = new AbstractState(); + sinon.stub(state, 'reject'); + flow.setState(state); + + const expectedPayload = {foo: 'bar'}; + + flow.reject(expectedPayload); + + sinon.assert.calledOnce(state.reject); + sinon.assert.calledWithExactly(state.reject, flow, expectedPayload); + }); + }); +});