diff options
| -rw-r--r-- | app/app.js | 3 | ||||
| -rw-r--r-- | app/components/Connect.js | 2 | ||||
| -rw-r--r-- | app/lib/backend-redux-actions.js | 8 | ||||
| -rw-r--r-- | app/lib/backend.js | 297 | ||||
| -rw-r--r-- | app/lib/ipc.js | 119 | ||||
| -rw-r--r-- | test/actions.spec.js | 64 | ||||
| -rw-r--r-- | test/mocks/backend.js | 32 | ||||
| -rw-r--r-- | test/routing.spec.js | 22 |
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')); }); }); |
