diff options
| author | Erik Larkö <erik@mullvad.net> | 2017-07-03 13:41:07 +0200 |
|---|---|---|
| committer | Erik Larkö <erik@mullvad.net> | 2017-07-10 07:18:33 +0200 |
| commit | 9c65adb7a677e9358c178e47e5e786b1e60b127f (patch) | |
| tree | 6ec964ced2fe548a70fefa9435aef4c27c6c96f6 | |
| parent | 0b45a621235e66f8145f7b842592acf713be8690 (diff) | |
| download | mullvadvpn-9c65adb7a677e9358c178e47e5e786b1e60b127f.tar.xz mullvadvpn-9c65adb7a677e9358c178e47e5e786b1e60b127f.zip | |
Autologin
| -rw-r--r-- | app/app.js | 4 | ||||
| -rw-r--r-- | app/lib/backend.js | 46 | ||||
| -rw-r--r-- | app/lib/ipc-facade.js | 12 | ||||
| -rw-r--r-- | test/autologin.spec.js | 110 | ||||
| -rw-r--r-- | test/mocks/ipc.js | 4 |
5 files changed, 175 insertions, 1 deletions
diff --git a/app/app.js b/app/app.js index 7710ff1acf..5ff241089f 100644 --- a/app/app.js +++ b/app/app.js @@ -23,10 +23,12 @@ const store = configureStore(initialState, memoryHistory); // Backend ////////////////////////////////////////////////////////////////////////// const backend = new Backend(store); - ipcRenderer.on('backend-info', (_event, args) => { backend.setLocation(args.addr); backend.sync(); + if(store.getState().settings.autoSecure) { + backend.autologin(); + } }); ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// diff --git a/app/lib/backend.js b/app/lib/backend.js index a8774dc599..9513a3f08b 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -186,6 +186,52 @@ export class Backend { }); } + autologin() { + log.info('Attempting to log in automatically'); + + this._store.dispatch(accountActions.loginChange({ + accountNumber: null, + status: 'logging in', + error: null, + })); + + return this._ipc.getAccount() + .then( accountNumber => { + if (!accountNumber) { + throw new Error('No account set in the backend, failing autologin'); + } + log.debug('The backend had an account number stored:', accountNumber); + + this._store.dispatch(accountActions.loginChange({ + accountNumber: accountNumber, + status: 'logging in', + error: null, + })); + + return this._ipc.getAccountData(accountNumber); + }) + .then( accountData => { + log.info('The stored account number still exists', accountData); + + this._store.dispatch(accountActions.loginChange({ + status: 'ok', + paidUntil: accountData.paid_until, + error: null, + })); + + this._store.dispatch(push('/connect')); + }) + .catch( e => { + log.warn('Unable to autologin', e); + + this._store.dispatch(accountActions.loginChange({ + status: 'none', + error: new BackendError('INVALID_ACCOUNT'), + })); + + this._store.dispatch(push('/')); + }); + } logout() { // @TODO: What does it mean for a logout to be successful or failed? diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js index ff2b7050ed..b5541e4297 100644 --- a/app/lib/ipc-facade.js +++ b/app/lib/ipc-facade.js @@ -24,6 +24,7 @@ export type BackendState = 'secured' | 'unsecured'; export interface IpcFacade { getAccountData(AccountNumber): Promise<AccountData>, + getAccount(): Promise<?AccountNumber>, setAccount(accountNumber: AccountNumber): Promise<void>, setCountry(address: string): Promise<void>, connect(): Promise<void>, @@ -53,6 +54,17 @@ export class RealIpc implements IpcFacade { }); } + getAccount(): Promise<?AccountNumber> { + return this._ipc.send('get_account') + .then( raw => { + if (raw === undefined || raw === null || typeof raw === 'string') { + return raw; + } else { + throw new InvalidReply(raw); + } + }); + } + setAccount(accountNumber: AccountNumber): Promise<void> { return this._ipc.send('set_account', accountNumber) .then(this._ignoreResponse); diff --git a/test/autologin.spec.js b/test/autologin.spec.js new file mode 100644 index 0000000000..43e5bfa333 --- /dev/null +++ b/test/autologin.spec.js @@ -0,0 +1,110 @@ +// @flow + +import { expect } from 'chai'; +import { setupBackendAndStore, setupBackendAndMockStore, getLocation } from './helpers/ipc-helpers'; +import { IpcChain } from './helpers/IpcChain'; + +describe('autologin', () => { + + it('should send get_account then get_account_data if an account is set', (done) => { + const { mockIpc, backend } = setupBackendAndStore(); + + const randomAccountNumber = '12345'; + + const chain = new IpcChain(mockIpc, done); + chain.addRequiredStep('getAccount') + .withReturnValue(randomAccountNumber) + .done(); + + chain.addRequiredStep('getAccountData') + .withInputValidation((num) => { + expect(num).to.equal(randomAccountNumber); + }) + .done(); + + backend.autologin(); + }); + + it('should redirect to the login page if no account is set', () => { + const { store, backend, mockIpc } = setupBackendAndMockStore(); + + mockIpc.getAccount = () => new Promise((_, reject) => reject('NO_ACCOUNT')); + + return backend.autologin() + .then( () => { + expect(getLocation(store)).to.equal('/'); + }); + }); + + it('should redirect to the login page for non-existing accounts', () => { + const { store, backend, mockIpc } = setupBackendAndMockStore(); + + mockIpc.getAccount = () => new Promise(r => r('123')); + mockIpc.getAccountData = () => new Promise((_, reject) => reject('NO ACCOUNT')); + + return backend.autologin() + .then( () => { + expect(getLocation(store)).to.equal('/'); + }); + }); + + it('should mark the state as not logged in if no account is set', () => { + const { store, backend, mockIpc } = setupBackendAndStore(); + + mockIpc.getAccount = () => new Promise(r => r(null)); + + return backend.autologin() + .then( () => { + const state = store.getState().account; + + expect(state.status).to.equal('none'); + expect(state.accountNumber).to.be.null; + expect(state.error).not.to.be.null; + }); + }); + + it('should mark the state as not logged in for non-existing accounts', () => { + const { store, backend, mockIpc } = setupBackendAndStore(); + + mockIpc.getAccount = () => new Promise(r => r('123')); + mockIpc.getAccountData = () => new Promise((_, reject) => reject('NO ACCOUNT')); + + return backend.autologin() + .then( () => { + const state = store.getState().account; + + expect(state.status).to.equal('none'); + expect(state.error).not.to.be.null; + }); + }); + + it('should put the account data in the state for existing accounts', () => { + const { store, backend, mockIpc } = setupBackendAndStore(); + mockIpc.getAccount = () => new Promise(r => r('123')); + mockIpc.getAccountData = () => new Promise(r => r({ + paid_until: '2001-01-01T00:00:00', + })); + + return backend.autologin() + .then( () => { + const state = store.getState().account; + expect(state.status).to.equal('ok'); + expect(state.accountNumber).to.equal('123'); + expect(state.paidUntil).to.equal('2001-01-01T00:00:00'); + }); + }); + + it('should redirect to /connect for existing accounts', () => { + const { store, backend, mockIpc } = setupBackendAndMockStore(); + + mockIpc.getAccount = () => new Promise(r => r('123')); + mockIpc.getAccountData = () => new Promise(r => r({ + paid_until: '2001-01-01T00:00:00', + })); + + return backend.autologin() + .then( () => { + expect(getLocation(store)).to.equal('/connect'); + }); + }); +}); diff --git a/test/mocks/ipc.js b/test/mocks/ipc.js index a172bef8e5..cc6146be73 100644 --- a/test/mocks/ipc.js +++ b/test/mocks/ipc.js @@ -5,6 +5,7 @@ interface MockIpc { sendNewState: (BackendState) => void; -getAccountData: *; -connect: *; + -getAccount: *; } export function newMockIpc() { @@ -18,6 +19,9 @@ export function newMockIpc() { paid_until: '', })); }, + getAccount: () => { + return new Promise(r => r('1111')); + }, setAccount: () => { return new Promise(r => r()); }, |
