summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-07-03 13:39:18 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-07-03 13:39:18 +0200
commit38968509b59329f2d0743312fc8f9a4f98daa66c (patch)
treef6e5f8b4f8dc72ae1bae0ddfe715b8094deb3750 /test
parentbe096ee87bb1256b67c3b24b61be77d523aff9cc (diff)
parentb3a6f58d7027e5961180b52d6473f7f504638284 (diff)
downloadmullvadvpn-38968509b59329f2d0743312fc8f9a4f98daa66c.tar.xz
mullvadvpn-38968509b59329f2d0743312fc8f9a4f98daa66c.zip
Merge branch 'app-overhaul'
Diffstat (limited to 'test')
-rw-r--r--test/auth.spec.js37
-rw-r--r--test/autologin.spec.js126
-rw-r--r--test/components/Login.spec.js104
-rw-r--r--test/connect.spec.js62
-rw-r--r--test/helpers/IpcChain.js100
-rw-r--r--test/helpers/ipc-helpers.js104
-rw-r--r--test/ipc.spec.js134
-rw-r--r--test/jsonrpc-transport.spec.js144
-rw-r--r--test/login.spec.js83
-rw-r--r--test/logout.spec.js67
-rw-r--r--test/mocks/redux.js4
-rw-r--r--test/mocks/rpc.js (renamed from test/mocks/ipc.js)75
12 files changed, 211 insertions, 829 deletions
diff --git a/test/auth.spec.js b/test/auth.spec.js
deleted file mode 100644
index e6b2c25486..0000000000
--- a/test/auth.spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// @flow
-
-import { setupIpcAndStore, setupBackendAndStore } from './helpers/ipc-helpers';
-import { IpcChain } from './helpers/IpcChain';
-import { Backend } from '../app/lib/backend';
-
-describe('authentication', () => {
- it('authenticates before ipc call if unauthenticated', (done) => {
- const { store, mockIpc } = setupIpcAndStore();
-
- const chain = new IpcChain(mockIpc);
- chain.onSuccessOrFailure(done);
- chain.expect('authenticate').withInputValidation((secret) => {
- expect(secret).to.equal(credentials.sharedSecret);
- });
- chain.expect('connect');
-
- const credentials = {
- sharedSecret: '',
- connectionString: '',
- };
- const backend = new Backend(store, credentials, mockIpc);
- backend.connect();
- });
-
- it('reauthenticates on reconnect', async () => {
- const { mockIpc, backend } = setupBackendAndStore();
-
- mockIpc.authenticate = spy(mockIpc.authenticate);
- mockIpc.killWebSocket();
-
- expect(mockIpc.authenticate).to.not.have.been.called();
-
- await backend.connect();
- expect(mockIpc.authenticate).to.have.been.called.once;
- });
-});
diff --git a/test/autologin.spec.js b/test/autologin.spec.js
deleted file mode 100644
index 60c5cc5bde..0000000000
--- a/test/autologin.spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-// @flow
-
-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 randomAccountToken = '12345';
-
- const chain = new IpcChain(mockIpc);
- chain.expect('getAccount').withReturnValue(randomAccountToken);
-
- chain.expect('getAccountData').withInputValidation((num) => {
- expect(num).to.equal(randomAccountToken);
- });
-
- chain.onSuccessOrFailure(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('/');
- })
- .catch((e) => {
- if (e !== 'NO_ACCOUNT') {
- throw e;
- }
- });
- });
-
- 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('/');
- })
- .catch((e) => {
- if (e !== 'NO_ACCOUNT') {
- throw e;
- }
- });
- });
-
- it('should mark the state as not logged in if no account is set', () => {
- const { store, backend, mockIpc } = setupBackendAndStore();
-
- mockIpc.getAccount = () => Promise.resolve(null);
-
- return backend
- .autologin()
- .catch(() => {}) // ignore errors
- .then(() => {
- const state = store.getState().account;
-
- expect(state.status).to.equal('none');
- expect(state.accountToken).to.be.null;
- expect(state.error).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()
- .catch(() => {}) // ignore errors
- .then(() => {
- const state = store.getState().account;
-
- expect(state.status).to.equal('none');
- expect(state.error).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({
- expiry: '2001-01-01T00:00:00Z',
- }),
- );
-
- return backend.autologin().then(() => {
- const state = store.getState().account;
- expect(state.status).to.equal('ok');
- expect(state.accountToken).to.equal('123');
- expect(state.expiry).to.equal('2001-01-01T00:00:00Z');
- });
- });
-
- 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({
- expiry: '2001-01-01T00:00:00Z',
- }),
- );
-
- return backend.autologin().then(() => {
- expect(getLocation(store)).to.equal('/connect');
- });
- });
-});
diff --git a/test/components/Login.spec.js b/test/components/Login.spec.js
index ce7f347acf..76acbd6330 100644
--- a/test/components/Login.spec.js
+++ b/test/components/Login.spec.js
@@ -2,32 +2,18 @@
import * as React from 'react';
import { shallow } from 'enzyme';
-
import Login from '../../app/components/Login';
-import AccountInput from '../../app/components/AccountInput';
describe('components/Login', () => {
- it('notifies on the first change after failure', () => {
- const onFirstChange = spy();
- const props = {
- account: Object.assign({}, defaultAccount, {
- status: 'failed',
- }),
- onFirstChangeAfterFailure: onFirstChange,
- };
-
- const component = renderWithProps(props);
- const accountInput = component.find(AccountInput);
-
- accountInput.simulate('change', 'foo');
- expect(onFirstChange).to.have.been.called.once;
-
- accountInput.simulate('change', 'bar');
- expect(onFirstChange).to.have.been.called.once;
- });
-
it('does not show the footer when logging in', () => {
- const component = renderLoggingIn();
+ const component = shallow(
+ <Login
+ {...{
+ ...defaultProps,
+ loginState: 'logging in',
+ }}
+ />,
+ );
const visibleFooters = getComponent(component, 'footerVisibility true');
const invisibleFooters = getComponent(component, 'footerVisibility false');
expect(visibleFooters.length).to.equal(0);
@@ -35,7 +21,7 @@ describe('components/Login', () => {
});
it('shows the footer and account input when not logged in', () => {
- const component = renderNotLoggedIn();
+ const component = shallow(<Login {...defaultProps} />);
const visibleFooters = getComponent(component, 'footerVisibility true');
const invisibleFooters = getComponent(component, 'footerVisibility false');
expect(visibleFooters.length).to.equal(1);
@@ -44,7 +30,14 @@ describe('components/Login', () => {
});
it('does not show the footer nor account input when logged in', () => {
- const component = renderLoggedIn();
+ const component = shallow(
+ <Login
+ {...{
+ ...defaultProps,
+ loginState: 'ok',
+ }}
+ />,
+ );
const visibleFooters = getComponent(component, 'footerVisibility true');
const invisibleFooters = getComponent(component, 'footerVisibility false');
expect(visibleFooters.length).to.equal(0);
@@ -53,14 +46,12 @@ describe('components/Login', () => {
});
it('logs in with the entered account number when clicking the login icon', (done) => {
- const component = renderNotLoggedIn();
+ const component = shallow(<Login {...defaultProps} />);
component.setProps({
- account: Object.assign({}, defaultAccount, {
- accountToken: '12345',
- }),
- onLogin: (an) => {
+ accountToken: '12345',
+ login: (accountToken) => {
try {
- expect(an).to.equal('12345');
+ expect(accountToken).to.equal('12345');
done();
} catch (e) {
done(e);
@@ -72,54 +63,19 @@ describe('components/Login', () => {
});
});
-const defaultAccount = {
+const defaultProps = {
accountToken: null,
accountHistory: [],
- expiry: null,
- status: 'none',
- error: null,
+ loginError: null,
+ loginState: 'none',
+ openSettings: () => {},
+ openExternalLink: (_type) => {},
+ login: (_accountToken) => {},
+ resetLoginError: () => {},
+ updateAccountToken: (_accountToken) => {},
+ removeAccountTokenFromHistory: (_accountToken) => Promise.resolve(),
};
-const defaultProps = {
- account: defaultAccount,
- onLogin: () => {},
- onSettings: () => {},
- onChange: () => {},
- onFirstChangeAfterFailure: () => {},
- onExternalLink: () => {},
- onAccountTokenChange: (_accountToken) => {},
- onRemoveAccountTokenFromHistory: (_accountToken) => {},
-};
-
-function renderLoggedIn() {
- return renderWithProps({
- account: Object.assign(defaultAccount, {
- status: 'ok',
- }),
- });
-}
-
-function renderLoggingIn() {
- return renderWithProps({
- account: Object.assign(defaultAccount, {
- status: 'logging in',
- }),
- });
-}
-
-function renderNotLoggedIn() {
- return renderWithProps({
- account: Object.assign(defaultAccount, {
- status: 'none',
- }),
- });
-}
-
-function renderWithProps(customProps) {
- const props = Object.assign({}, defaultProps, customProps);
- return shallow(<Login {...props} />);
-}
-
function getComponent(container, testName) {
return container.findWhere((n) => n.prop('testName') === testName);
}
diff --git a/test/connect.spec.js b/test/connect.spec.js
deleted file mode 100644
index f1621dbeb6..0000000000
--- a/test/connect.spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// @flow
-
-import connectionActions from '../app/redux/connection/actions';
-import { setupBackendAndStore, checkNextTick } from './helpers/ipc-helpers';
-
-describe('connect', () => {
- it("should set the connection state to 'disconnected' on failed attempts", (done) => {
- const { store, mockIpc, backend } = setupBackendAndStore();
-
- mockIpc.connect = () => new Promise((_, reject) => reject('Some error'));
-
- store.dispatch(connectionActions.connected());
-
- expect(store.getState().connection.status).not.to.equal('disconnected');
-
- store.dispatch(connectionActions.connect(backend));
-
- checkNextTick(() => {
- expect(store.getState().connection.status).to.equal('disconnected');
- }, done);
- });
-
- it('should update the state with the server address', () => {
- const { store, backend } = setupBackendAndStore();
-
- return backend.connect().then(() => {
- const state = store.getState().connection;
- expect(state.status).to.equal('connecting');
- });
- });
-
- it("should correctly deduce 'connected' from backend states", (done) => {
- const { store, mockIpc } = setupBackendAndStore();
-
- checkNextTick(() => {
- expect(store.getState().connection.status).not.to.equal('connected');
- mockIpc.sendNewState({ state: 'secured', target_state: 'secured' });
- expect(store.getState().connection.status).to.equal('connected');
- }, done);
- });
-
- it("should correctly deduce 'connecting' from backend states", (done) => {
- const { store, mockIpc } = setupBackendAndStore();
-
- checkNextTick(() => {
- expect(store.getState().connection.status).not.to.equal('connecting');
- mockIpc.sendNewState({ state: 'unsecured', target_state: 'secured' });
- expect(store.getState().connection.status).to.equal('connecting');
- }, done);
- });
-
- it("should correctly deduce 'disconnected' from backend states", (done) => {
- const { store, mockIpc } = setupBackendAndStore();
- store.dispatch(connectionActions.connected());
-
- checkNextTick(() => {
- expect(store.getState().connection.status).not.to.equal('disconnected');
- mockIpc.sendNewState({ state: 'unsecured', target_state: 'unsecured' });
- expect(store.getState().connection.status).to.equal('disconnected');
- }, done);
- });
-});
diff --git a/test/helpers/IpcChain.js b/test/helpers/IpcChain.js
deleted file mode 100644
index 1505621cc0..0000000000
--- a/test/helpers/IpcChain.js
+++ /dev/null
@@ -1,100 +0,0 @@
-// @flow
-
-import { check, failFast } from './ipc-helpers';
-
-export class IpcChain {
- _expectedCalls: Array<string>;
- _recordedCalls: Array<string>;
- _mockIpc: {};
- _done: (?Error) => void;
- _aborted: boolean;
-
- constructor(mockIpc: {}) {
- this._expectedCalls = [];
- this._recordedCalls = [];
- this._mockIpc = mockIpc;
- this._aborted = false;
- }
-
- expect<R>(ipcCall: string): StepBuilder<R> {
- const builder = new StepBuilder(ipcCall);
- this._expectedCalls.push(ipcCall);
- this._addStep(builder);
-
- return builder;
- }
-
- _addStep<R>(step: StepBuilder<R>) {
- this._mockIpc[step.ipcCall] = (...args: Array<mixed>) => {
- return new Promise((r) => this._stepPromiseCallback(step, r, args));
- };
- }
-
- _stepPromiseCallback<R>(step: StepBuilder<R>, resolve: (?R) => void, args: Array<mixed>) {
- if (this._aborted) {
- return;
- }
-
- this._registerCall(step.ipcCall);
-
- const inputValidation = step.inputValidation;
- if (inputValidation) {
- const failedInputValidation = failFast(() => {
- inputValidation(...args);
- }, this._done);
-
- if (failedInputValidation) {
- this._abort();
- return;
- }
- }
-
- if (this._isLastCall()) {
- this._ensureChainCalledCorrectly();
- }
-
- resolve(step.returnValue);
- }
-
- _abort() {
- this._aborted = true;
- }
-
- _isLastCall(): boolean {
- return this._recordedCalls.length === this._expectedCalls.length;
- }
-
- _ensureChainCalledCorrectly() {
- check(() => {
- expect(this._expectedCalls).to.deep.equal(this._recordedCalls);
- }, this._done);
- }
-
- _registerCall(ipcCall: string) {
- this._recordedCalls.push(ipcCall);
- }
-
- onSuccessOrFailure(done: (*) => void) {
- this._done = done;
- }
-}
-
-class StepBuilder<R> {
- ipcCall: string;
- inputValidation: ?(...args: Array<mixed>) => void;
- returnValue: ?R;
-
- constructor(ipcCall: string) {
- this.ipcCall = ipcCall;
- }
-
- withInputValidation(iv: (...args: Array<mixed>) => void): this {
- this.inputValidation = iv;
- return this;
- }
-
- withReturnValue(rv: R): this {
- this.returnValue = rv;
- return this;
- }
-}
diff --git a/test/helpers/ipc-helpers.js b/test/helpers/ipc-helpers.js
deleted file mode 100644
index 4466c06eb2..0000000000
--- a/test/helpers/ipc-helpers.js
+++ /dev/null
@@ -1,104 +0,0 @@
-// @flow
-
-import { Backend } from '../../app/lib/backend';
-import { newMockIpc } from '../mocks/ipc';
-import configureStore from '../../app/redux/store';
-import { createMemoryHistory } from 'history';
-import { mockStore } from '../mocks/redux';
-
-type DoneCallback = (?Error) => void;
-type Check = () => void;
-
-export function setupIpcAndStore() {
- const memoryHistory = createMemoryHistory();
- const store = configureStore(null, memoryHistory);
-
- const mockIpc = newMockIpc();
-
- return { store, mockIpc };
-}
-
-export function setupBackendAndStore() {
- const { store, mockIpc } = setupIpcAndStore();
-
- const credentials = {
- sharedSecret: '',
- connectionString: '',
- };
- const backend = new Backend(store, credentials, mockIpc);
-
- return { store, mockIpc, backend };
-}
-
-export function setupBackendAndMockStore() {
- const store = mockStore(_initialState());
- const mockIpc = newMockIpc();
- const credentials = {
- sharedSecret: '',
- connectionString: '',
- };
- const backend = new Backend(store, credentials, mockIpc);
- return { store, mockIpc, backend };
-}
-
-function _initialState() {
- const { store } = setupIpcAndStore();
- return store.getState();
-}
-
-// chai and async aren't the best of friends. To allow us
-// to get the assertion error in the output of failed async
-// tests we need to do this try-catch thing.
-export function check(fn: Check, done: DoneCallback) {
- try {
- fn();
- done();
- } catch (e) {
- done(e);
- }
-}
-
-// Sometimes with redux we cannot know when all reducers have
-// finished running. This function puts the check at the end
-// of the execution queue, hopefully resulting in the check being
-// run after the reducers are finished
-export function checkNextTick(fn: Check, done: DoneCallback) {
- setTimeout(() => {
- check(fn, done);
- }, 1);
-}
-
-// In async tests where we want to test a chain of IPC messages
-// we can only invoke `done` for the last message. This function
-// is for the intermediate messages.
-export function failFast(fn: Check, done: DoneCallback): boolean {
- try {
- fn();
- return false;
- } catch (e) {
- done(e);
- return true;
- }
-}
-export function failFastNextTick(fn: Check, done: DoneCallback) {
- setTimeout(() => {
- failFast(fn, done);
- }, 1);
-}
-
-type MockStore = {
- getActions: () => Array<{ type: string, payload: Object }>,
-};
-// Parses the action log to find out which URL we most recently navigated to
-// Note that this cannot be done with the real redux store, but rather must be
-// done with the mock store.
-export function getLocation(store: MockStore): ?string {
- const navigations = store
- .getActions()
- .filter((action) => action.type === '@@router/CALL_HISTORY_METHOD');
- if (navigations.length === 0) {
- return null;
- }
-
- return navigations[navigations.length - 1].payload.args[0];
-}
diff --git a/test/ipc.spec.js b/test/ipc.spec.js
deleted file mode 100644
index 1c8b19b5a6..0000000000
--- a/test/ipc.spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-// @flow
-
-import Ipc from '../app/lib/jsonrpc-ws-ipc';
-import jsonrpc from 'jsonrpc-lite';
-import type { JsonRpcMessage } from '../app/lib/jsonrpc-ws-ipc';
-
-describe('The IPC server', () => {
- it('should send as soon as the websocket connects', () => {
- const { ws, ipc } = setupIpc();
- ws.close();
-
- let sent = false;
- const p = ipc.send('hello').then(() => {
- expect(sent).to.be.true;
- });
-
- ws.on('hello', (msg) => {
- sent = true;
-
- ws.replyOk(msg.id);
- });
- ws.acceptConnection();
-
- return p;
- });
-
- it('should reject failed jsonrpc requests', () => {
- const { ws, ipc } = setupIpc();
- ws.on('WHAT_IS_THIS', (msg) => {
- ws.replyFail(msg.id, 'Method not found', -32601);
- });
-
- return ipc.send('WHAT_IS_THIS').catch((e) => {
- expect(e.code).to.equal(-32601);
- expect(e.message).to.contain('Method not found');
- });
- });
-
- it('should route reply to correct promise', () => {
- const { ws, ipc } = setupIpc();
-
- ws.on('a message', (msg) => ws.replyOk(msg.id, 'a reply'));
-
- const decoy = ipc
- .send('a decoy', [], 1)
- .then(() => {
- throw new Error('Should not be called');
- })
- .catch((e) => {
- if (e.name !== 'TimeOutError') {
- throw e;
- }
- });
- const message = ipc.send('a message', [], 1).then((reply) => expect(reply).to.equal('a reply'));
-
- return Promise.all([message, decoy]);
- });
-
- it('should timeout if no response is returned', () => {
- const { ipc } = setupIpc();
-
- return ipc.send('a message', [], 1).catch((e) => {
- expect(e.name).to.equal('TimeOutError');
- expect(e.message).to.contain('timed out');
- });
- });
-
- it('should route notifications', (done) => {
- const { ws, ipc } = setupIpc();
-
- const eventListener = (event) => {
- try {
- expect(event).to.equal('an event!');
- done();
- } catch (ex) {
- done(ex);
- }
- };
-
- ws.on('event_subscribe', (msg) => ws.replyOk(msg.id, 1));
- ipc
- .on('event', eventListener)
- .then(() => {
- ws.reply(jsonrpc.notification('event', { subscription: 1, result: 'an event!' }));
- })
- .catch((e) => done(e));
- });
-});
-
-function mockWebsocket() {
- const ws: any = {
- listeners: {},
- readyState: 1,
- };
-
- ws.on = (event, listener) => (ws.listeners[event] = listener);
- ws.send = (data) => {
- const listener = ws.listeners[data.method];
- if (listener) {
- listener(data);
- }
- };
-
- ws.factory = () => ws;
-
- ws.acceptConnection = () => {
- ws.readyState = 1;
- ws.onopen();
- };
- ws.close = () => {
- ws.readyState = 3;
- ws.onclose();
- };
-
- ws.reply = (msg: JsonRpcMessage) => {
- ws.onmessage({ data: JSON.stringify(msg) });
- };
- ws.replyOk = (id: string, msg) => {
- ws.reply(jsonrpc.success(id, msg || ''));
- };
- ws.replyFail = (id: string, msg: string, code: number) => {
- ws.reply(jsonrpc.error(id, new jsonrpc.JsonRpcError(msg, code)));
- };
-
- return ws;
-}
-
-function setupIpc() {
- const ws = mockWebsocket();
- return {
- ws: ws,
- ipc: new Ipc('1.2.3.4', ws.factory),
- };
-}
diff --git a/test/jsonrpc-transport.spec.js b/test/jsonrpc-transport.spec.js
new file mode 100644
index 0000000000..124bba8f00
--- /dev/null
+++ b/test/jsonrpc-transport.spec.js
@@ -0,0 +1,144 @@
+// @flow
+
+import JsonRpcTransport, {
+ TimeOutError as JsonRpcTransportTimeOutError,
+} from '../app/lib/jsonrpc-transport';
+import jsonrpc from 'jsonrpc-lite';
+import { Server, WebSocket as MockWebSocket } from 'mock-socket';
+
+describe('JSON RPC transport', () => {
+ const WEBSOCKET_URL = 'ws://localhost:8080';
+ let server: Server, transport: JsonRpcTransport;
+
+ beforeEach(() => {
+ server = new Server(WEBSOCKET_URL);
+ transport = new JsonRpcTransport((s) => new MockWebSocket(s));
+ });
+
+ afterEach(() => {
+ server.close();
+ });
+
+ it('should send as soon as the websocket connects', (done) => {
+ server.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+
+ if (payload.method === 'hello') {
+ server.send(JSON.stringify(jsonrpc.success(payload.id, 'ok')));
+ }
+ });
+
+ transport
+ .send('hello')
+ .then(() => {
+ done();
+ })
+ .catch((error) => {
+ done(error);
+ });
+
+ transport.connect(WEBSOCKET_URL);
+ });
+
+ it('should reject failed jsonrpc requests', (done) => {
+ server.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+
+ if (payload.method === 'invalid-method') {
+ server.send(
+ JSON.stringify(
+ jsonrpc.error(payload.id, new jsonrpc.JsonRpcError('Method not found', -32601)),
+ ),
+ );
+ }
+ });
+
+ transport.send('invalid-method').catch((error) => {
+ try {
+ expect(error.code).to.equal(-32601);
+ expect(error.message).to.contain('Method not found');
+ done();
+ } catch (error) {
+ done(error);
+ }
+ });
+
+ transport.connect(WEBSOCKET_URL);
+ });
+
+ it('should route reply to correct promise', () => {
+ server.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+
+ if (payload.method === 'a message') {
+ server.send(JSON.stringify(jsonrpc.success(payload.id, 'a reply')));
+ }
+ });
+
+ const decoy = transport
+ .send('a decoy', [], 100)
+ .then(() => {
+ throw new Error('Should not be called');
+ })
+ .catch((error) => {
+ expect(error).to.be.an.instanceof(JsonRpcTransportTimeOutError);
+ });
+
+ const message = transport.send('a message', [], 100).then((reply) => {
+ expect(reply).to.equal('a reply');
+ });
+
+ transport.connect(WEBSOCKET_URL);
+
+ return Promise.all([message, decoy]);
+ });
+
+ it('should timeout if no response is returned', (done) => {
+ transport
+ .send('timeout-message', {}, 1)
+ .then(() => {
+ done(new Error('Should not be called'));
+ })
+ .catch((error) => {
+ try {
+ expect(error).to.be.an.instanceof(JsonRpcTransportTimeOutError);
+ expect(error.message).to.contain('Request timed out');
+ done();
+ } catch (error) {
+ done(error);
+ }
+ });
+
+ transport.connect(WEBSOCKET_URL);
+ });
+
+ it('should route notifications', (done) => {
+ server.on('message', (msg) => {
+ const { payload } = jsonrpc.parse(msg);
+
+ if (payload.method === 'event_subscribe') {
+ server.send(JSON.stringify(jsonrpc.success(payload.id, 1)));
+ }
+ });
+
+ transport
+ .subscribe('event', (event) => {
+ try {
+ expect(event).to.equal('an event!');
+ done();
+ } catch (error) {
+ done(error);
+ }
+ })
+ .then(() => {
+ server.send(
+ JSON.stringify(jsonrpc.notification('event', { subscription: 1, result: 'an event!' })),
+ );
+ })
+ .catch((error) => {
+ done(error);
+ });
+
+ transport.connect(WEBSOCKET_URL);
+ });
+});
diff --git a/test/login.spec.js b/test/login.spec.js
deleted file mode 100644
index 68db14494b..0000000000
--- a/test/login.spec.js
+++ /dev/null
@@ -1,83 +0,0 @@
-// @flow
-
-import {
- setupBackendAndStore,
- setupBackendAndMockStore,
- checkNextTick,
- getLocation,
- failFast,
- check,
-} from './helpers/ipc-helpers';
-import { IpcChain } from './helpers/IpcChain';
-import accountActions from '../app/redux/account/actions';
-
-describe('Logging in', () => {
- it('should validate the account number and then set it in the backend', (done) => {
- const { store, mockIpc, backend } = setupBackendAndStore();
-
- const chain = new IpcChain(mockIpc);
- chain.expect('getAccountData').withInputValidation((an) => {
- expect(an).to.equal('123');
- });
-
- chain.expect('setAccount').withInputValidation((an) => {
- expect(an).to.equal('123');
- });
-
- chain.onSuccessOrFailure(done);
-
- store.dispatch(accountActions.login(backend, '123'));
- });
-
- it('should put the account data in the state', () => {
- const { store, backend, mockIpc } = setupBackendAndStore();
- mockIpc.getAccountData = () =>
- new Promise((r) =>
- r({
- expiry: '2001-01-01T00:00:00Z',
- }),
- );
-
- return backend.login('123').then(() => {
- const state = store.getState().account;
- expect(state.status).to.equal('ok');
- expect(state.accountToken).to.equal('123');
- expect(state.expiry).to.equal('2001-01-01T00:00:00Z');
- });
- });
-
- it('should indicate failure for non-existing accounts', (done) => {
- const { store, mockIpc, backend } = setupBackendAndStore();
-
- mockIpc.getAccountData = (_num) =>
- new Promise((_, reject) => {
- reject('NO SUCH ACCOUNT');
- });
-
- store.dispatch(accountActions.login(backend, '123'));
-
- checkNextTick(() => {
- const state = store.getState().account;
- expect(state.status).to.equal('failed');
- expect(state.error).to.not.be.null;
- }, done);
- });
-
- it('should redirect to /connect after 1s after successful login', (done) => {
- const { store, backend } = setupBackendAndMockStore();
-
- store.dispatch(accountActions.login(backend, '123'));
-
- setTimeout(() => {
- failFast(() => {
- expect(getLocation(store)).not.to.equal('/connect');
- }, done);
- }, 100);
-
- setTimeout(() => {
- check(() => {
- expect(getLocation(store)).to.equal('/connect');
- }, done);
- }, 1100);
- });
-});
diff --git a/test/logout.spec.js b/test/logout.spec.js
deleted file mode 100644
index 65bc3aba09..0000000000
--- a/test/logout.spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// @flow
-
-import {
- setupBackendAndStore,
- setupBackendAndMockStore,
- getLocation,
- checkNextTick,
- failFastNextTick,
-} from './helpers/ipc-helpers';
-import { IpcChain } from './helpers/IpcChain';
-import accountActions from '../app/redux/account/actions';
-
-describe('logging out', () => {
- it('should set the account to null and then disconnect', (done) => {
- const { mockIpc, backend } = setupBackendAndStore();
-
- const chain = new IpcChain(mockIpc);
- chain.expect('setAccount').withInputValidation((num) => {
- expect(num).to.be.null;
- });
- chain.expect('disconnect');
- chain.onSuccessOrFailure(done);
-
- backend.logout();
- });
-
- it('should remove the account number from the store', (done) => {
- const { store, backend, mockIpc } = setupBackendAndStore();
- mockIpc.getAccountData = () =>
- new Promise((r) =>
- r({
- expiry: '2001-01-01T00:00:00.000Z',
- }),
- );
- const action: any = accountActions.login(backend, '123');
- store.dispatch(action);
-
- const expectedLogoutState = {
- status: 'none',
- accountToken: null,
- expiry: null,
- error: null,
- };
-
- failFastNextTick(() => {
- let state = store.getState().account;
- expect(state).not.to.include(expectedLogoutState);
-
- backend.logout();
-
- checkNextTick(() => {
- state = store.getState().account;
- expect(state).to.include(expectedLogoutState);
- }, done);
- }, done);
- });
-
- it('should redirect to / on logout', (done) => {
- const { store, backend } = setupBackendAndMockStore();
-
- backend.logout();
-
- checkNextTick(() => {
- expect(getLocation(store)).to.equal('/');
- }, done);
- });
-});
diff --git a/test/mocks/redux.js b/test/mocks/redux.js
deleted file mode 100644
index bbab7b17f4..0000000000
--- a/test/mocks/redux.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-
-export const mockStore = configureMockStore([thunk]);
diff --git a/test/mocks/ipc.js b/test/mocks/rpc.js
index 590313ec13..4b37ca35aa 100644
--- a/test/mocks/ipc.js
+++ b/test/mocks/rpc.js
@@ -1,39 +1,38 @@
// @flow
-import type { IpcFacade, AccountToken, AccountData, BackendState } from '../../app/lib/ipc-facade';
+import type {
+ DaemonRpcProtocol,
+ AccountToken,
+ AccountData,
+ BackendState,
+} from '../../app/lib/daemon-rpc';
-interface MockIpc {
+interface MockRpc {
sendNewState: (BackendState) => void;
- killWebSocket: () => void;
-getAccountData: (AccountToken) => Promise<AccountData>;
- -connect: () => Promise<void>;
+ -connectTunnel: () => Promise<void>;
-getAccount: () => Promise<?AccountToken>;
-authenticate: (string) => Promise<void>;
}
-export function newMockIpc() {
+export function newMockRpc() {
const stateListeners = [];
- const connectionCloseListeners = [];
+ const openListeners = [];
+ const closeListeners = [];
- const mockIpc: IpcFacade & MockIpc = {
+ const mockIpc: DaemonRpcProtocol & MockRpc = {
setConnectionString: (_str: string) => {},
-
getAccountData: (accountToken) =>
Promise.resolve({
accountToken: accountToken,
expiry: '',
}),
-
getRelayLocations: () =>
Promise.resolve({
countries: [],
}),
-
getAccount: () => Promise.resolve('1111'),
-
setAccount: () => Promise.resolve(),
-
updateRelaySettings: () => Promise.resolve(),
-
getRelaySettings: () =>
Promise.resolve({
custom_tunnel_endpoint: {
@@ -46,17 +45,20 @@ export function newMockIpc() {
},
},
}),
-
setAllowLan: (_allowLan: boolean) => Promise.resolve(),
-
getAllowLan: () => Promise.resolve(true),
-
- connect: () => Promise.resolve(),
-
- disconnect: () => Promise.resolve(),
-
- shutdown: () => Promise.resolve(),
-
+ connect: () => {
+ for (const listener of openListeners) {
+ listener();
+ }
+ },
+ disconnect: () => {
+ for (const listener of closeListeners) {
+ listener();
+ }
+ },
+ connectTunnel: () => Promise.resolve(),
+ disconnectTunnel: () => Promise.resolve(),
getLocation: () =>
Promise.resolve({
ip: '',
@@ -66,38 +68,35 @@ export function newMockIpc() {
longitude: 0.0,
mullvad_exit_ip: false,
}),
-
getState: () =>
Promise.resolve({
state: 'unsecured',
target_state: 'unsecured',
}),
-
- registerStateListener: (listener: (BackendState) => void) => {
+ subscribeStateListener: (listener: (state: ?BackendState, error: ?Error) => void) => {
stateListeners.push(listener);
+ return Promise.resolve();
},
-
sendNewState: (state: BackendState) => {
for (const listener of stateListeners) {
listener(state);
}
},
-
- setCloseConnectionHandler: (listener: () => void) => {
- connectionCloseListeners.push(listener);
+ addOpenConnectionObserver: (listener: () => void) => {
+ openListeners.push(listener);
+ return {
+ unsubscribe: () => {},
+ };
+ },
+ addCloseConnectionObserver: (listener: (error: ?Error) => void) => {
+ closeListeners.push(listener);
+ return {
+ unsubscribe: () => {},
+ };
},
-
authenticate: (_secret: string) => Promise.resolve(),
-
getAccountHistory: () => Promise.resolve([]),
-
removeAccountFromHistory: (_accountToken) => Promise.resolve(),
-
- killWebSocket: () => {
- for (const listener of connectionCloseListeners) {
- listener();
- }
- },
};
return mockIpc;