diff options
| -rw-r--r-- | app/lib/backend.js | 44 | ||||
| -rw-r--r-- | app/lib/ipc-facade.js | 71 | ||||
| -rw-r--r-- | app/lib/jsonrpc-ws-ipc.js (renamed from app/lib/ipc.js) | 67 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | test/actions.spec.js | 41 | ||||
| -rw-r--r-- | test/mocks/ipc.js | 37 | ||||
| -rw-r--r-- | test/mocks/redux.js (renamed from test/mocks/backend.js) | 32 | ||||
| -rw-r--r-- | test/routing.spec.js | 12 |
8 files changed, 213 insertions, 92 deletions
diff --git a/app/lib/backend.js b/app/lib/backend.js index d310c6f28d..ce7c6d17c3 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -4,7 +4,7 @@ import log from 'electron-log'; import Enum from './enum'; import EventEmitter from 'events'; import { servers } from '../config'; -import Ipc from './ipc'; +import { IpcFacade, RealIpc } from './ipc-facade'; /** * Server info @@ -66,7 +66,6 @@ import Ipc from './ipc'; * @event Backend.EventType.updatedIp * @param {string} new IP address */ - /** * Updated location event * @@ -117,12 +116,6 @@ class BackendError extends Error { } -type Location = { - latlong: Array<number>, - city: string, - country: string, -}; - /** * Backend implementation * @@ -169,16 +162,16 @@ export default class Backend extends EventEmitter { */ static EventType = new Enum('connect', 'connecting', 'disconnect', 'login', 'logging', 'logout', 'updatedIp', 'updatedLocation', 'updatedReachability'); - _ipc: Ipc; + _ipc: IpcFacade; /** * Creates an instance of Backend. * * @memberOf Backend */ - constructor(ipc: Ipc) { + constructor(ipc: IpcFacade) { super(); - this._ipc = ipc || new Ipc(undefined); + this._ipc = ipc || new RealIpc(undefined); this._registerIpcListeners(); // check for network reachability @@ -188,15 +181,15 @@ export default class Backend extends EventEmitter { setLocation(loc: string) { log.info('Got connection info to backend', loc); - this._ipc = new Ipc(loc); + this._ipc = new RealIpc(loc); this._registerIpcListeners(); } sync() { log.info('Syncing with the backend...'); - this._ipc.send('get_ip') - .then( (ip: string) => { + this._ipc.getIp() + .then( ip => { log.info('Got ip', ip); this.emit(Backend.EventType.updatedIp, ip); }) @@ -204,8 +197,8 @@ export default class Backend extends EventEmitter { log.info('Failed syncing with the backend', e); }); - this._ipc.send('get_location') - .then((location: Location) => { + this._ipc.getLocation() + .then( location => { log.info('Got location', location); const newLocation = { location: location.latlong, @@ -285,14 +278,13 @@ export default class Backend extends EventEmitter { // emit: logging in this.emit(Backend.EventType.logging, { account }, null); - - - this._ipc.send('get_account_data', account) - .then(response => { + this._ipc.getAccountData(account) + .then( response => { log.info('Account exists', response); - return this._ipc.send('set_account', account) - .then(() => response ); + return this._ipc.setAccount(account) + .then( () => response ); + }).then( accountData => { log.info('Log in complete'); @@ -315,7 +307,7 @@ export default class Backend extends EventEmitter { */ logout() { // @TODO: What does it mean for a logout to be successful or failed? - this._ipc.send('set_account', '') + this._ipc.setAccount('') .then(() => { // emit event this.emit(Backend.EventType.logout); @@ -342,9 +334,9 @@ export default class Backend extends EventEmitter { // emit: connecting this.emit(Backend.EventType.connecting, addr); - this._ipc.send('set_country', addr) + this._ipc.setCountry(addr) .then( () => { - return this._ipc.send('connect'); + return this._ipc.connect(); }) .then(() => { this.emit(Backend.EventType.connect, addr); @@ -364,7 +356,7 @@ export default class Backend extends EventEmitter { */ disconnect() { // @TODO: Failure modes - this._ipc.send('disconnect') + this._ipc.disconnect() .then(() => { // emit: disconnect this.emit(Backend.EventType.disconnect); diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js new file mode 100644 index 0000000000..03fa4f961a --- /dev/null +++ b/app/lib/ipc-facade.js @@ -0,0 +1,71 @@ +// @flow + +import JsonRpcWs from './jsonrpc-ws-ipc'; + +export type AccountData = {paid_until: string}; +export type AccountNumber = string; +export type Ip = string; +export type Location = { + latlong: Array<Number>, + country: string, + city: string, +}; + +export interface IpcFacade { + getAccountData(AccountNumber): Promise<AccountData>, + setAccount(accountNumber: AccountNumber): Promise<void>, + setCountry(address: string): Promise<void>, + connect(): Promise<void>, + disconnect(): Promise<void>, + getIp(): Promise<Ip>, + getLocation(): Promise<Location>, +} + +export class RealIpc implements IpcFacade { + + _ipc: JsonRpcWs; + + constructor(connectionString: ?string) { + this._ipc = new JsonRpcWs(connectionString); + } + + getAccountData(accountNumber: AccountNumber): Promise<AccountData> { + return this._ipc.send('get_account_data', accountNumber) + .then(raw => { + // TODO: Validate here + return raw; + }); + } + + setAccount(accountNumber: AccountNumber): Promise<void> { + return this._ipc.send('set_account', accountNumber); + } + + setCountry(address: string): Promise<void> { + return this._ipc.send('set_country', address); + } + + connect(): Promise<void> { + return this._ipc.send('connect'); + } + + disconnect(): Promise<void> { + return this._ipc.send('disconnect'); + } + + getIp(): Promise<Ip> { + return this._ipc.send('get_ip') + .then(raw => { + // TODO: Validate here + return raw; + }); + } + + getLocation(): Promise<Location> { + return this._ipc.send('get_location') + .then(raw => { + // TODO: Validate here + return raw; + }); + } +} diff --git a/app/lib/ipc.js b/app/lib/jsonrpc-ws-ipc.js index 8471360c9c..1fdcdf6428 100644 --- a/app/lib/ipc.js +++ b/app/lib/jsonrpc-ws-ipc.js @@ -1,12 +1,55 @@ +// @flow + import jsonrpc from 'jsonrpc-lite'; import uuid from 'uuid'; import log from 'electron-log'; +export type UnansweredRequest<T, E> = { + resolve: (T) => void, + reject: (E) => void, + timeout: number, +} + +export type JsonRpcError = { + type: 'error', + payload: { + id: string, + error: { + message: string, + } + } +} +export type JsonRpcNotification = { + type: 'notification', + payload: { + method: string, + params: { + subscription: string, + result: any, + } + } +} +export type JsonRpcSuccess = { + type: 'success', + payload: { + id: string, + result: any, + } +} +export type JsonRpcMessage = JsonRpcError | JsonRpcNotification | JsonRpcSuccess; + const DEFAULT_TIMEOUT_MILLIS = 750; export default class Ipc { - constructor(connectionString) { + _connectionString: ?string; + _onConnect: Array<{resolve: ()=>void}>; + _unansweredRequests: {[string]: UnansweredRequest<any, any>}; + _subscriptions: {[string]: (any) => void}; + _websocket: WebSocket; + _backoff: ReconnectionBackoff; + + constructor(connectionString: ?string) { this._connectionString = connectionString; this._onConnect = []; this._unansweredRequests = {}; @@ -16,7 +59,7 @@ export default class Ipc { this._reconnect(); } - on(event, listener) { + on(event: string, listener: (any) => void) { // We're currently not actually using the event parameter. // This is because we aren't sure if the backend will use // one subscription per event or one subscription per @@ -27,7 +70,7 @@ export default class Ipc { .then(subscriptionId => this._subscriptions[subscriptionId] = listener); } - send(action, ...data) { + send(action: string, ...data: Array<any>): Promise<any> { return this._getWebSocket() .then(ws => this._send(ws, action, data)) .catch(e => { @@ -77,7 +120,7 @@ export default class Ipc { request.reject('The request timed out'); } - _onMessage(message) { + _onMessage(message: string) { const json = JSON.parse(message); const c = jsonrpc.parseObject(json); @@ -88,7 +131,7 @@ export default class Ipc { } } - _onNotification(message) { + _onNotification(message: JsonRpcNotification) { const subscriptionId = message.payload.params.subscription; const listener = this._subscriptions[subscriptionId]; @@ -100,7 +143,7 @@ export default class Ipc { } } - _onReply(message) { + _onReply(message: JsonRpcError | JsonRpcSuccess) { const id = message.payload.id; const request = this._unansweredRequests[id]; delete this._unansweredRequests[id]; @@ -123,10 +166,11 @@ export default class Ipc { } _reconnect() { - if (!this._connectionString) return; + const connectionString = this._connectionString; + if (!connectionString) return; - log.info('Connecting to websocket', this._connectionString); - this._websocket = new WebSocket(this._connectionString); + log.info('Connecting to websocket', connectionString); + this._websocket = new WebSocket(connectionString); this._websocket.onopen = () => { log.debug('Websocket is connected'); @@ -138,7 +182,8 @@ export default class Ipc { }; this._websocket.onmessage = (evt) => { - this._onMessage(evt.data); + const data: string = (evt.data: any); + this._onMessage(data); }; this._websocket.onclose = () => { @@ -157,6 +202,8 @@ export default class Ipc { * to 3000ms */ class ReconnectionBackoff { + _attempt: number; + constructor() { this._attempt = 0; } diff --git a/package.json b/package.json index 02484e0b36..f5429c29c2 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "eslint": "^3.14.1", "eslint-plugin-react": "^6.9.0", "flow-bin": "^0.46.0", + "flow-typed": "^2.1.2", "isomorphic-fetch": "^2.2.1", "jsdom": "^9.11.0", "mocha": "^3.2.0", diff --git a/test/actions.spec.js b/test/actions.spec.js index d8f8bfbc26..e1420e92d2 100644 --- a/test/actions.spec.js +++ b/test/actions.spec.js @@ -1,6 +1,9 @@ +// @flow + import { expect } from 'chai'; -import { filterMinorActions, mockBackend, mockState, mockStore } from './mocks/backend'; +import { filterMinorActions, mockState, mockStore } from './mocks/redux'; import Backend from '../app/lib/backend'; +import { newMockIpc } from './mocks/ipc'; import userActions from '../app/actions/user'; import connectActions from '../app/actions/connect'; import mapBackendEventsToReduxActions from '../app/lib/backend-redux-actions'; @@ -15,12 +18,15 @@ describe('actions', function() { { type: 'USER_LOGIN_CHANGE', payload: { paidUntil: '2013-01-01T00:00:00.000Z', status: 'ok', error: undefined } } ]; const store = mockStore(mockState()); - const backend = mockBackend({ - users: { - 1: { - paid_until: '2013-01-01T00:00:00.000Z', - }} - }); + const mockIpc = newMockIpc(); + mockIpc.getAccountData = () => { + return new Promise(r => r({ + paid_until: '2013-01-01T00:00:00.000Z', + })); + }; + + const backend = new Backend(mockIpc); + mapBackendEventsToReduxActions(backend, store); backend.once(Backend.EventType.login, () => { @@ -38,7 +44,8 @@ describe('actions', function() { ]; const store = mockStore(mockState()); - const backend = mockBackend(); + const mockIpc = newMockIpc(); + const backend = new Backend(mockIpc); mapBackendEventsToReduxActions(backend, store); backend.once(Backend.EventType.logout, () => { @@ -58,13 +65,13 @@ describe('actions', function() { ]; const store = mockStore(mockState()); - const backend = mockBackend({ - users: { - '1': { - paid_until: '2038-01-01T00:00:00.000Z', - status: LoginState.ok - } - }}); + const mockIpc = newMockIpc(); + const backend = new Backend(mockIpc); + mockIpc.getAccountData = () => { + return new Promise(r => r({ + paid_until: '2038-01-01T00:00:00.000Z', + })); + }; mapBackendEventsToReduxActions(backend, store); backend.once(Backend.EventType.connect, () => { @@ -102,7 +109,7 @@ describe('actions', function() { }); const store = mockStore(state); - const backend = mockBackend(); + const backend = new Backend(newMockIpc()); mapBackendEventsToReduxActions(backend, store); backend.once(Backend.EventType.disconnect, () => { @@ -134,7 +141,7 @@ describe('actions', function() { }); const store = mockStore(state); - const backend = mockBackend(); + const backend = new Backend(newMockIpc()); mapBackendEventsToReduxActions(backend, store); backend.once(Backend.EventType.disconnect, () => { diff --git a/test/mocks/ipc.js b/test/mocks/ipc.js new file mode 100644 index 0000000000..bb1a872454 --- /dev/null +++ b/test/mocks/ipc.js @@ -0,0 +1,37 @@ +// @flow +import type { IpcFacade } from '../../app/lib/ipc-facade'; + +export function newMockIpc() { + return Object.assign({}, mockIpc); +} + +const mockIpc: IpcFacade = { + + getAccountData: () => { + return new Promise(r => r({ + paid_until: '', + })); + }, + setAccount: () => { + return new Promise(r => r()); + }, + setCountry: () => { + return new Promise(r => r()); + }, + connect: () => { + return new Promise(r => r()); + }, + disconnect: () => { + return new Promise(r => r()); + }, + getIp: () => { + return new Promise(r => r('1.2.3.4')); + }, + getLocation: () => { + return new Promise(r => r({ + city: '', + country: '', + latlong: [], + })); + }, +}; diff --git a/test/mocks/backend.js b/test/mocks/redux.js index f665df8ee7..6855274296 100644 --- a/test/mocks/backend.js +++ b/test/mocks/redux.js @@ -1,7 +1,5 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import Backend from '../../app/lib/backend'; -import Ipc from '../../app/lib/ipc'; import { defaultServer } from '../../app/config'; import { LoginState, ConnectionState } from '../../app/enums'; @@ -30,36 +28,6 @@ export const mockState = () => { }; }; -export const mockBackend = (backendData) => { - return new Backend(mockIpc(backendData)); -}; - -const mockIpc = (backendData) => { - const ipc = new Ipc(); - ipc.send = (action, data) => { - return new Promise((resolve, reject) => { - - switch (action) { - case 'get_account_data': { - const accountNumber = data; - return resolve(backendData.users[accountNumber]); - } - case 'set_account': - case 'set_country': - case 'connect': - case 'disconnect': - return resolve(); - - case 'event_subscribe': - return resolve(); - } - - reject('Unknown action: ' + action); - }); - }; - return ipc; -}; - export const filterMinorActions = (actions) => { return actions.filter((action) => { if(action.type === 'CONNECTION_CHANGE' && action.payload.clientIp) { diff --git a/test/routing.spec.js b/test/routing.spec.js index ad0fa16322..2ce33e73c2 100644 --- a/test/routing.spec.js +++ b/test/routing.spec.js @@ -1,9 +1,11 @@ import { expect } from 'chai'; -import { filterMinorActions, mockBackend, mockState, mockStore } from './mocks/backend'; +import { filterMinorActions, mockState, mockStore } from './mocks/redux'; import userActions from '../app/actions/user'; import mapBackendEventsToRouter from '../app/lib/backend-routing'; import { LoginState } from '../app/enums'; +import Backend from '../app/lib/backend'; +import { newMockIpc } from './mocks/ipc'; describe('routing', function() { this.timeout(10000); @@ -21,7 +23,7 @@ describe('routing', function() { }); const store = mockStore(state); - const backend = mockBackend(); + const backend = new Backend(newMockIpc()); mapBackendEventsToRouter(backend, store); store.dispatch(userActions.logout(backend)); @@ -38,11 +40,7 @@ describe('routing', function() { ]; const store = mockStore(mockState()); - const backend = mockBackend({ - users: { - '1': { status: LoginState.none }, - } - }); + const backend = new Backend(newMockIpc()); mapBackendEventsToRouter(backend, store); store.subscribe(() => { |
