diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-03 13:39:18 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-07-03 13:39:18 +0200 |
| commit | 38968509b59329f2d0743312fc8f9a4f98daa66c (patch) | |
| tree | f6e5f8b4f8dc72ae1bae0ddfe715b8094deb3750 /test | |
| parent | be096ee87bb1256b67c3b24b61be77d523aff9cc (diff) | |
| parent | b3a6f58d7027e5961180b52d6473f7f504638284 (diff) | |
| download | mullvadvpn-38968509b59329f2d0743312fc8f9a4f98daa66c.tar.xz mullvadvpn-38968509b59329f2d0743312fc8f9a4f98daa66c.zip | |
Merge branch 'app-overhaul'
Diffstat (limited to 'test')
| -rw-r--r-- | test/auth.spec.js | 37 | ||||
| -rw-r--r-- | test/autologin.spec.js | 126 | ||||
| -rw-r--r-- | test/components/Login.spec.js | 104 | ||||
| -rw-r--r-- | test/connect.spec.js | 62 | ||||
| -rw-r--r-- | test/helpers/IpcChain.js | 100 | ||||
| -rw-r--r-- | test/helpers/ipc-helpers.js | 104 | ||||
| -rw-r--r-- | test/ipc.spec.js | 134 | ||||
| -rw-r--r-- | test/jsonrpc-transport.spec.js | 144 | ||||
| -rw-r--r-- | test/login.spec.js | 83 | ||||
| -rw-r--r-- | test/logout.spec.js | 67 | ||||
| -rw-r--r-- | test/mocks/redux.js | 4 | ||||
| -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; |
