diff options
| -rw-r--r-- | app/lib/backend.js | 291 | ||||
| -rw-r--r-- | app/lib/ipc-facade.js | 161 | ||||
| -rw-r--r-- | app/main.js | 2 | ||||
| -rw-r--r-- | test/auth.spec.js | 56 | ||||
| -rw-r--r-- | test/connect.spec.js | 30 | ||||
| -rw-r--r-- | test/helpers/IpcChain.js | 19 | ||||
| -rw-r--r-- | test/helpers/ipc-helpers.js | 26 | ||||
| -rw-r--r-- | test/mocks/ipc.js | 16 |
8 files changed, 368 insertions, 233 deletions
diff --git a/app/lib/backend.js b/app/lib/backend.js index a8b560812f..2e4cc90cc7 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,7 +10,7 @@ import connectionActions from '../redux/connection/actions'; import type { ReduxStore } from '../redux/store'; import { push } from 'react-router-redux'; -import type { BackendState, RelayEndpoint, IpcCredentials } from './ipc-facade'; +import type { BackendState, RelayEndpoint } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; export type EventType = 'connect' | 'connecting' | 'disconnect' | 'login' | 'logging' | 'logout' | 'updatedIp' | 'updatedLocation' | 'updatedReachability'; @@ -66,31 +67,65 @@ 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(); } } setCredentials(credentials: IpcCredentials) { - log.info('Got connection info to backend', credentials); + log.info('Got connection info to backend', credentials.connectionString); + this._credentials = credentials; if (this._ipc) { - this._ipc.setCredentials(credentials); + this._credentials = credentials; } else { - this._ipc = new RealIpc(credentials); + 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.auth(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 9e191ddf30..2d63f4b9ba 100644 --- a/app/lib/ipc-facade.js +++ b/app/lib/ipc-facade.js @@ -3,7 +3,6 @@ import JsonRpcWs, { InvalidReply } from './jsonrpc-ws-ipc'; import { object, string, arrayOf, number } from 'validated/schema'; import { validate } from 'validated/object'; -import log from 'electron-log'; import type { Coordinate2d } from '../types'; @@ -32,26 +31,9 @@ export type RelayEndpoint = { protocol: 'tcp' | 'udp', }; -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; - } -} - export interface IpcFacade { - setCredentials(IpcCredentials): void, + setConnectionString(string): void, getAccountData(AccountToken): Promise<AccountData>, getAccount(): Promise<?AccountToken>, setAccount(accountToken: ?AccountToken): Promise<void>, @@ -62,27 +44,20 @@ export interface IpcFacade { getLocation(): Promise<Location>, getState(): Promise<BackendState>, registerStateListener((BackendState) => void): void, + setCloseConnectionHandler(() => void): void, + auth(sharedSecret: string): Promise<void>, } export class RealIpc implements IpcFacade { _ipc: JsonRpcWs; - _credentials: ?IpcCredentials; - _authenticationPromise: ?Promise<void>; - constructor(credentials: IpcCredentials) { - this._credentials = credentials; - this._ipc = new JsonRpcWs(credentials.connectionString); - - // force to re-authenticate when connection closed - this._ipc.setCloseConnectionHandler(() => { - this._authenticationPromise = null; - }); + constructor(connectionString: string) { + this._ipc = new JsonRpcWs(connectionString); } - setCredentials(credentials: IpcCredentials) { - this._credentials = credentials; - this._ipc.setConnectionString(credentials.connectionString); + setConnectionString(str: string) { + this._ipc.setConnectionString(str); } getAccountData(accountToken: AccountToken): Promise<AccountData> { @@ -100,23 +75,19 @@ export class RealIpc implements IpcFacade { } getAccount(): Promise<?AccountToken> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_account') - .then( raw => { - if (raw === undefined || raw === null || typeof raw === 'string') { - return raw; - } else { - throw new InvalidReply(raw); - } - }); - }); + return this._ipc.send('get_account') + .then( raw => { + if (raw === undefined || raw === null || typeof raw === 'string') { + return raw; + } else { + throw new InvalidReply(raw); + } + }); } setAccount(accountToken: ?AccountToken): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('set_account', accountToken) - .then(this._ignoreResponse); - }); + return this._ipc.send('set_account', accountToken) + .then(this._ignoreResponse); } _ignoreResponse(_response: mixed): void { @@ -124,60 +95,48 @@ export class RealIpc implements IpcFacade { } setCustomRelay(relayEndpoint: RelayEndpoint): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('set_custom_relay', [relayEndpoint]) - .then(this._ignoreResponse); - }); + return this._ipc.send('set_custom_relay', [relayEndpoint]) + .then(this._ignoreResponse); } connect(): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('connect') - .then(this._ignoreResponse); - }); + return this._ipc.send('connect') + .then(this._ignoreResponse); } disconnect(): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('disconnect') - .then(this._ignoreResponse); - }); + return this._ipc.send('disconnect') + .then(this._ignoreResponse); } getIp(): Promise<Ip> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_ip') - .then(raw => { - if (typeof raw === 'string' && raw) { - return raw; - } else { - throw new InvalidReply(raw, 'Expected a string'); - } - }); - }); + return this._ipc.send('get_ip') + .then(raw => { + if (typeof raw === 'string' && raw) { + return raw; + } else { + throw new InvalidReply(raw, 'Expected a string'); + } + }); } getLocation(): Promise<Location> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_location') - .then(raw => { - try { - const validated: any = validate(LocationSchema, raw); - return (validated: Location); - } catch (e) { - throw new InvalidReply(raw, e); - } - }); - }); + return this._ipc.send('get_location') + .then(raw => { + try { + const validated: any = validate(LocationSchema, raw); + return (validated: Location); + } catch (e) { + throw new InvalidReply(raw, e); + } + }); } getState(): Promise<BackendState> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_state') - .then(raw => { - return this._parseBackendState(raw); - }); - }); + return this._ipc.send('get_state') + .then(raw => { + return this._parseBackendState(raw); + }); } _parseBackendState(raw: mixed): BackendState { @@ -200,35 +159,19 @@ export class RealIpc implements IpcFacade { } registerStateListener(listener: (BackendState) => void) { - this._ensureAuthenticated().then(() => { - this._ipc.on('new_state', (rawEvent) => { - const parsedEvent : BackendState = this._parseBackendState(rawEvent); + this._ipc.on('new_state', (rawEvent) => { + const parsedEvent : BackendState = this._parseBackendState(rawEvent); - listener(parsedEvent); - }); + listener(parsedEvent); }); } - _ensureAuthenticated(): Promise<void> { - if(this._credentials) { - const credentials = this._credentials; - if(!this._authenticationPromise) { - this._authenticationPromise = this._authenticate(credentials.sharedSecret); - } - return this._authenticationPromise; - } else { - return Promise.reject(new Error('Missing authentication credentials.')); - } + auth(sharedSecret: string): Promise<void> { + return this._ipc.send('auth', sharedSecret) + .then(this._ignoreResponse); } - _authenticate(sharedSecret: string): Promise<void> { - return this._ipc.send('auth', sharedSecret) - .then(() => { - log.info('Authenticated with backend'); - }) - .catch((e) => { - log.error('Failed to authenticate with backend: ', e.message); - throw e; - }); + setCloseConnectionHandler(handler: () => void) { + console.log('appa', handler); } } diff --git a/app/main.js b/app/main.js index 1ca22fc115..7dcb83f9c6 100644 --- a/app/main.js +++ b/app/main.js @@ -7,7 +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/ipc-facade'; +import { parseIpcCredentials } from './lib/backend'; import type { TrayIconType } from './lib/tray-icon-manager'; diff --git a/test/auth.spec.js b/test/auth.spec.js new file mode 100644 index 0000000000..6bb23a7136 --- /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('auth') + .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.auth = () => { + 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 8f0b82ba37..2672a431fd 100644 --- a/test/mocks/ipc.js +++ b/test/mocks/ipc.js @@ -1,20 +1,23 @@ // @flow -import type { IpcFacade, BackendState, IpcCredentials } from '../../app/lib/ipc-facade'; +import type { IpcFacade, BackendState } from '../../app/lib/ipc-facade'; interface MockIpc { sendNewState: (BackendState) => void; + killWebSocket: () => void; -getAccountData: *; -connect: *; -getAccount: *; + -auth: *; } export function newMockIpc() { const stateListeners = []; + const connectionCloseListeners = []; const mockIpc: IpcFacade & MockIpc = { - setCredentials: (_credentials: IpcCredentials) => {}, + setConnectionString: (_str: string) => {}, getAccountData: (accountToken) => { return new Promise(r => r({ accountToken: accountToken, @@ -60,6 +63,15 @@ export function newMockIpc() { l(state); } }, + auth: (_secret: string) => Promise.resolve(), + setCloseConnectionHandler: (listener: () => void) => { + connectionCloseListeners.push(listener); + }, + killWebSocket: () => { + for(const l of connectionCloseListeners) { + l(); + } + } }; return mockIpc; |
