diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-03-27 15:46:33 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-04-01 13:55:12 +0200 |
| commit | c54a8e8af8ed5e53151ded7f66d9eea7cf7a5d20 (patch) | |
| tree | 6a155396bedff004b0f733ad7fc304b8cc215336 | |
| parent | bcf731749bddebb2eb25ccc6a5b83b45de8fdb0e (diff) | |
| download | mullvadvpn-c54a8e8af8ed5e53151ded7f66d9eea7cf7a5d20.tar.xz mullvadvpn-c54a8e8af8ed5e53151ded7f66d9eea7cf7a5d20.zip | |
Fix tests
| -rw-r--r-- | gui/package.json | 5 | ||||
| -rw-r--r-- | gui/src/main/jsonrpc-client.ts | 70 | ||||
| -rw-r--r-- | gui/src/main/keyframe-animation.ts | 28 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 121 | ||||
| -rw-r--r-- | gui/src/renderer/lib/account-data-cache.ts | 118 | ||||
| -rw-r--r-- | gui/src/renderer/lib/auth-failure.ts | 3 | ||||
| -rw-r--r-- | gui/test/account-data-cache.spec.ts | 41 | ||||
| -rw-r--r-- | gui/test/components/NotificationArea.spec.tsx | 102 | ||||
| -rw-r--r-- | gui/test/jsonrpc-transport.spec.ts | 119 | ||||
| -rw-r--r-- | gui/test/keyframe-animation.spec.ts | 22 | ||||
| -rw-r--r-- | gui/test/setup/main.js (renamed from gui/test/setup/main.ts) | 2 | ||||
| -rw-r--r-- | gui/yarn.lock | 30 |
12 files changed, 341 insertions, 320 deletions
diff --git a/gui/package.json b/gui/package.json index 588dc9d855..431fa7dccd 100644 --- a/gui/package.json +++ b/gui/package.json @@ -18,7 +18,7 @@ "electron-log": "^2.2.8", "gettext-parser": "^3.1.0", "history": "^4.6.1", - "jsonrpc-lite": "^2.0.1", + "jsonrpc-lite": "^2.0.5", "mkdirp": "^0.5.1", "moment": "^2.24.0", "node-gettext": "^2.0.0", @@ -68,7 +68,6 @@ "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.7.0", "gettext-extractor": "^3.4.2", - "mock-socket": "^8.0.5", "npm-run-all": "^4.0.1", "prettier": "1.16.4", "rimraf": "^2.5.4", @@ -87,7 +86,7 @@ "format": "yarn run private:format --write", "check-format": "yarn run private:format --list-different", "develop": "cross-env run-s private:copy-assets private:watch", - "test": "electron-mocha --renderer -R spec --require ts-node/register --require-main ts-node/register --require-main \"test/setup/main.ts\" --preload \"test/setup/renderer.ts\" \"test/*.spec.ts\" \"test/**/*.spec.ts\" \"test/**/*.spec.tsx\" || true", + "test": "cross-env NODE_ENV=test electron-mocha --renderer --reporter spec --require-main \"test/setup/main.js\" --require ts-node/register --require \"test/setup/renderer.ts\" \"test/**/*.{ts,tsx}\"", "update-translations": "node scripts/extract-translations", "pack:mac": "run-s build private:pack:mac private:postbuild:mac", "pack:win": "run-s build private:pack:win", diff --git a/gui/src/main/jsonrpc-client.ts b/gui/src/main/jsonrpc-client.ts index 2d66aad50d..0a6ada62d8 100644 --- a/gui/src/main/jsonrpc-client.ts +++ b/gui/src/main/jsonrpc-client.ts @@ -246,9 +246,8 @@ export default class JsonRpcClient<T> extends EventEmitter { } private onMessage(obj: object) { - let message: any; + let message: ReturnType<typeof jsonrpc.parseObject>; try { - // @ts-ignore message = jsonrpc.parseObject(obj); } catch (error) { log.error(`Failed to parse JSON-RPC message: ${error} for object`); @@ -256,9 +255,9 @@ export default class JsonRpcClient<T> extends EventEmitter { } if (message.type === 'notification') { - this.onNotification(message); + this.onNotification(message as IJsonRpcNotification); } else { - this.onReply(message); + this.onReply(message as (IJsonRpcErrorResponse | IJsonRpcSuccess)); } } @@ -297,7 +296,7 @@ export default class JsonRpcClient<T> extends EventEmitter { } } -interface ITransport<T> { +export interface ITransport<T> { onOpen: () => void; onMessage: (data: object) => void; onClose: (error?: Error) => void; @@ -306,67 +305,6 @@ interface ITransport<T> { connect(params: T): void; } -export class WebsocketTransport implements ITransport<string> { - public ws?: WebSocket; - - constructor(ws?: WebSocket) { - this.ws = ws; - } - public onOpen = () => { - // no-op - }; - public onMessage = (_message: object) => { - // no-op - }; - public onClose = (_error?: Error) => { - // no-op - }; - - public close() { - if (this.ws) { - this.ws.close(); - } - } - - public send(msg: string) { - if (this.ws) { - this.ws.send(msg); - } - } - - public connect(params: string): void { - if (this.ws) { - this.ws.close(); - } - this.ws = new WebSocket(params); - this.ws.onopen = (_event) => { - this.onOpen(); - }; - this.ws.onmessage = (event) => { - try { - const data = event.data; - if (typeof data === 'string') { - const msg = JSON.parse(data); - this.onMessage(msg); - } else { - throw event; - } - } catch (error) { - log.error('Got invalid reply from server: ', error); - } - }; - - this.ws.onclose = (event) => { - log.info(`The websocket connection closed with code: ${event.code}`); - if (event.code === 1000) { - this.onClose(); - } else { - this.onClose(new WebSocketError(event.code)); - } - }; - } -} - // Given the correct parameters, this transport supports named pipes/unix // domain sockets, and also TCP/UDP sockets export class SocketTransport implements ITransport<{ path: string }> { diff --git a/gui/src/main/keyframe-animation.ts b/gui/src/main/keyframe-animation.ts index 25cd83a5db..1a95e1a106 100644 --- a/gui/src/main/keyframe-animation.ts +++ b/gui/src/main/keyframe-animation.ts @@ -13,7 +13,7 @@ export default class KeyframeAnimation { private onFrameValue?: OnFrameFn; private onFinishValue?: OnFinishFn; - private currentFrame: number = 0; + private currentFrameValue: number = 0; private targetFrame: number = 0; private isRunningValue: boolean = false; @@ -21,6 +21,20 @@ export default class KeyframeAnimation { private timeout?: NodeJS.Timeout; + get currentFrame(): number { + return this.currentFrameValue; + } + + // This setter is only meant to be used when running tests + // @internal + set currentFrame(newValue: number) { + if (process.env.NODE_ENV === 'test') { + this.currentFrameValue = newValue; + } else { + throw new Error('The setter for currentFrame is only available in test environment.'); + } + } + set onFrame(newValue: OnFrameFn | undefined) { this.onFrameValue = newValue; } @@ -56,7 +70,7 @@ export default class KeyframeAnimation { const { start, end } = options; if (start !== undefined) { - this.currentFrame = start; + this.currentFrameValue = start; } this.targetFrame = end; @@ -88,7 +102,7 @@ export default class KeyframeAnimation { private render() { if (this.onFrameValue) { - this.onFrameValue(this.currentFrame); + this.onFrameValue(this.currentFrameValue); } } @@ -119,12 +133,12 @@ export default class KeyframeAnimation { return; } - if (this.currentFrame === this.targetFrame) { + if (this.currentFrameValue === this.targetFrame) { this.didFinish(); - } else if (this.currentFrame < this.targetFrame) { - this.currentFrame += 1; + } else if (this.currentFrameValue < this.targetFrame) { + this.currentFrameValue += 1; } else { - this.currentFrame -= 1; + this.currentFrameValue -= 1; } } } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 7d2cd2a69a..54b2743467 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -25,11 +25,11 @@ import { IWindowShapeParameters } from '../main/window-controller'; import { loadTranslations } from '../shared/gettext'; import { IGuiSettingsState } from '../shared/gui-settings-state'; import { IpcRendererEventChannel } from '../shared/ipc-event-channel'; +import AccountDataCache, { AccountFetchRetryAction } from './lib/account-data-cache'; import AccountExpiry from './lib/account-expiry'; import { AccountToken, - IAccountData, ILocation, IRelayList, ISettings, @@ -38,6 +38,8 @@ import { TunnelStateTransition, } from '../shared/daemon-rpc-types'; +type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; + export default class AppRenderer { private memoryHistory = createMemoryHistory(); private reduxStore = configureStore(this.memoryHistory); @@ -597,120 +599,3 @@ export default class AppRenderer { this.reduxActions.settings.updateAutoStart(autoStart); } } - -type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; -export enum AccountFetchRetryAction { - stop, - retry, -} -interface IAccountFetchWatcher { - onFinish: () => void; - onError: (error: Error) => AccountFetchRetryAction; -} - -// An account data cache that helps to throttle RPC requests to get_account_data and retain the -// cached value for 1 minute. -export class AccountDataCache { - private currentAccount?: AccountToken; - private expiresAt?: Date; - private fetchAttempt = 0; - private fetchRetryTimeout?: NodeJS.Timeout; - private watchers: IAccountFetchWatcher[] = []; - - constructor( - private fetchHandler: (token: AccountToken) => Promise<IAccountData>, - private updateHandler: (data?: IAccountData) => void, - ) {} - - public fetch(accountToken: AccountToken, watcher?: IAccountFetchWatcher) { - // invalidate cache if account token has changed - if (accountToken !== this.currentAccount) { - this.invalidate(); - this.currentAccount = accountToken; - } - - // Only fetch is value has expired - if (this.isExpired()) { - if (watcher) { - this.watchers.push(watcher); - } - - this.performFetch(accountToken); - } else if (watcher) { - watcher.onFinish(); - } - } - - public invalidate() { - if (this.fetchRetryTimeout) { - clearTimeout(this.fetchRetryTimeout); - this.fetchRetryTimeout = undefined; - this.fetchAttempt = 0; - } - - this.expiresAt = undefined; - this.updateHandler(); - this.notifyWatchers((watcher) => { - watcher.onError(new Error('Cancelled')); - }); - } - - private setValue(value: IAccountData) { - this.expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration - this.updateHandler(value); - this.notifyWatchers((watcher) => watcher.onFinish()); - } - - private isExpired() { - return !this.expiresAt || this.expiresAt < new Date(); - } - - private async performFetch(accountToken: AccountToken) { - try { - // it's possible for invalidate() to be called or for a fetch for a different account token - // to start before this fetch completes, so checking if the current account token is the one - // used is necessary below. - const accountData = await this.fetchHandler(accountToken); - - if (this.currentAccount === accountToken) { - this.setValue(accountData); - } - } catch (error) { - if (this.currentAccount === accountToken) { - this.handleFetchError(accountToken, error); - } - } - } - - private handleFetchError(accountToken: AccountToken, error: any) { - let shouldRetry = true; - - this.notifyWatchers((watcher) => { - if (watcher.onError(error) === AccountFetchRetryAction.stop) { - shouldRetry = false; - } - }); - - if (shouldRetry) { - this.scheduleRetry(accountToken); - } - } - - private scheduleRetry(accountToken: AccountToken) { - this.fetchAttempt += 1; - - // tslint:disable-next-line - const delay = Math.min(2048, 1 << (this.fetchAttempt + 2)) * 1000; - - log.warn(`Failed to fetch account data. Retrying in ${delay} ms`); - - this.fetchRetryTimeout = global.setTimeout(() => { - this.fetchRetryTimeout = undefined; - this.performFetch(accountToken); - }, delay); - } - - private notifyWatchers(notify: (watcher: IAccountFetchWatcher) => void) { - this.watchers.splice(0).forEach(notify); - } -} diff --git a/gui/src/renderer/lib/account-data-cache.ts b/gui/src/renderer/lib/account-data-cache.ts new file mode 100644 index 0000000000..51154c9792 --- /dev/null +++ b/gui/src/renderer/lib/account-data-cache.ts @@ -0,0 +1,118 @@ +import log from 'electron-log'; +import { AccountToken, IAccountData } from '../../shared/daemon-rpc-types'; + +export enum AccountFetchRetryAction { + stop, + retry, +} +interface IAccountFetchWatcher { + onFinish: () => void; + onError: (error: Error) => AccountFetchRetryAction; +} + +// An account data cache that helps to throttle RPC requests to get_account_data and retain the +// cached value for 1 minute. +export default class AccountDataCache { + private currentAccount?: AccountToken; + private expiresAt?: Date; + private fetchAttempt = 0; + private fetchRetryTimeout?: NodeJS.Timeout; + private watchers: IAccountFetchWatcher[] = []; + + constructor( + private fetchHandler: (token: AccountToken) => Promise<IAccountData>, + private updateHandler: (data?: IAccountData) => void, + ) {} + + public fetch(accountToken: AccountToken, watcher?: IAccountFetchWatcher) { + // invalidate cache if account token has changed + if (accountToken !== this.currentAccount) { + this.invalidate(); + this.currentAccount = accountToken; + } + + // Only fetch is value has expired + if (this.isExpired()) { + if (watcher) { + this.watchers.push(watcher); + } + + this.performFetch(accountToken); + } else if (watcher) { + watcher.onFinish(); + } + } + + public invalidate() { + if (this.fetchRetryTimeout) { + clearTimeout(this.fetchRetryTimeout); + this.fetchRetryTimeout = undefined; + this.fetchAttempt = 0; + } + + this.expiresAt = undefined; + this.updateHandler(); + this.notifyWatchers((watcher) => { + watcher.onError(new Error('Cancelled')); + }); + } + + private setValue(value: IAccountData) { + this.expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration + this.updateHandler(value); + this.notifyWatchers((watcher) => watcher.onFinish()); + } + + private isExpired() { + return !this.expiresAt || this.expiresAt < new Date(); + } + + private async performFetch(accountToken: AccountToken) { + try { + // it's possible for invalidate() to be called or for a fetch for a different account token + // to start before this fetch completes, so checking if the current account token is the one + // used is necessary below. + const accountData = await this.fetchHandler(accountToken); + + if (this.currentAccount === accountToken) { + this.setValue(accountData); + } + } catch (error) { + if (this.currentAccount === accountToken) { + this.handleFetchError(accountToken, error); + } + } + } + + private handleFetchError(accountToken: AccountToken, error: any) { + let shouldRetry = true; + + this.notifyWatchers((watcher) => { + if (watcher.onError(error) === AccountFetchRetryAction.stop) { + shouldRetry = false; + } + }); + + if (shouldRetry) { + this.scheduleRetry(accountToken); + } + } + + private scheduleRetry(accountToken: AccountToken) { + this.fetchAttempt += 1; + + // tslint:disable-next-line + const delay = Math.min(2048, 1 << (this.fetchAttempt + 2)) * 1000; + + log.warn(`Failed to fetch account data. Retrying in ${delay} ms`); + + this.fetchRetryTimeout = global.setTimeout(() => { + this.fetchRetryTimeout = undefined; + this.performFetch(accountToken); + }, delay); + } + + private notifyWatchers(notify: (watcher: IAccountFetchWatcher) => void) { + this.watchers.splice(0).forEach(notify); + } +} diff --git a/gui/src/renderer/lib/auth-failure.ts b/gui/src/renderer/lib/auth-failure.ts index ae381d5677..6428d0f65a 100644 --- a/gui/src/renderer/lib/auth-failure.ts +++ b/gui/src/renderer/lib/auth-failure.ts @@ -18,8 +18,7 @@ export function parseAuthFailure(rawFailureMessage?: string): IAuthFailure { if (results && results.length === 3) { const kind = parseRawFailureKind(results[1]); - const message = - kind === AuthFailureKind.unknown ? rawFailureMessage : messageForFailureKind(kind); + const message = kind === AuthFailureKind.unknown ? results[2] : messageForFailureKind(kind); return { kind, diff --git a/gui/test/account-data-cache.spec.ts b/gui/test/account-data-cache.spec.ts index 33cb80e556..f9838a6dbb 100644 --- a/gui/test/account-data-cache.spec.ts +++ b/gui/test/account-data-cache.spec.ts @@ -1,16 +1,11 @@ -import { AccountDataCache } from '../src/renderer/app'; -import { AccountData } from '../src/shared/daemon-rpc-types'; +import AccountDataCache, { AccountFetchRetryAction } from '../src/renderer/lib/account-data-cache'; +import { IAccountData } from '../src/shared/daemon-rpc-types'; import * as sinon from 'sinon'; -import chai from 'chai'; -import spies from 'chai-spies'; -import chaiAsPromised from 'chai-as-promised'; -import { it, describe, beforeEach, afterEach } from 'mocha'; +import { expect, spy } from 'chai'; -const { expect, spy } = chai; - -describe('AccountData cache', () => { +describe('IAccountData cache', () => { const dummyAccountToken = '9876543210'; - const dummyAccountData: AccountData = { + const dummyAccountData: IAccountData = { expiry: new Date('2038-01-01').toISOString(), }; @@ -35,7 +30,7 @@ describe('AccountData cache', () => { onFinish: () => resolve(), onError: (_error: Error) => { reject(); - return 'stop'; + return AccountFetchRetryAction.stop; }, }); }); @@ -54,7 +49,7 @@ describe('AccountData cache', () => { onFinish: (_reason?: any) => resolve(), onError: (_error: Error) => { reject(); - return 'stop'; + return AccountFetchRetryAction.stop; }, }); }); @@ -70,7 +65,7 @@ describe('AccountData cache', () => { onFinish: spy(), onError: (_error: Error) => { reject(); - return 'stop'; + return AccountFetchRetryAction.stop; }, }); }); @@ -96,7 +91,7 @@ describe('AccountData cache', () => { cache.fetch(dummyAccountToken, { onFinish: () => reject(), - onError: spy((_error: Error) => 'retry'), + onError: spy((_error: Error) => AccountFetchRetryAction.retry), }); }); @@ -123,7 +118,7 @@ describe('AccountData cache', () => { cache.fetch(dummyAccountToken, { onFinish: spy(), - onError: spy((_error: Error) => 'stop'), + onError: spy((_error: Error) => AccountFetchRetryAction.stop), }); }); @@ -131,12 +126,12 @@ describe('AccountData cache', () => { }); it('should cancel first fetch', async () => { - const firstError = spy((_) => 'stop'); + const firstError = spy((_error: Error) => AccountFetchRetryAction.stop); const secondSuccess = spy(); - const update = new Promise((resolve, reject) => { + const update = new Promise<IAccountData>((resolve, reject) => { let firstAttempt = true; - const fetch = () => { + const fetch = (_token: string) => { if (firstAttempt) { firstAttempt = false; @@ -144,18 +139,22 @@ describe('AccountData cache', () => { onFinish: secondSuccess, onError: () => { reject(); - return 'stop'; + return AccountFetchRetryAction.stop; }, }); - return new Promise((resolve) => setTimeout(() => resolve(dummyAccountData), 1000)); + return new Promise<IAccountData>((resolve) => { + setTimeout(() => resolve(dummyAccountData), 1000); + }); } else { reject(); return Promise.resolve(dummyAccountData); } }; - const cache = new AccountDataCache(fetch, () => resolve()); + const cache = new AccountDataCache(fetch, (_accountData?: IAccountData) => { + resolve(); + }); setTimeout(resolve, 12000); diff --git a/gui/test/components/NotificationArea.spec.tsx b/gui/test/components/NotificationArea.spec.tsx index 54aae1f8a6..c117db914e 100644 --- a/gui/test/components/NotificationArea.spec.tsx +++ b/gui/test/components/NotificationArea.spec.tsx @@ -2,7 +2,9 @@ import moment from 'moment'; import * as React from 'react'; import { shallow } from 'enzyme'; import NotificationArea from '../../src/renderer/components/NotificationArea'; +import { AfterDisconnect } from '../../src/shared/daemon-rpc-types'; import AccountExpiry from '../../src/renderer/lib/account-expiry'; +import { expect } from 'chai'; describe('components/NotificationArea', () => { const defaultVersion = { @@ -12,7 +14,6 @@ describe('components/NotificationArea', () => { current: '2018.2', latest: '2018.2-beta1', latestStable: '2018.2', - nextUpgrade: null, }; const defaultExpiry = new AccountExpiry( @@ -23,35 +24,90 @@ describe('components/NotificationArea', () => { ); it('handles disconnecting state', () => { - for (const reason of ['nothing', 'block', 'reconnect']) { + for (const reason of ['nothing', 'block'] as AfterDisconnect[]) { const component = shallow( <NotificationArea tunnelState={{ state: 'disconnecting', - details: { reason }, + details: reason, }} version={defaultVersion} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); expect(component.state('visible')).to.be.false; } }); - it('handles connected or disconnected states', () => { - for (const state of ['connected', 'disconnected']) { - const component = shallow( - <NotificationArea - tunnelState={{ - state, - }} - version={defaultVersion} - accountExpiry={defaultExpiry} - />, - ); + it('handles disconnecting state when reconnecting', () => { + const component = shallow( + <NotificationArea + tunnelState={{ + state: 'disconnecting', + details: 'reconnect', + }} + version={defaultVersion} + accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} + />, + ); + expect(component.state('visible')).to.be.true; + }); - expect(component.state('visible')).to.be.false; - } + it('handles connected state', () => { + const component = shallow( + <NotificationArea + tunnelState={{ + state: 'connected', + details: { + address: '1.2.3.4', + protocol: 'tcp', + tunnel: 'openvpn', + }, + }} + version={defaultVersion} + accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} + />, + ); + + expect(component.state('visible')).to.be.false; + }); + + it('handles disconnected state', () => { + const component = shallow( + <NotificationArea + tunnelState={{ + state: 'disconnected', + }} + version={defaultVersion} + accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} + />, + ); + + expect(component.state('visible')).to.be.false; + }); + + it('handles disconnected state, blocking when connected', () => { + const component = shallow( + <NotificationArea + tunnelState={{ + state: 'disconnected', + }} + version={defaultVersion} + accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={true} + />, + ); + + expect(component.state('visible')).to.be.true; }); it('handles connecting state', () => { @@ -62,6 +118,8 @@ describe('components/NotificationArea', () => { }} version={defaultVersion} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); @@ -80,6 +138,8 @@ describe('components/NotificationArea', () => { }} version={defaultVersion} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); @@ -98,6 +158,8 @@ describe('components/NotificationArea', () => { consistent: false, }} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); @@ -119,6 +181,8 @@ describe('components/NotificationArea', () => { nextUpgrade: '2018.2', }} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); @@ -141,6 +205,8 @@ describe('components/NotificationArea', () => { nextUpgrade: '2018.3', }} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); @@ -164,6 +230,8 @@ describe('components/NotificationArea', () => { nextUpgrade: '2018.4-beta3', }} accountExpiry={defaultExpiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); @@ -186,6 +254,8 @@ describe('components/NotificationArea', () => { }} version={defaultVersion} accountExpiry={expiry} + openExternalLink={() => {}} + blockWhenDisconnected={false} />, ); diff --git a/gui/test/jsonrpc-transport.spec.ts b/gui/test/jsonrpc-transport.spec.ts index 7e4e56c80a..26d812b793 100644 --- a/gui/test/jsonrpc-transport.spec.ts +++ b/gui/test/jsonrpc-transport.spec.ts @@ -1,56 +1,49 @@ import { expect } from 'chai'; -import { it, describe, beforeEach, afterEach } from 'mocha'; +import { it, describe, beforeEach } from 'mocha'; import jsonrpc from 'jsonrpc-lite'; -import { Server } from 'mock-socket'; -import JsonRpcClient, { WebsocketTransport, TimeOutError } from '../src/main/jsonrpc-client'; +import JsonRpcClient, { ITransport, TimeOutError } from '../src/main/jsonrpc-client'; describe('JSON RPC transport', () => { - const WEBSOCKET_URL = 'ws://localhost:8080'; - let server: Server, transport: JsonRpcClient<string>; + let client: JsonRpcClient<string>, transport: MockTransport; beforeEach(() => { - server = new Server(WEBSOCKET_URL); - transport = new JsonRpcClient(new WebsocketTransport()); - }); - - afterEach(() => { - server.close(); + transport = new MockTransport(); + client = new JsonRpcClient(transport); + return client.connect(''); }); it('should reject failed jsonrpc requests', async () => { - server.on('connection', (socket) => { - socket.on('message', (msg) => { - const { payload } = jsonrpc.parse(msg); - if (payload.method === 'invalid-method') { - socket.send( - JSON.stringify( - jsonrpc.error(payload.id, new jsonrpc.JsonRpcError('Method not found', -32601)), + transport.onServerMessage = (msg) => { + const parsedMessage = jsonrpc.parseObject(msg); + + if (parsedMessage.type === 'request' && parsedMessage.payload.method === 'invalid-method') { + transport.reply( + JSON.stringify( + jsonrpc.error( + parsedMessage.payload.id, + new jsonrpc.JsonRpcError('Method not found', -32601), ), - ); - } - }); - }); + ), + ); + } + }; - await transport.connect(WEBSOCKET_URL); - const sendPromise = transport.send('invalid-method'); + const sendPromise = client.send('invalid-method'); return expect(sendPromise).to.eventually.be.rejectedWith('Method not found'); }); it('should route reply to correct promise', async () => { - server.on('connection', (socket) => { - socket.on('message', (msg) => { - const { payload } = jsonrpc.parse(msg); - if (payload.method === 'a message') { - socket.send(JSON.stringify(jsonrpc.success(payload.id, 'a reply'))); - } - }); - }); + transport.onServerMessage = (msg) => { + const parsedMessage = jsonrpc.parseObject(msg); - await transport.connect(WEBSOCKET_URL); + if (parsedMessage.type === 'request' && parsedMessage.payload.method === 'a message') { + transport.reply(JSON.stringify(jsonrpc.success(parsedMessage.payload.id, 'a reply'))); + } + }; - const decoyPromise = transport.send('a decoy', [], 100); - const messagePromise = transport.send('a message', [], 100); + const decoyPromise = client.send('a decoy', [], 100); + const messagePromise = client.send('a message', [], 100); return Promise.all([ expect(messagePromise).to.eventually.be.equal('a reply'), @@ -59,26 +52,22 @@ describe('JSON RPC transport', () => { }); it('should timeout if no response is returned', async () => { - await transport.connect(WEBSOCKET_URL); - const sendPromise = transport.send('timeout-message', {}, 1); + const sendPromise = client.send('timeout-message', {}, 1); return expect(sendPromise).to.eventually.be.rejectedWith(TimeOutError, 'Request timed out'); }); it('should route notifications', async () => { - server.on('connection', (socket) => { - socket.on('message', (msg) => { - const { payload } = jsonrpc.parse(msg); - if (payload.method === 'event_subscribe') { - socket.send(JSON.stringify(jsonrpc.success(payload.id, 1))); - } - }); - }); + transport.onServerMessage = (msg) => { + const parsedMessage = jsonrpc.parseObject(msg); - await transport.connect(WEBSOCKET_URL); + if (parsedMessage.type === 'request' && parsedMessage.payload.method === 'event_subscribe') { + transport.reply(JSON.stringify(jsonrpc.success(parsedMessage.payload.id, 1))); + } + }; const eventPromiseHelper = (() => { - let borrowedResolve: (param: any) => void | undefined = undefined; + let borrowedResolve: ((param: any) => void) | undefined = undefined; const promise = new Promise((resolve) => (borrowedResolve = resolve)); /* Flow does not understand that the body of Promise runs immediately. see https://github.com/facebook/flow/issues/6711 */ @@ -91,13 +80,43 @@ describe('JSON RPC transport', () => { }; })(); - await transport.subscribe('event', eventPromiseHelper.resolve); + await client.subscribe('event', eventPromiseHelper.resolve); - server.emit( - 'message', + transport.reply( JSON.stringify(jsonrpc.notification('event', { subscription: 1, result: 'beacon' })), ); return expect(eventPromiseHelper.promise).to.eventually.be.equal('beacon'); }); }); + +class MockTransport implements ITransport<string> { + public onOpen = () => { + // no-op + }; + public onMessage = (_message: object) => { + // no-op + }; + public onServerMessage = (_message: object) => { + // no-op + }; + public onClose = (_error?: Error) => { + // no-op + }; + + public close() { + this.onClose(); + } + + public send(msg: string) { + this.onServerMessage(JSON.parse(msg)); + } + + public reply(msg: string) { + this.onMessage(JSON.parse(msg)); + } + + public connect(_params: string) { + this.onOpen(); + } +} diff --git a/gui/test/keyframe-animation.spec.ts b/gui/test/keyframe-animation.spec.ts index 05efa6ad76..11eeaf0c54 100644 --- a/gui/test/keyframe-animation.spec.ts +++ b/gui/test/keyframe-animation.spec.ts @@ -19,7 +19,7 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([0, 1, 2, 3, 4]); - expect(animation._currentFrame).to.be.equal(4); + expect(animation.currentFrame).to.be.equal(4); done(); }; @@ -34,7 +34,7 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([3]); - expect(animation._currentFrame).to.be.equal(3); + expect(animation.currentFrame).to.be.equal(3); done(); }; @@ -49,7 +49,7 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([2, 3, 4]); - expect(animation._currentFrame).to.be.equal(4); + expect(animation.currentFrame).to.be.equal(4); done(); }; @@ -64,7 +64,7 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([4, 3, 2]); - expect(animation._currentFrame).to.be.equal(2); + expect(animation.currentFrame).to.be.equal(2); done(); }; @@ -79,11 +79,11 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([0, 1, 2, 3, 4]); - expect(animation._currentFrame).to.be.equal(4); + expect(animation.currentFrame).to.be.equal(4); done(); }; - animation._currentFrame = 0; + animation.currentFrame = 0; animation.play({ end: 4 }); }); @@ -95,11 +95,11 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([4, 3, 2]); - expect(animation._currentFrame).to.be.equal(2); + expect(animation.currentFrame).to.be.equal(2); done(); }; - animation._currentFrame = 4; + animation.currentFrame = 4; animation.play({ end: 2 }); }); @@ -111,11 +111,11 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([4, 3, 2, 1]); - expect(animation._currentFrame).to.be.equal(1); + expect(animation.currentFrame).to.be.equal(1); done(); }; - animation._currentFrame = 4; + animation.currentFrame = 4; animation.play({ end: 1 }); }); @@ -127,7 +127,7 @@ describe('lib/keyframe-animation', function() { }; animation.onFinish = () => { expect(seq).to.be.deep.equal([4, 3, 2, 1, 0]); - expect(animation._currentFrame).to.be.equal(0); + expect(animation.currentFrame).to.be.equal(0); done(); }; diff --git a/gui/test/setup/main.ts b/gui/test/setup/main.js index 9b6ecc5ef7..dd458a30b6 100644 --- a/gui/test/setup/main.ts +++ b/gui/test/setup/main.js @@ -1,4 +1,4 @@ -import log from 'electron-log'; +const log = require('electron-log'); log.transports.console.level = false; log.transports.file.level = false; diff --git a/gui/yarn.lock b/gui/yarn.lock index d4d0c9cf86..d4d6650fb2 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -3067,10 +3067,10 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsonrpc-lite@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/jsonrpc-lite/-/jsonrpc-lite-2.0.4.tgz#a8f8e9db2830d1a383d21ee97bd259c3321cff25" - integrity sha512-vr78eFnTrluTljM3lEydW9qDclgfGGpRoRDe1y/0i650oSx5fiTefW3PwDMUQ/+uJN76wG7yGPwTh0BVFaBP4Q== +jsonrpc-lite@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/jsonrpc-lite/-/jsonrpc-lite-2.0.5.tgz#00d60b5ab0f1a81e19f7806ca9c236dad66bb5ff" + integrity sha512-3+WESfAxrlU7//u8qS+UeKYjkeTxcQuGwASwOwNdAjWU9lFCvo1/2yceKE2IXDPpL1YIDLZyBshEcu6cfTMZaA== jsprim@^1.2.2: version "1.4.1" @@ -3474,13 +3474,6 @@ mocha@^5.2.0: mkdirp "0.5.1" supports-color "5.4.0" -mock-socket@^8.0.5: - version "8.0.5" - resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-8.0.5.tgz#4ce8909601b2bcdf5f7680f35c2f7b34beb2afc4" - integrity sha512-dE2EbcxJKQCeYLZSsI7BAiMZCe/bHbJ2LHb5aGwUuDmfoOINEJ8QI6qYJ85NHsSNkNa90F3s6onZcmt/+MppFA== - dependencies: - url-parse "^1.2.0" - moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" @@ -4254,11 +4247,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -querystringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755" - integrity sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw== - quickselect@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2" @@ -4628,7 +4616,7 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -requires-port@1.x.x, requires-port@^1.0.0: +requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= @@ -5587,14 +5575,6 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" -url-parse@^1.2.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.3.tgz#bfaee455c889023219d757e045fa6a684ec36c15" - integrity sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw== - dependencies: - querystringify "^2.0.0" - requires-port "^1.0.0" - use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" |
