summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2017-03-25 20:17:19 +0800
committerErik Larkö <erik@mullvad.net>2017-04-03 01:44:33 +0800
commit59948d8c573daae1fadbecb09df5da83b337d3df (patch)
tree427324f5ebdd664ffa4fb3f6a5ee2b148ccda040
parenteeec205e2e51a05dde7ece5ed0e83d186abed5aa (diff)
downloadmullvadvpn-59948d8c573daae1fadbecb09df5da83b337d3df.tar.xz
mullvadvpn-59948d8c573daae1fadbecb09df5da83b337d3df.zip
Mock backend
-rw-r--r--app/app.js3
-rw-r--r--app/components/Connect.js2
-rw-r--r--app/lib/backend-redux-actions.js8
-rw-r--r--app/lib/backend.js297
-rw-r--r--app/lib/ipc.js119
-rw-r--r--test/actions.spec.js64
-rw-r--r--test/mocks/backend.js32
-rw-r--r--test/routing.spec.js22
8 files changed, 292 insertions, 255 deletions
diff --git a/app/app.js b/app/app.js
index b732dac7fd..39768a419c 100644
--- a/app/app.js
+++ b/app/app.js
@@ -67,8 +67,7 @@ const updateTrayIcon = () => {
ipcRenderer.send('changeTrayIcon', getIconType(connect.status));
};
-// patch backend
-backend.syncWithReduxStore(store);
+backend.sync();
// Setup primary event handlers to translate backend events into redux dispatch
mapBackendEventsToReduxActions(backend, store);
diff --git a/app/components/Connect.js b/app/components/Connect.js
index 29b97ac562..d759c994fd 100644
--- a/app/components/Connect.js
+++ b/app/components/Connect.js
@@ -100,7 +100,7 @@ export default class Connect extends Component {
</If>
</div>
</div>
- )
+ );
}
renderMap() {
diff --git a/app/lib/backend-redux-actions.js b/app/lib/backend-redux-actions.js
index 853f20b750..66688bc4d0 100644
--- a/app/lib/backend-redux-actions.js
+++ b/app/lib/backend-redux-actions.js
@@ -26,8 +26,12 @@ export default function mapBackendEventsToReduxActions(backend, store) {
}));
};
- const onConnect = (serverAddress) => {
- store.dispatch(connectActions.connectionChange({ status: ConnectionState.connected }));
+ const onConnect = (serverAddress, error) => {
+ if (error) {
+ console.error('UNABLE TO CONNECT TO', serverAddress, error);
+ } else {
+ store.dispatch(connectActions.connectionChange({ status: ConnectionState.connected }));
+ }
};
const onDisconnect = () => {
diff --git a/app/lib/backend.js b/app/lib/backend.js
index e93ed3d87a..728eac1214 100644
--- a/app/lib/backend.js
+++ b/app/lib/backend.js
@@ -2,7 +2,7 @@ import moment from 'moment';
import Enum from './enum';
import { EventEmitter } from 'events';
import { servers } from '../config';
-import { ConnectionState } from '../enums';
+import Ipc from './ipc';
/**
* Server info
@@ -90,23 +90,23 @@ class BackendError extends Error {
static localizedTitle(code) {
switch(code) {
- case Backend.ErrorType.noCredit:
- return 'Out of time';
- case Backend.ErrorType.noInternetConnection:
- return 'Offline';
- default:
- return 'Something went wrong';
+ case Backend.ErrorType.noCredit:
+ return 'Out of time';
+ case Backend.ErrorType.noInternetConnection:
+ return 'Offline';
+ default:
+ return 'Something went wrong';
}
}
static localizedMessage(code) {
switch(code) {
- case Backend.ErrorType.noCredit:
- return 'Buy more time, so you can continue using the internet securely';
- case Backend.ErrorType.noInternetConnection:
- return 'Your internet connection will be secured when you get back online';
- default:
- return '';
+ case Backend.ErrorType.noCredit:
+ return 'Buy more time, so you can continue using the internet securely';
+ case Backend.ErrorType.noInternetConnection:
+ return 'Your internet connection will be secured when you get back online';
+ default:
+ return '';
}
}
@@ -163,43 +163,15 @@ export default class Backend extends EventEmitter {
*
* @memberOf Backend
*/
- constructor() {
+ constructor(ipc) {
super();
- this._account = null;
- this._paidUntil = null;
- this._serverAddress = null;
- this._connStatus = ConnectionState.disconnected;
- this._cancellationHandler = null;
-
- // update IP in background
- setTimeout(::this._refreshIp, 0);
+ this._ipc = ipc || new Ipc(undefined);
+ this._registerIpcListeners();
// check for network reachability
this._startReachability();
}
- // Accessors
-
- /**
- * Account number
- *
- * @type {string}
- * @readonly
- *
- * @memberOf Backend
- */
- get account() { return this._account; }
-
- /**
- * Until when services are paid for (ISO string)
- *
- * @type {string}
- * @readonly
- *
- * @memberOf Backend
- */
- get paidUntil() { return this._paidUntil; }
-
/**
* Tells whether account has credits
*
@@ -213,50 +185,31 @@ export default class Backend extends EventEmitter {
moment(this._paidUntil).isAfter(moment());
}
- /**
- * Server IP address or domain name
- *
- * @type {string}
- * @readonly
- *
- * @memberOf Backend
- */
- get serverAddress() { return this._serverAddress; }
-
- // Public methods
-
- /**
- * 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.
- *
- * @param {Redux.Store} store - an instance of Redux store
- *
- * @memberOf Backend
- */
- syncWithReduxStore(store) {
- const { user, connect } = store.getState();
- const server = this.serverInfo(connect.preferredServer);
+ sync() {
+ console.log('Syncing with the backend...');
- if(server) {
- this._serverAddress = server.address;
- }
-
- if(user.account) {
- this._account = user.account;
- }
-
- if(user.paidUntil) {
- this._paidUntil = user.paidUntil;
- }
+ this._ipc.send('getConnectionInfo')
+ .then( connectionInfo => {
+ console.log('Got connection info', connectionInfo);
+ this.emit(Backend.EventType.updatedIp, connectionInfo.ip);
+ })
+ .catch(e => {
+ console.log('Failed syncing with the backend', e);
+ });
- this._connStatus = connect.status;
+ this._ipc.send('getLocation')
+ .then(location => {
+ console.log('Got location', location);
+ const newLocation = {
+ location: location.latlong,
+ country: location.country,
+ city: location.city
+ };
+ this.emit(Backend.EventType.updatedLocation, newLocation, null);
+ })
+ .catch(e => {
+ console.log('Failed getting new location', e);
+ });
}
/**
@@ -320,30 +273,23 @@ export default class Backend extends EventEmitter {
* @memberOf Backend
*/
login(account) {
- this._account = account;
+ console.log('Attempting to login with account number', account);
this._paidUntil = null;
// emit: logging in
- this.emit(Backend.EventType.logging, { account, paidUntil: this._paidUntil }, null);
+ this.emit(Backend.EventType.logging, { account }, null);
- // @TODO: Add login call
- setTimeout(() => {
- let err = null;
- let res = { account };
-
- if(account.startsWith('1111')) { // accounts starting with 1111 expire in one month
- this._paidUntil = res.paidUntil = moment().startOf('day').add(15, 'days').toISOString();
- } else if(account.startsWith('2222')) { // expired in 2013
- this._paidUntil = res.paidUntil = moment('2013-01-01').toISOString();
- } else if(account.startsWith('3333')) { // expire in 2038
- this._paidUntil = res.paidUntil = moment('2038-01-01').toISOString();
- } else {
- err = new BackendError(Backend.ErrorType.invalidAccount);
- }
-
- // emit: login
- this.emit(Backend.EventType.login, res, err);
- }, 2000);
+ this._ipc.send('login', {
+ accountNumber: account,
+ }).then(response => {
+ console.log('Successfully logged in', response);
+ this._paidUntil = response.paidUntil;
+ this.emit(Backend.EventType.login, response, undefined);
+ }).catch(e => {
+ console.warn('Failed to log in', e);
+ const err = new BackendError(Backend.ErrorType.invalidAccount);
+ this.emit(Backend.EventType.login, {}, err);
+ });
}
/**
@@ -353,16 +299,20 @@ export default class Backend extends EventEmitter {
* @memberOf Backend
*/
logout() {
- this._account = null;
- this._paidUntil = null;
-
- // emit event
- this.emit(Backend.EventType.logout);
+ // @TODO: What does it mean for a logout to be successful or failed?
+ this._ipc.send('logout')
+ .then(() => {
+ this._paidUntil = null;
- // disconnect user during logout
- this.disconnect();
+ // emit event
+ this.emit(Backend.EventType.logout);
- // @TODO: Add logout call
+ // disconnect user during logout
+ this.disconnect();
+ })
+ .catch(e => {
+ console.log('Failed to logout', e);
+ });
}
/**
@@ -375,42 +325,23 @@ export default class Backend extends EventEmitter {
* @memberOf Backend
*/
connect(addr) {
- this.disconnect();
-
// do not attempt to connect when no credits available
if(!this.hasCredits) {
return;
}
- this._connStatus = ConnectionState.connecting;
- this._serverAddress = addr;
-
// emit: connecting
this.emit(Backend.EventType.connecting, addr);
-
- // @TODO: Add connect call
- let timer = null;
-
- timer = setTimeout(() => {
- this._connStatus = ConnectionState.connected;
- // emit: connect
- this.emit(Backend.EventType.connect, addr);
- this._refreshIp();
-
- // reset timer
- timer = null;
- this._cancellationHandler = null;
- }, 5000);
-
- this._cancellationHandler = () => {
- if(timer !== null) {
- clearTimeout(timer);
- this._timer = null;
- }
- this._cancellationHandler = null;
- this._connStatus = ConnectionState.disconnected;
- };
+ this._ipc.send('connect', { address: addr })
+ .then(() => {
+ this.emit(Backend.EventType.connect, addr);
+ this.sync(); // TODO: This is a pooooooor way of updating the location and the IP and stuff
+ })
+ .catch(e => {
+ console.log('Failed connecting to', addr, e);
+ this.emit(Backend.EventType.connect, undefined, e);
+ });
}
/**
@@ -420,69 +351,22 @@ export default class Backend extends EventEmitter {
* @memberOf Backend
*/
disconnect() {
- if(this._connStatus === ConnectionState.disconnected) { return; }
-
- this._connStatus = ConnectionState.disconnected;
- this._serverAddress = null;
-
- // cancel ongoing connection attempt
- if(this._cancellationHandler) {
- this._cancellationHandler();
- } else {
- this._refreshIp();
- }
-
- // emit: disconnect
- this.emit(Backend.EventType.disconnect);
-
- // @TODO: Add disconnect call
- }
-
- /**
- * Fetch user location
- *
- * @private
- * @returns {promise}
- *
- * @memberOf Backend
- */
- _fetchLocation() {
- return fetch('https://freegeoip.net/json/').then((res) => {
- return res.json();
- });
- }
-
- /**
- * Request updates for user IP
- *
- * @private
- * @emits Backend.EventType.updatedLocation
- * @emits Backend.EventType.updatedIp
- *
- * @memberOf Backend
- */
- _refreshIp() {
- if(this._connStatus === ConnectionState.disconnected) {
- this._fetchLocation().then((res) => {
- const data = {
- location: [ res.latitude, res.longitude ], // lat, lng
- city: res.city,
- country: res.country_name
- };
- this.emit(Backend.EventType.updatedLocation, data);
- this.emit(Backend.EventType.updatedIp, res.ip);
- }).catch((error) => {
- console.log('Got error: ', error);
+ // @TODO: Failure modes
+ this._ipc.send('cancelConnection')
+ .catch(e => {
+ console.log('Failed cancelling connection', e);
});
- return;
- }
-
- let ip = [];
- for(let i = 0; i < 4; i++) {
- ip.push(parseInt(Math.random() * 253 + 1));
- }
- this.emit(Backend.EventType.updatedIp, ip.join('.'));
+ // @TODO: Failure modes
+ this._ipc.send('disconnect')
+ .then(() => {
+ // emit: disconnect
+ this.emit(Backend.EventType.disconnect);
+ this.sync(); // TODO: This is a pooooooor way of updating the location and the IP and stuff
+ })
+ .catch(e => {
+ console.log('Failed to disconnect', e);
+ });
}
/**
@@ -509,4 +393,11 @@ export default class Backend extends EventEmitter {
this.emit(Backend.EventType.updatedReachability, false);
});
}
+
+
+ _registerIpcListeners() {
+ this._ipc.on('connection-info', (newConnectionInfo) => {
+ console.log('Got new connection info from backend', newConnectionInfo);
+ });
+ }
}
diff --git a/app/lib/ipc.js b/app/lib/ipc.js
new file mode 100644
index 0000000000..a1f31c4a9a
--- /dev/null
+++ b/app/lib/ipc.js
@@ -0,0 +1,119 @@
+import moment from 'moment';
+
+const mockUnsecureGeoIp = {
+ ip: '192.168.1.2',
+ latlong: [60,61],
+ country: 'sweden',
+ city: 'bollebygd',
+};
+const mockSecureGeoIp = {
+ ip: '1.2.3.4',
+ latlong: [1,2],
+ country: 'Narnia',
+ city: 'LE CITY',
+};
+export default class Ipc {
+
+ constructor(connectionString) {
+ this.connectionString = connectionString;
+ }
+
+ on(event/*, listener*/) {
+ console.log('Adding a listener to', event);
+ }
+
+ send(action, data) {
+ return this.mockSend(action, data)
+ .catch(e => {
+ console.error('IPC call failed', action, data, e);
+ throw e;
+ });
+ }
+
+ mockSend(action, data) {
+ const actions = {
+ login: this.mockLogin,
+ logout: this.mockLogout,
+ connect: ::this.mockConnect,
+ cancelConnection: this.mockCancelConnection,
+ disconnect: ::this.mockDisconnect,
+ getConnectionInfo: () => {
+ return new Promise((resolve) => {
+ resolve({
+ ip: this._mockIsConnected ? mockSecureGeoIp.ip : mockUnsecureGeoIp.ip,
+ });
+ });
+ },
+ getLocation: () => {
+ return new Promise((resolve) => {
+ const data = this._mockIsConnected ? mockSecureGeoIp : mockUnsecureGeoIp;
+ resolve({
+ latlong: data.latlong,
+ country: data.country,
+ city: data.city,
+ });
+ });
+ },
+ };
+
+ const actionCb = actions[action];
+ if (actionCb) {
+ return actionCb(action, data);
+ } else {
+ console.log('UNKNOWN IPC ACTION', action);
+ return new Promise((resolve, reject) => {
+ reject('Unknown IPC action ' + action);
+ });
+ }
+ }
+
+ mockLogin(action, data) {
+ return new Promise((resolve, reject) => {
+ let res = {account: data.accountNumber};
+
+ if(data.accountNumber.startsWith('1111')) { // accounts starting with 1111 expire in one month
+ res.paidUntil = moment().startOf('day').add(15, 'days').toISOString();
+ } else if(data.accountNumber.startsWith('2222')) { // expired in 2013
+ res.paidUntil = moment('2013-01-01').toISOString();
+ } else if(data.accountNumber.startsWith('3333')) { // expire in 2038
+ res.paidUntil = moment('2038-01-01').toISOString();
+ } else {
+ reject(new Error('Invalid account number.'));
+ return;
+ }
+
+ resolve(res);
+ });
+ }
+
+ mockLogout() {
+ return new Promise(function(resolve) {
+ resolve();
+ });
+ }
+
+ mockConnect(action, data) {
+ return new Promise((resolve, reject) => {
+ // Prototype: Swedish servers will throw error during connect
+ if(/se\d+\.mullvad\.net/.test(data.address)) {
+ reject(new Error('Server is unreachable'));
+ } else {
+ this._mockIsConnected = true;
+ resolve();
+ }
+ });
+ }
+
+ mockCancelConnection() {
+ return new Promise(function(resolve) {
+ resolve();
+ });
+ }
+
+ mockDisconnect() {
+ return new Promise((resolve) => {
+ this._mockIsConnected = false;
+ resolve();
+ });
+ }
+}
diff --git a/test/actions.spec.js b/test/actions.spec.js
index 84a53c935f..f91a99a1bf 100644
--- a/test/actions.spec.js
+++ b/test/actions.spec.js
@@ -11,11 +11,16 @@ describe('actions', function() {
it('should login', (done) => {
const expectedActions = [
- { type: 'USER_LOGIN_CHANGE', payload: { status: 'connecting', error: null, account: '222223456789', paidUntil: null } },
- { type: 'USER_LOGIN_CHANGE', payload: { paidUntil: '2013-01-01T00:00:00.000Z', status: 'ok', error: null } }
+ { type: 'USER_LOGIN_CHANGE', payload: { status: 'connecting', error: null, account: '1'} },
+ { type: 'USER_LOGIN_CHANGE', payload: { paidUntil: '2013-01-01T00:00:00.000Z', status: 'ok', error: undefined } }
];
const store = mockStore(mockState());
- const backend = mockBackend(store);
+ const backend = mockBackend({
+ users: {
+ 1: {
+ paidUntil: '2013-01-01T00:00:00.000Z',
+ }}
+ });
mapBackendEventsToReduxActions(backend, store);
backend.once(Backend.EventType.login, () => {
@@ -24,24 +29,16 @@ describe('actions', function() {
done();
});
- store.dispatch(userActions.login(backend, '222223456789'));
+ store.dispatch(userActions.login(backend, '1'));
});
it('should logout', (done) => {
const expectedActions = [
- { type: 'USER_LOGIN_CHANGE', payload: { account: null, paidUntil: null, status: 'none', error: null } }
+ { type: 'USER_LOGIN_CHANGE', payload: { account: null, paidUntil: null, status: 'none', error: null } },
];
- let state = Object.assign(mockState(), {
- user: {
- account: '3333234567890',
- paidUntil: '2038-01-01T00:00:00.000Z',
- status: LoginState.ok
- }
- });
-
- const store = mockStore(state);
- const backend = mockBackend(store);
+ const store = mockStore(mockState());
+ const backend = mockBackend();
mapBackendEventsToReduxActions(backend, store);
backend.once(Backend.EventType.logout, () => {
@@ -50,7 +47,7 @@ describe('actions', function() {
expect(storeActions).deep.equal(expectedActions);
done();
});
-
+
store.dispatch(userActions.logout(backend));
});
@@ -60,31 +57,36 @@ describe('actions', function() {
{ type: 'CONNECTION_CHANGE', payload: { status: 'connected' } }
];
- let state = Object.assign(mockState(), {
- user: {
- account: '3333234567890',
- paidUntil: '2038-01-01T00:00:00.000Z',
- status: LoginState.ok
- }
- });
-
- const store = mockStore(state);
- const backend = mockBackend(store);
+ const store = mockStore(mockState());
+ const backend = mockBackend({
+ users: {
+ '1': {
+ paidUntil: '2038-01-01T00:00:00.000Z',
+ status: LoginState.ok
+ }
+ }});
mapBackendEventsToReduxActions(backend, store);
backend.once(Backend.EventType.connect, () => {
- const storeActions = filterMinorActions(store.getActions());
+
+ const storeActions = filterMinorActions(store.getActions())
+ .filter(action => {
+ return action.type === 'CONNECTION_CHANGE' && action.payload.status !== 'disconnected';
+ });
expect(storeActions).deep.equal(expectedActions);
done();
});
- store.dispatch(connectActions.connect(backend, '1.2.3.4'));
+ backend.once(Backend.EventType.login, () => {
+ store.dispatch(connectActions.connect(backend, '1.2.3.4'));
+ });
+ store.dispatch(userActions.login(backend, '1'));
});
it('should disconnect from VPN server', (done) => {
const expectedActions = [
- { type: 'CONNECTION_CHANGE', payload: { serverAddress: null, status: 'disconnected' } }
+ { type: 'CONNECTION_CHANGE', payload: { status: 'disconnected', serverAddress: null } }
];
let state = Object.assign(mockState(), {
@@ -100,7 +102,7 @@ describe('actions', function() {
});
const store = mockStore(state);
- const backend = mockBackend(store);
+ const backend = mockBackend();
mapBackendEventsToReduxActions(backend, store);
backend.once(Backend.EventType.disconnect, () => {
@@ -132,7 +134,7 @@ describe('actions', function() {
});
const store = mockStore(state);
- const backend = mockBackend(store);
+ const backend = mockBackend();
mapBackendEventsToReduxActions(backend, store);
backend.once(Backend.EventType.disconnect, () => {
diff --git a/test/mocks/backend.js b/test/mocks/backend.js
index bac5fac19e..3011c8e26b 100644
--- a/test/mocks/backend.js
+++ b/test/mocks/backend.js
@@ -1,6 +1,7 @@
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';
@@ -29,13 +30,34 @@ export const mockState = () => {
};
};
-export const mockBackend = (store) => {
- const backend = new Backend();
+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) => {
- // patch backend
- backend.syncWithReduxStore(store);
+ switch (action) {
+ case 'login':
+ return resolve(backendData.users[data.accountNumber]);
+ case 'logout':
+ case 'cancelConnection':
+ case 'connect':
+ case 'disconnect':
+ return resolve();
- return backend;
+ case 'getLocation':
+ return resolve({});
+ case 'getConnectionInfo':
+ return resolve({});
+ }
+
+ reject('Unknown action: ' + action);
+ });
+ };
+ return ipc;
};
export const filterMinorActions = (actions) => {
diff --git a/test/routing.spec.js b/test/routing.spec.js
index 38af64b58c..ad0fa16322 100644
--- a/test/routing.spec.js
+++ b/test/routing.spec.js
@@ -21,13 +21,15 @@ describe('routing', function() {
});
const store = mockStore(state);
- const backend = mockBackend(store);
+ const backend = mockBackend();
mapBackendEventsToRouter(backend, store);
store.dispatch(userActions.logout(backend));
-
- const storeActions = filterMinorActions(store.getActions());
- expect(storeActions).deep.equal(expectedActions);
+
+ setTimeout(() => {
+ const storeActions = filterMinorActions(store.getActions());
+ expect(storeActions).deep.equal(expectedActions);
+ }, 0);
});
it('should redirect to connect screen on login', (done) => {
@@ -35,14 +37,12 @@ describe('routing', function() {
{ type: '@@router/CALL_HISTORY_METHOD', payload: { method: 'replace', args: [ '/connect' ] } }
];
- let state = Object.assign(mockState(), {
- user: {
- account: '1111234567890',
- status: LoginState.none
+ const store = mockStore(mockState());
+ const backend = mockBackend({
+ users: {
+ '1': { status: LoginState.none },
}
});
- const store = mockStore(state);
- const backend = mockBackend(store);
mapBackendEventsToRouter(backend, store);
store.subscribe(() => {
@@ -50,7 +50,7 @@ describe('routing', function() {
expect(storeActions).deep.equal(expectedActions);
done();
});
- store.dispatch(userActions.login(backend, '1111234567890'));
+ store.dispatch(userActions.login(backend, '1'));
});
});