summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2017-05-28 08:11:57 +0200
committerErik Larkö <erik@mullvad.net>2017-05-28 08:11:57 +0200
commitf6cc8959a01bb245095f959f6c7ff0be1867e2fa (patch)
tree0354b0a582d4e80e95ee2b89b41dca6505845f56
parente4e5a8dbe22d2c4be7540e8d70861990907a0b27 (diff)
parent28d66c1afe80cb9fed366a50f9cf5051a15d78dc (diff)
downloadmullvadvpn-f6cc8959a01bb245095f959f6c7ff0be1867e2fa.tar.xz
mullvadvpn-f6cc8959a01bb245095f959f6c7ff0be1867e2fa.zip
Merge branch 'ipc-facade'
-rw-r--r--app/lib/backend.js44
-rw-r--r--app/lib/ipc-facade.js71
-rw-r--r--app/lib/jsonrpc-ws-ipc.js (renamed from app/lib/ipc.js)67
-rw-r--r--package.json1
-rw-r--r--test/actions.spec.js41
-rw-r--r--test/mocks/ipc.js37
-rw-r--r--test/mocks/redux.js (renamed from test/mocks/backend.js)32
-rw-r--r--test/routing.spec.js12
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(() => {