summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2017-10-16 12:45:19 +0200
committerErik Larkö <erik@mullvad.net>2017-10-16 12:45:19 +0200
commit21ef1e1be95518530ef69572fa14f9b09e33475b (patch)
tree71a59da8aedaa09c15d5c9e34e4cdc55c72ba8ed
parentfa39927850f5147545afe42e2560c0ce094092f6 (diff)
parent5c70a74bd6fd94d6c1a71b327d02012e85198aeb (diff)
downloadmullvadvpn-21ef1e1be95518530ef69572fa14f9b09e33475b.tar.xz
mullvadvpn-21ef1e1be95518530ef69572fa14f9b09e33475b.zip
Merge branch 'websocket-auth'
-rw-r--r--app/app.js2
-rw-r--r--app/lib/backend.js295
-rw-r--r--app/lib/ipc-facade.js11
-rw-r--r--app/lib/jsonrpc-ws-ipc.js9
-rw-r--r--app/main.js19
-rw-r--r--test/auth.spec.js56
-rw-r--r--test/connect.spec.js30
-rw-r--r--test/helpers/IpcChain.js19
-rw-r--r--test/helpers/ipc-helpers.js26
-rw-r--r--test/mocks/ipc.js12
10 files changed, 349 insertions, 130 deletions
diff --git a/app/app.js b/app/app.js
index 4886e42527..c0682343c3 100644
--- a/app/app.js
+++ b/app/app.js
@@ -24,7 +24,7 @@ const store = configureStore(initialState, memoryHistory);
//////////////////////////////////////////////////////////////////////////
const backend = new Backend(store);
ipcRenderer.on('backend-info', (_event, args) => {
- backend.setLocation(args.addr);
+ backend.setCredentials(args.credentials);
backend.sync();
backend.autologin()
.catch( e => {
diff --git a/app/lib/backend.js b/app/lib/backend.js
index e159ca51df..b7f3f3e489 100644
--- a/app/lib/backend.js
+++ b/app/lib/backend.js
@@ -1,6 +1,7 @@
// @flow
-import log from 'electron-log';
+//import log from 'electron-log';
+const log = console;
import EventEmitter from 'events';
import { servers } from '../config';
import { IpcFacade, RealIpc } from './ipc-facade';
@@ -9,9 +10,8 @@ import connectionActions from '../redux/connection/actions';
import type { ReduxStore } from '../redux/store';
import { push } from 'react-router-redux';
-import type { BackendState } from './ipc-facade';
+import type { BackendState, RelayEndpoint } from './ipc-facade';
import type { ConnectionState } from '../redux/connection/reducers';
-import type { RelayEndpoint } from './ipc-facade';
export type EventType = 'connect' | 'connecting' | 'disconnect' | 'login' | 'logging' | 'logout' | 'updatedIp' | 'updatedLocation' | 'updatedReachability';
export type ErrorType = 'NO_CREDIT' | 'NO_INTERNET' | 'INVALID_ACCOUNT' | 'NO_ACCOUNT';
@@ -66,31 +66,66 @@ export class BackendError extends Error {
}
+
+export type IpcCredentials = {
+ connectionString: string,
+ sharedSecret: string,
+};
+export function parseIpcCredentials(data: string): ?IpcCredentials {
+ const [connectionString, sharedSecret] = data.split('\n', 2);
+ if(connectionString && sharedSecret) {
+ return {
+ connectionString,
+ sharedSecret,
+ };
+ } else {
+ return null;
+ }
+}
+
+
/**
* Backend implementation
*/
export class Backend {
_ipc: IpcFacade;
+ _credentials: ?IpcCredentials;
+ _authenticationPromise: ?Promise<void>;
_store: ReduxStore;
_eventEmitter = new EventEmitter();
- constructor(store: ReduxStore, ipc: ?IpcFacade) {
+ constructor(store: ReduxStore, credentials?: IpcCredentials, ipc: ?IpcFacade) {
this._store = store;
+ this._credentials = credentials;
+
+
if(ipc) {
this._ipc = ipc;
+
+ // force to re-authenticate when connection closed
+ this._ipc.setCloseConnectionHandler(() => {
+ this._authenticationPromise = null;
+ });
+
this._registerIpcListeners();
this._startReachability();
}
}
- setLocation(loc: string) {
- log.info('Got connection info to backend', loc);
+ setCredentials(credentials: IpcCredentials) {
+ log.info('Got connection info to backend', credentials.connectionString);
+ this._credentials = credentials;
if (this._ipc) {
- this._ipc.setConnectionString(loc);
+ this._credentials = credentials;
} else {
- this._ipc = new RealIpc(loc);
+ this._ipc = new RealIpc(credentials.connectionString);
+
+ // force to re-authenticate when connection closed
+ this._ipc.setCloseConnectionHandler(() => {
+ this._authenticationPromise = null;
+ });
}
this._registerIpcListeners();
}
@@ -98,27 +133,33 @@ export class Backend {
sync() {
log.info('Syncing with the backend...');
- this._ipc.getIp()
- .then( ip => {
- log.info('Got ip', ip);
- this._store.dispatch(connectionActions.newPublicIp(ip));
- })
- .catch(e => {
- log.info('Failed syncing with the backend,', e.message);
+ this._ensureAuthenticated()
+ .then( () => {
+ this._ipc.getIp()
+ .then( ip => {
+ log.info('Got ip', ip);
+ this._store.dispatch(connectionActions.newPublicIp(ip));
+ })
+ .catch(e => {
+ log.info('Failed syncing with the backend,', e.message);
+ });
});
- this._ipc.getLocation()
- .then( location => {
- log.info('Got location', location);
- const newLocation = {
- location: location.latlong,
- country: location.country,
- city: location.city
- };
- this._store.dispatch(connectionActions.newLocation(newLocation));
- })
- .catch(e => {
- log.info('Failed getting new location,', e.message);
+ this._ensureAuthenticated()
+ .then( () => {
+ this._ipc.getLocation()
+ .then( location => {
+ log.info('Got location', location);
+ const newLocation = {
+ location: location.latlong,
+ country: location.country,
+ city: location.city
+ };
+ this._store.dispatch(connectionActions.newLocation(newLocation));
+ })
+ .catch(e => {
+ log.info('Failed getting new location,', e.message);
+ });
});
}
@@ -131,30 +172,33 @@ export class Backend {
this._store.dispatch(accountActions.startLogin(accountToken));
- return this._ipc.getAccountData(accountToken)
- .then( response => {
- log.info('Account exists', response);
+ return this._ensureAuthenticated()
+ .then( () => {
+ return this._ipc.getAccountData(accountToken)
+ .then( response => {
+ log.info('Account exists', response);
- return this._ipc.setAccount(accountToken)
- .then( () => response );
+ return this._ipc.setAccount(accountToken)
+ .then( () => response );
- }).then( accountData => {
- log.info('Log in complete');
+ }).then( accountData => {
+ log.info('Log in complete');
- this._store.dispatch(accountActions.loginSuccessful(accountData.expiry));
+ this._store.dispatch(accountActions.loginSuccessful(accountData.expiry));
- // Redirect the user after some time to allow for
- // the 'Login Successful' screen to be visible
- setTimeout(() => {
- this._store.dispatch(push('/connect'));
- this.connect();
- }, 1000);
- }).catch(e => {
- log.error('Failed to log in,', e.message);
+ // Redirect the user after some time to allow for
+ // the 'Login Successful' screen to be visible
+ setTimeout(() => {
+ this._store.dispatch(push('/connect'));
+ this.connect();
+ }, 1000);
+ }).catch(e => {
+ log.error('Failed to log in,', e.message);
- // TODO: This is not true. If there is a communication link failure the promise will be rejected too
- const err = new BackendError('INVALID_ACCOUNT');
- this._store.dispatch(accountActions.loginFailed(err));
+ // TODO: This is not true. If there is a communication link failure the promise will be rejected too
+ const err = new BackendError('INVALID_ACCOUNT');
+ this._store.dispatch(accountActions.loginFailed(err));
+ });
});
}
@@ -163,49 +207,55 @@ export class Backend {
this._store.dispatch(accountActions.startLogin());
- return this._ipc.getAccount()
- .then( accountToken => {
- if (!accountToken) {
- throw new BackendError('NO_ACCOUNT');
- }
- log.debug('The backend had an account number stored:', accountToken);
- this._store.dispatch(accountActions.startLogin(accountToken));
+ return this._ensureAuthenticated()
+ .then( () => {
+ return this._ipc.getAccount()
+ .then( accountToken => {
+ if (!accountToken) {
+ throw new BackendError('NO_ACCOUNT');
+ }
+ log.debug('The backend had an account number stored:', accountToken);
+ this._store.dispatch(accountActions.startLogin(accountToken));
- return this._ipc.getAccountData(accountToken);
- })
- .then( accountData => {
- log.info('The stored account number still exists', accountData);
+ return this._ipc.getAccountData(accountToken);
+ })
+ .then( accountData => {
+ log.info('The stored account number still exists', accountData);
- this._store.dispatch(accountActions.loginSuccessful(accountData.expiry));
+ this._store.dispatch(accountActions.loginSuccessful(accountData.expiry));
- this._store.dispatch(push('/connect'));
- this.connect();
- })
- .catch( e => {
- log.warn('Unable to autologin,', e.message);
+ this._store.dispatch(push('/connect'));
+ this.connect();
+ })
+ .catch( e => {
+ log.warn('Unable to autologin,', e.message);
- this._store.dispatch(accountActions.autoLoginFailed());
- this._store.dispatch(push('/'));
+ this._store.dispatch(accountActions.autoLoginFailed());
+ this._store.dispatch(push('/'));
- throw e;
+ throw e;
+ });
});
}
logout() {
// @TODO: What does it mean for a logout to be successful or failed?
- this._ipc.setAccount(null)
- .then(() => {
+ return this._ensureAuthenticated()
+ .then( () => {
+ return this._ipc.setAccount(null)
+ .then(() => {
- this._store.dispatch(accountActions.loggedOut());
+ this._store.dispatch(accountActions.loggedOut());
- // disconnect user during logout
- return this.disconnect()
- .then( () => {
- this._store.dispatch(push('/'));
+ // disconnect user during logout
+ return this.disconnect()
+ .then( () => {
+ this._store.dispatch(push('/'));
+ });
+ })
+ .catch(e => {
+ log.info('Failed to logout,', e.message);
});
- })
- .catch(e => {
- log.info('Failed to logout,', e.message);
});
}
@@ -215,28 +265,37 @@ export class Backend {
if (relayEndpoint) {
this._store.dispatch(connectionActions.connectingTo(relayEndpoint));
- return this._ipc.setCustomRelay(relayEndpoint)
+ return this._ensureAuthenticated()
.then( () => {
- return this._ipc.connect();
- })
- .catch(e => {
- log.info('Failed connecting to', relayEndpoint.host, '-', e.message);
- this._store.dispatch(connectionActions.disconnected());
+ return this._ipc.setCustomRelay(relayEndpoint)
+ .then( () => {
+ return this._ipc.connect();
+ })
+ .catch(e => {
+ log.info('Failed connecting to', relayEndpoint.host, '-', e.message);
+ this._store.dispatch(connectionActions.disconnected());
+ });
});
} else {
- return this._ipc.connect()
- .catch(e => {
- log.info('Failed connecting to the relay set in the backend, ', e.message);
- this._store.dispatch(connectionActions.disconnected());
+ return this._ensureAuthenticated()
+ .then( () => {
+ return this._ipc.connect()
+ .catch(e => {
+ log.info('Failed connecting to the relay set in the backend, ', e.message);
+ this._store.dispatch(connectionActions.disconnected());
+ });
});
}
}
disconnect(): Promise<void> {
// @TODO: Failure modes
- return this._ipc.disconnect()
- .catch(e => {
- log.info('Failed to disconnect,', e.message);
+ return this._ensureAuthenticated()
+ .then( () => {
+ return this._ipc.disconnect()
+ .catch(e => {
+ log.info('Failed to disconnect,', e.message);
+ });
});
}
@@ -266,24 +325,27 @@ export class Backend {
}
_registerIpcListeners() {
- this._ipc.registerStateListener(newState => {
- log.info('Got new state from backend', newState);
+ return this._ensureAuthenticated()
+ .then( () => {
+ return this._ipc.registerStateListener(newState => {
+ log.info('Got new state from backend', newState);
- const newStatus = this._securityStateToConnectionState(newState);
- switch(newStatus) {
- case 'connecting':
- this._store.dispatch(connectionActions.connecting());
- break;
- case 'connected':
- this._store.dispatch(connectionActions.connected());
- break;
- case 'disconnected':
- this._store.dispatch(connectionActions.disconnected());
- break;
- }
+ const newStatus = this._securityStateToConnectionState(newState);
+ switch(newStatus) {
+ case 'connecting':
+ this._store.dispatch(connectionActions.connecting());
+ break;
+ case 'connected':
+ this._store.dispatch(connectionActions.connected());
+ break;
+ case 'disconnected':
+ this._store.dispatch(connectionActions.disconnected());
+ break;
+ }
- this.sync();
- });
+ this.sync();
+ });
+ });
}
_securityStateToConnectionState(backendState: BackendState): ConnectionState {
@@ -296,4 +358,27 @@ export class Backend {
}
throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState));
}
+
+ _ensureAuthenticated(): Promise<void> {
+ const credentials = this._credentials;
+ if(credentials) {
+ if(!this._authenticationPromise) {
+ this._authenticationPromise = this._authenticate(credentials.sharedSecret);
+ }
+ return this._authenticationPromise;
+ } else {
+ return Promise.reject(new Error('Missing authentication credentials.'));
+ }
+ }
+
+ _authenticate(sharedSecret: string): Promise<void> {
+ return this._ipc.authenticate(sharedSecret)
+ .then(() => {
+ log.info('Authenticated with backend');
+ })
+ .catch((e) => {
+ log.error('Failed to authenticate with backend: ', e.message);
+ throw e;
+ });
+ }
}
diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js
index 29c369dd9b..4039d44ee9 100644
--- a/app/lib/ipc-facade.js
+++ b/app/lib/ipc-facade.js
@@ -44,6 +44,8 @@ export interface IpcFacade {
getLocation(): Promise<Location>,
getState(): Promise<BackendState>,
registerStateListener((BackendState) => void): void,
+ setCloseConnectionHandler(() => void): void,
+ authenticate(sharedSecret: string): Promise<void>,
}
export class RealIpc implements IpcFacade {
@@ -163,4 +165,13 @@ export class RealIpc implements IpcFacade {
listener(parsedEvent);
});
}
+
+ authenticate(sharedSecret: string): Promise<void> {
+ return this._ipc.send('auth', sharedSecret)
+ .then(this._ignoreResponse);
+ }
+
+ setCloseConnectionHandler(handler: () => void) {
+ console.log('appa', handler);
+ }
}
diff --git a/app/lib/jsonrpc-ws-ipc.js b/app/lib/jsonrpc-ws-ipc.js
index 2609d1514c..deefce3d66 100644
--- a/app/lib/jsonrpc-ws-ipc.js
+++ b/app/lib/jsonrpc-ws-ipc.js
@@ -75,6 +75,7 @@ export default class Ipc {
_websocket: WebSocket;
_backoff: ReconnectionBackoff;
_websocketFactory: (string) => WebSocket;
+ _closeConnectionHandler: ?() => void;
constructor(connectionString: string, websocketFactory: ?(string)=>WebSocket) {
this._connectionString = connectionString;
@@ -91,6 +92,10 @@ export default class Ipc {
this._connectionString = str;
}
+ setCloseConnectionHandler(handler: ?() => void) {
+ this._closeConnectionHandler = handler;
+ }
+
on(event: string, listener: (mixed) => void): Promise<*> {
log.info('Adding a listener to', event);
@@ -246,6 +251,10 @@ export default class Ipc {
};
this._websocket.onclose = () => {
+ if(this._closeConnectionHandler) {
+ this._closeConnectionHandler();
+ }
+
const delay = this._backoff.getIncreasedBackoff();
log.warn('The websocket connetion closed, attempting to reconnect it in', delay, 'milliseconds');
setTimeout(() => this._reconnect(), delay);
diff --git a/app/main.js b/app/main.js
index 5d24239791..7dcb83f9c6 100644
--- a/app/main.js
+++ b/app/main.js
@@ -7,6 +7,7 @@ import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron';
import TrayIconManager from './lib/tray-icon-manager';
import ElectronSudo from 'electron-sudo';
import { version } from '../package.json';
+import { parseIpcCredentials } from './lib/backend';
import type { TrayIconType } from './lib/tray-icon-manager';
@@ -88,6 +89,7 @@ const appDelegate = {
browserWindowReady = true;
appDelegate._pollForConnectionInfoFile();
});
+
ipcMain.on('show-window', () => {
appDelegate._showWindow(window, appDelegate._tray);
});
@@ -101,6 +103,8 @@ const appDelegate = {
// create tray icon on macOS
if(isMacOS) {
appDelegate._tray = appDelegate._createTray(window);
+ } else {
+ appDelegate._showWindow(window, null);
}
appDelegate._setAppMenu();
@@ -227,10 +231,13 @@ const appDelegate = {
return log.error('Could not find backend connection info', err);
}
- log.info('Read IPC connection info', data);
- window.webContents.send('backend-info', {
- addr: data,
- });
+ const credentials = parseIpcCredentials(data);
+ if(credentials) {
+ log.info('Read IPC connection info', credentials.connectionString);
+ window.webContents.send('backend-info', { credentials });
+ } else {
+ log.error('Could not parse IPC credentials.');
+ }
});
},
@@ -255,6 +262,7 @@ const appDelegate = {
resizable: false,
maximizable: false,
fullscreenable: false,
+ show: false,
webPreferences: {
// prevents renderer process code from not running when window is hidden
backgroundThrottling: false,
@@ -268,8 +276,7 @@ const appDelegate = {
options = Object.assign({}, options, {
height: contentHeight + 12, // 12 is the size of transparent area around arrow
frame: false,
- transparent: true,
- show: false
+ transparent: true
});
}
diff --git a/test/auth.spec.js b/test/auth.spec.js
new file mode 100644
index 0000000000..c3fd78c83d
--- /dev/null
+++ b/test/auth.spec.js
@@ -0,0 +1,56 @@
+// @flow
+
+import { expect } from 'chai';
+import { setupIpcAndStore, setupBackendAndStore, failFast, checkNextTick } from './helpers/ipc-helpers';
+import { IpcChain } from './helpers/IpcChain';
+import { Backend } from '../app/lib/backend';
+
+describe('authentication', () => {
+
+ it('authenticates before ipc call if unauthenticated', (done) => {
+ const { store, mockIpc } = setupIpcAndStore();
+ const credentials = {
+ sharedSecret: 'foo',
+ connectionString: '',
+ };
+
+
+ const chain = new IpcChain(mockIpc);
+ chain.require('authenticate')
+ .withInputValidation( secret => {
+ expect(secret).to.equal(credentials.sharedSecret);
+ })
+ .done();
+
+ chain.require('connect')
+ .done();
+
+ chain.onSuccessOrFailure(done);
+
+
+ const backend = new Backend(store, credentials, mockIpc);
+ backend.connect();
+ });
+
+ it('reauthenticates on reconnect', (done) => {
+ const { mockIpc, backend } = setupBackendAndStore();
+
+ let authCount = 0;
+ mockIpc.authenticate = () => {
+ authCount++;
+ return Promise.resolve();
+ };
+
+
+ mockIpc.killWebSocket();
+ failFast(() => {
+ expect(authCount).to.equal(0);
+ }, done);
+
+
+ backend.connect();
+ checkNextTick(() => {
+ expect(authCount).to.equal(1);
+ }, done);
+ });
+});
diff --git a/test/connect.spec.js b/test/connect.spec.js
index bb6ac41526..07e3091c6e 100644
--- a/test/connect.spec.js
+++ b/test/connect.spec.js
@@ -61,29 +61,35 @@ describe('connect', () => {
});
});
- it('should correctly deduce \'connected\' from backend states', () => {
+ it('should correctly deduce \'connected\' from backend states', (done) => {
const { store, mockIpc } = setupBackendAndStore();
- expect(store.getState().connection.status).not.to.equal('connected');
- mockIpc.sendNewState({ state: 'secured', target_state: 'secured' });
- expect(store.getState().connection.status).to.equal('connected');
+ checkNextTick( () => {
+ expect(store.getState().connection.status).not.to.equal('connected');
+ mockIpc.sendNewState({ state: 'secured', target_state: 'secured' });
+ expect(store.getState().connection.status).to.equal('connected');
+ }, done);
});
- it('should correctly deduce \'connecting\' from backend states', () => {
+ it('should correctly deduce \'connecting\' from backend states', (done) => {
const { store, mockIpc } = setupBackendAndStore();
- expect(store.getState().connection.status).not.to.equal('connecting');
- mockIpc.sendNewState({ state: 'unsecured', target_state: 'secured' });
- expect(store.getState().connection.status).to.equal('connecting');
+ checkNextTick( () => {
+ expect(store.getState().connection.status).not.to.equal('connecting');
+ mockIpc.sendNewState({ state: 'unsecured', target_state: 'secured' });
+ expect(store.getState().connection.status).to.equal('connecting');
+ }, done);
});
- it('should correctly deduce \'disconnected\' from backend states', () => {
+ it('should correctly deduce \'disconnected\' from backend states', (done) => {
const { store, mockIpc } = setupBackendAndStore();
store.dispatch(connectionActions.connected());
- expect(store.getState().connection.status).not.to.equal('disconnected');
- mockIpc.sendNewState({ state: 'unsecured', target_state: 'unsecured' });
- expect(store.getState().connection.status).to.equal('disconnected');
+ checkNextTick( () => {
+ expect(store.getState().connection.status).not.to.equal('disconnected');
+ mockIpc.sendNewState({ state: 'unsecured', target_state: 'unsecured' });
+ expect(store.getState().connection.status).to.equal('disconnected');
+ }, done);
});
});
diff --git a/test/helpers/IpcChain.js b/test/helpers/IpcChain.js
index a7916e1e9f..a62626a6a6 100644
--- a/test/helpers/IpcChain.js
+++ b/test/helpers/IpcChain.js
@@ -8,11 +8,13 @@ export class IpcChain {
_recordedCalls: Array<string>;
_mockIpc: {};
_done: (*) => void;
+ _aborted: boolean;
constructor(mockIpc: {}) {
this._expectedCalls = [];
this._recordedCalls = [];
this._mockIpc = mockIpc;
+ this._aborted = false;
}
require(ipcCall: string): StepBuilder {
@@ -28,10 +30,21 @@ export class IpcChain {
}
_stepPromiseCallback(step, resolve, args) {
+ if (this._aborted) {
+ return;
+ }
+
this._registerCall(step.ipcCall);
if (step.inputValidation) {
- failFast(() => step.inputValidation(...args), this._done);
+ const failedInputValidation = failFast(() => {
+ step.inputValidation(...args);
+ }, this._done);
+
+ if (failedInputValidation) {
+ this._abort();
+ return;
+ }
}
if (this._isLastCall()) {
@@ -41,6 +54,10 @@ export class IpcChain {
resolve(step.returnValue);
}
+ _abort() {
+ this._aborted = true;
+ }
+
_isLastCall(): boolean {
return this._recordedCalls.length === this._expectedCalls.length;
}
diff --git a/test/helpers/ipc-helpers.js b/test/helpers/ipc-helpers.js
index 5e4b2941d6..e2a578973b 100644
--- a/test/helpers/ipc-helpers.js
+++ b/test/helpers/ipc-helpers.js
@@ -9,14 +9,24 @@ import { mockState, mockStore } from '../mocks/redux';
type DoneCallback = (?mixed) => void;
type Check = () => void;
-export function setupBackendAndStore() {
-
+export function setupIpcAndStore() {
const memoryHistory = createMemoryHistory();
const store = configureStore(null, memoryHistory);
const mockIpc = newMockIpc();
- const backend = new Backend(store, mockIpc);
+ return { store, mockIpc };
+}
+
+export function setupBackendAndStore() {
+
+ const { store, mockIpc } = setupIpcAndStore();
+
+ const credentials = {
+ sharedSecret: '',
+ connectionString: '',
+ };
+ const backend = new Backend(store, credentials, mockIpc);
return { store, mockIpc, backend };
}
@@ -24,7 +34,11 @@ export function setupBackendAndStore() {
export function setupBackendAndMockStore() {
const store = mockStore(mockState());
const mockIpc = newMockIpc();
- const backend = new Backend(store, mockIpc);
+ const credentials = {
+ sharedSecret: '',
+ connectionString: '',
+ };
+ const backend = new Backend(store, credentials, mockIpc);
return { store, mockIpc, backend };
}
@@ -54,11 +68,13 @@ export function checkNextTick(fn: Check, done: DoneCallback) {
// In async tests where we want to test a chain of IPC messages
// we can only invoke `done` for the last message. This function
// is for the intermediate messages.
-export function failFast(fn: Check, done: DoneCallback) {
+export function failFast(fn: Check, done: DoneCallback): boolean {
try {
fn();
+ return false;
} catch(e) {
done(e);
+ return true;
}
}
export function failFastNextTick(fn: Check, done: DoneCallback) {
diff --git a/test/mocks/ipc.js b/test/mocks/ipc.js
index 472811ef4c..5b337ebbc9 100644
--- a/test/mocks/ipc.js
+++ b/test/mocks/ipc.js
@@ -3,14 +3,17 @@ import type { IpcFacade, BackendState } from '../../app/lib/ipc-facade';
interface MockIpc {
sendNewState: (BackendState) => void;
+ killWebSocket: () => void;
-getAccountData: *;
-connect: *;
-getAccount: *;
+ -authenticate: *;
}
export function newMockIpc() {
const stateListeners = [];
+ const connectionCloseListeners = [];
const mockIpc: IpcFacade & MockIpc = {
@@ -60,6 +63,15 @@ export function newMockIpc() {
l(state);
}
},
+ authenticate: (_secret: string) => Promise.resolve(),
+ setCloseConnectionHandler: (listener: () => void) => {
+ connectionCloseListeners.push(listener);
+ },
+ killWebSocket: () => {
+ for(const l of connectionCloseListeners) {
+ l();
+ }
+ }
};
return mockIpc;