summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorErik Larkö <erik@mullvad.net>2017-07-03 13:41:07 +0200
committerErik Larkö <erik@mullvad.net>2017-07-10 07:18:33 +0200
commit9c65adb7a677e9358c178e47e5e786b1e60b127f (patch)
tree6ec964ced2fe548a70fefa9435aef4c27c6c96f6
parent0b45a621235e66f8145f7b842592acf713be8690 (diff)
downloadmullvadvpn-9c65adb7a677e9358c178e47e5e786b1e60b127f.tar.xz
mullvadvpn-9c65adb7a677e9358c178e47e5e786b1e60b127f.zip
Autologin
-rw-r--r--app/app.js4
-rw-r--r--app/lib/backend.js46
-rw-r--r--app/lib/ipc-facade.js12
-rw-r--r--test/autologin.spec.js110
-rw-r--r--test/mocks/ipc.js4
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());
},