diff options
| -rw-r--r-- | app/app.js | 102 | ||||
| -rw-r--r-- | app/lib/backend-redux-actions.js | 79 | ||||
| -rw-r--r-- | app/lib/backend.js | 13 | ||||
| -rw-r--r-- | test/actions.spec.js | 74 |
4 files changed, 192 insertions, 76 deletions
diff --git a/app/app.js b/app/app.js index 1438984cf7..9c600a3840 100644 --- a/app/app.js +++ b/app/app.js @@ -2,13 +2,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { Router, createMemoryHistory } from 'react-router'; -import { syncHistoryWithStore, replace } from 'react-router-redux'; +import { syncHistoryWithStore } from 'react-router-redux'; import { webFrame, ipcRenderer } from 'electron'; import makeRoutes from './routes'; import configureStore from './store'; import userActions from './actions/user'; import connectActions from './actions/connect'; import Backend from './lib/backend'; +import mapBackendEventsToReduxActions from './lib/backend-redux'; import { LoginState, ConnectionState } from './constants'; const initialState = {}; @@ -58,80 +59,43 @@ const updateTrayIcon = () => { ipcRenderer.send('changeTrayIcon', iconName); }; -updateTrayIcon(); - // Create backend const backend = new Backend(); -// Setup events - -backend.on(Backend.EventType.updatedIp, (clientIp) => { - store.dispatch(connectActions.connectionChange({ clientIp })); -}); - -backend.on(Backend.EventType.connecting, (serverAddress) => { - store.dispatch(connectActions.connectionChange({ - status: ConnectionState.connecting, - error: null, - serverAddress - })); -}); - -backend.on(Backend.EventType.connect, (serverAddress, error) => { - const status = error ? ConnectionState.disconnected : ConnectionState.connected; - store.dispatch(connectActions.connectionChange({ error, status })); - - updateTrayIcon(); -}); - -backend.on(Backend.EventType.disconnect, () => { - store.dispatch(connectActions.connectionChange({ - status: ConnectionState.disconnected, - serverAddress: null, - error: null - })); - - updateTrayIcon(); -}); - -backend.on(Backend.EventType.logging, (account) => { - store.dispatch(userActions.loginChange({ - status: LoginState.connecting, - error: null, - account - })); -}); - -backend.on(Backend.EventType.login, (account, error) => { - const status = error ? LoginState.failed : LoginState.ok; - store.dispatch(userActions.loginChange({ status, error })); - - // redirect to main screen after delay - if(status === LoginState.ok) { - const preferredServer = store.getState().settings.preferredServer; - const server = backend.serverInfo(preferredServer); +/** + * Patch backend state. + * + * Currently backend does not have external state + * such as VPN connection status or IP address. + * + * So far we store everything in redux and have to + * sync redux state with backend. + * + * In future this will be the other way around. + */ +const syncBackendWithReduxStore = (backend, store) => { + const mapConnStatus = (s) => { + const S = ConnectionState; + const BS = Backend.ConnectionState; + switch(s) { + case S.connected: return BS.connected; + case S.connecting: return BS.connecting; + default: return BS.disconnected; + } + }; + backend._connStatus = mapConnStatus(store.getState().connect.status); +}; +syncBackendWithReduxStore(backend, store); - // auto-connect - setTimeout(() => { - backend.connect(server.address); - store.dispatch(replace('/connect')); - }, 1000); - } -}); +// Setup primary event handlers to translate backend events into redux dispatch +mapBackendEventsToReduxActions(backend, store); -backend.on(Backend.EventType.logout, () => { - store.dispatch(userActions.loginChange({ - status: LoginState.none, - account: null, - error: null - })); +// Setup events to update tray icon +backend.on(Backend.EventType.connect, updateTrayIcon); +backend.on(Backend.EventType.disconnect, updateTrayIcon); - // return to login screen - store.dispatch(replace('/')); - - // disconnect when user logged out - backend.disconnect(); -}); +// force update tray +updateTrayIcon(); // helper method for router to pass backend down the component tree const createElement = (Component, props) => { diff --git a/app/lib/backend-redux-actions.js b/app/lib/backend-redux-actions.js new file mode 100644 index 0000000000..09811e19cf --- /dev/null +++ b/app/lib/backend-redux-actions.js @@ -0,0 +1,79 @@ +import { replace } from 'react-router-redux'; +import userActions from '../actions/user'; +import connectActions from '../actions/connect'; +import Backend from './backend'; +import { LoginState, ConnectionState } from '../constants'; + +export default function mapBackendEventsToReduxActions(backend, store) { + const onUpdateIp = (clientIp) => { + store.dispatch(connectActions.connectionChange({ clientIp })); + }; + + const onConnecting = (serverAddress) => { + store.dispatch(connectActions.connectionChange({ + status: ConnectionState.connecting, + error: null, + serverAddress + })); + }; + + const onConnect = (serverAddress, error) => { + const status = error ? ConnectionState.disconnected : ConnectionState.connected; + store.dispatch(connectActions.connectionChange({ error, status })); + }; + + const onDisconnect = () => { + store.dispatch(connectActions.connectionChange({ + status: ConnectionState.disconnected, + serverAddress: null, + error: null + })); + }; + + const onLoggingIn = (account) => { + store.dispatch(userActions.loginChange({ + status: LoginState.connecting, + error: null, + account + })); + }; + + const onLogin = (account, error) => { + const status = error ? LoginState.failed : LoginState.ok; + store.dispatch(userActions.loginChange({ status, error })); + + // redirect to main screen after delay + if(status === LoginState.ok) { + const preferredServer = store.getState().settings.preferredServer; + const server = backend.serverInfo(preferredServer); + + // auto-connect + setTimeout(() => { + backend.connect(server.address); + store.dispatch(replace('/connect')); + }, 1000); + } + }; + + const onLogout = () => { + store.dispatch(userActions.loginChange({ + status: LoginState.none, + account: null, + error: null + })); + + // return to login screen + store.dispatch(replace('/')); + + // disconnect when user logged out + backend.disconnect(); + }; + + backend.on(Backend.EventType.updatedIp, onUpdateIp); + backend.on(Backend.EventType.connecting, onConnecting); + backend.on(Backend.EventType.connect, onConnect); + backend.on(Backend.EventType.disconnect, onDisconnect); + backend.on(Backend.EventType.logging, onLoggingIn); + backend.on(Backend.EventType.login, onLogin); + backend.on(Backend.EventType.logout, onLogout); +}; diff --git a/app/lib/backend.js b/app/lib/backend.js index 682398a37d..b0f851f9bb 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; import { servers } from '../constants'; const EventType = Enum('connect', 'connecting', 'disconnect', 'login', 'logging', 'logout', 'updatedIp'); +const ConnectionState = Enum('disconnected', 'connecting', 'connected'); /** * Backend implementation @@ -12,11 +13,13 @@ const EventType = Enum('connect', 'connecting', 'disconnect', 'login', 'logging' export default class Backend extends EventEmitter { static EventType = EventType; + static ConnectionState = ConnectionState; constructor() { super(); this._account = null; this._serverAddress = null; + this._connStatus = ConnectionState.disconnected; this._cancellationHandler = null; // update IP in background @@ -86,7 +89,8 @@ export default class Backend extends EventEmitter { connect(addr) { this.disconnect(); - + + this._connStatus = ConnectionState.connecting; this._serverAddress = addr; // emit: connecting @@ -100,6 +104,9 @@ export default class Backend extends EventEmitter { // if(!/se\d+\.mullvad\.net/.test(addr)) { // err = new Error('Server is unreachable'); // } + + this._connStatus = ConnectionState.connected; + // emit: connect this.emit(EventType.connect, addr, err); this.refreshIp(); @@ -115,10 +122,14 @@ export default class Backend extends EventEmitter { this._timer = null; } this._cancellationHandler = null; + this._connStatus = ConnectionState.disconnected; }; } disconnect() { + if(this._connStatus === ConnectionState.disconnected) { return; } + + this._connStatus = ConnectionState.disconnected; this._serverAddress = null; // cancel ongoing connection attempt diff --git a/test/actions.spec.js b/test/actions.spec.js index ccee5a544f..1ab7049ed4 100644 --- a/test/actions.spec.js +++ b/test/actions.spec.js @@ -3,23 +3,85 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import Backend from '../app/lib/backend'; import userActions from '../app/actions/user'; +import mapBackendEventsToReduxActions from '../app/lib/backend-redux-actions'; import { LoginState, ConnectionState, defaultServer } from '../app/constants'; const middlewares = [ thunk ]; const mockStore = configureMockStore(middlewares); +const mockState = () => { + return { + user: { + account: null, + status: LoginState.none, + error: null + }, + connect: { + status: ConnectionState.disconnected, + serverAddress: null, + clientIp: null + }, + settings: { + autoSecure: false, + preferredServer: defaultServer + } + }; +}; -describe('actions', () => { +const filterIpUpdateActions = (actions) => { + return actions.filter((action) => { + return !(action.type === 'CONNECTION_CHANGE' && action.payload.clientIp); + }); +}; + +describe('actions', function() { + this.timeout(5000); + + it('should login', (done) => { + const expectedActions = [ + { type: 'USER_LOGIN_CHANGE', payload: { status: 'connecting', error: null, account: '111123456789' } }, + { type: 'USER_LOGIN_CHANGE', payload: { status: 'ok', error: undefined } } + ]; + + const backend = new Backend(); + const store = mockStore(mockState()); + + mapBackendEventsToReduxActions(backend, store); - it('should create action for USER_LOGIN_CHANGE', () => { + backend.once(Backend.EventType.login, () => { + const storeActions = filterIpUpdateActions(store.getActions()); + + expect(storeActions).deep.equal(expectedActions); + done(); + }); + + store.dispatch(userActions.login(backend, '111123456789')); + }); + + it('should logout', (done) => { const expectedActions = [ - { type: 'USER_LOGIN_CHANGE', payload: {} } + { type: 'USER_LOGIN_CHANGE', payload: { account: null, status: 'none', error: null } } ]; + let state = Object.assign(mockState(), { + user: { + account: '1111234567890', + status: LoginState.ok + } + }); + const backend = new Backend(); - const store = mockStore({}); + const store = mockStore(state); + + mapBackendEventsToReduxActions(backend, store); + + backend.once(Backend.EventType.logout, () => { + const storeActions = filterIpUpdateActions(store.getActions()); + + expect(storeActions).deep.equal(expectedActions); + done(); + }); - // @TODO: Figure out how to test actions in event based system - // store.dispatch(userActions.login(backend, '111123456789')); + store.dispatch(userActions.logout(backend)); }); }); |
