summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--app/app.js102
-rw-r--r--app/lib/backend-redux-actions.js79
-rw-r--r--app/lib/backend.js13
-rw-r--r--test/actions.spec.js74
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));
});
});