Tests for electron app storage (#3118)

* Add unit tests for electron account management

* Use tmp package in file store tests.

* Enable @typescript-eslint/no-useless-constructor extension eslint rule.

* Explicitly assign private field in constructor.

Co-authored-by: Krzysztof Jelski <krzysztof@ethworks.io>
This commit is contained in:
Ivan Rukhavets 2020-07-07 15:00:02 +03:00 committed by GitHub
parent a92b955304
commit 90200ed2f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 233 additions and 27 deletions

View File

@ -11,6 +11,6 @@ module.exports = {
},
rules: {
...base.rules,
'@typescript-eslint/no-explicit-any': 'off'
'@typescript-eslint/no-explicit-any': 'off',
}
};

View File

@ -70,7 +70,7 @@
"lint:css": "stylelint './packages/**/src/**/*.tsx'",
"postinstall": "polkadot-dev-yarn-only",
"postinstall:electron": "electron-builder install-app-deps",
"test": "polkadot-dev-run-test packages/page-claims/src",
"test": "polkadot-dev-run-test packages/page-claims/src packages/apps-electron",
"test:one": "polkadot-dev-run-test",
"start": "yarn clean && cd packages/apps && webpack --config webpack.config.js",
"start:electron": "yarn clean:electronBuild && concurrently 'yarn build:devElectronMain && cd packages/apps-electron && electron ./build/electron.js' 'yarn build:devElectronRenderer'"

View File

@ -14,12 +14,14 @@
"devDependencies": {
"@types/react": "^16.9.41",
"@types/react-dom": "^16.9.8",
"@types/tmp": "^0.2.0",
"babel-loader": "^8.1.0",
"copy-webpack-plugin": "^6.0.3",
"html-webpack-plugin": "^4.3.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"thread-loader": "^2.1.3",
"tmp": "^0.2.1",
"webpack": "^4.43.0"
}
}

View File

@ -18,13 +18,14 @@ import { BlockAuthors, Events } from '@polkadot/react-query';
import AccountSidebar from '@polkadot/app-accounts/Sidebar';
import { Api } from '@polkadot/react-api';
import Apps from '@polkadot/apps/Apps';
import { electronMainApi } from './api/global-exported-api';
import { RemoteElectronStore } from './renderer/remote-electron-store';
const rootId = 'root';
const rootElement = document.getElementById(rootId);
const theme = { theme: settings.uiTheme };
const store = new RemoteElectronStore();
const store = new RemoteElectronStore(electronMainApi.accountStore);
if (!rootElement) {
throw new Error(`Unable to find element with id '${rootId}'`);

View File

@ -0,0 +1,60 @@
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { FileStore } from '@polkadot/ui-keyring/stores';
import { accountStoreIpcHandler } from './account-store';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { IpcMainHandler } from './ipc-main-handler';
import * as tmp from 'tmp';
const exampleAccount = (address: string): KeyringJson => ({
address,
meta: {}
});
describe('Account store', () => {
let accountStore: IpcMainHandler;
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
accountStore = accountStoreIpcHandler(new FileStore(tmpDir.name));
});
afterEach(() => {
tmpDir.removeCallback();
});
it('all returns empty array at first', () => {
expect(accountStore['account-store-all']()).toEqual([]);
});
it('after adding accounts, they become visible', async () => {
await accountStore['account-store-set']('1', exampleAccount('a'));
await accountStore['account-store-set']('2', exampleAccount('b'));
expect(accountStore['account-store-all']()).toEqual([{
key: '1', value: exampleAccount('a')
}, {
key: '2', value: exampleAccount('b')
}]);
});
it('get returns account if exists', async () => {
await accountStore['account-store-set']('1', exampleAccount('a'));
expect(await accountStore['account-store-get']('1')).toEqual(exampleAccount('a'));
});
it('get returns null if account does not exist', async () => {
expect(await accountStore['account-store-get']('1')).toEqual(null);
});
it('account disappears from list after it is removed', async () => {
await accountStore['account-store-set']('1', exampleAccount('a'));
await accountStore['account-store-remove']('1');
expect(await accountStore['account-store-get']('1')).toEqual(null);
expect(accountStore['account-store-all']()).toEqual([]);
});
});

View File

@ -2,30 +2,18 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { app } from 'electron';
import { FileStore } from '@polkadot/ui-keyring/stores';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { app, ipcMain } from 'electron';
import path from 'path';
import { IpcMainHandler } from './ipc-main-handler';
import { registerIpcHandler } from './register-ipc-handler';
const ACCOUNTS_SUBFOLDER = 'polkadot-accounts';
export const registerAccountStoreHandlers = (): void => {
const defaultStorePath = path.join(app.getPath('userData'), ACCOUNTS_SUBFOLDER);
const fileStore = new FileStore(defaultStorePath);
ipcMain.handle('account-store-set', async (_, key: string, value: KeyringJson) => new Promise((resolve) =>
fileStore.set(key, value, resolve)
));
ipcMain.handle('account-store-get', async (_, key: string) => new Promise((resolve) =>
fileStore.get(key, resolve)
));
ipcMain.handle('account-store-remove', async (_, key: string) => new Promise((resolve) =>
fileStore.remove(key, resolve)
));
ipcMain.handle('account-store-all', () => {
export const accountStoreIpcHandler = (fileStore: FileStore): IpcMainHandler => ({
'account-store-all': () => {
let result: { key: string, value: KeyringJson }[] = [];
const collect = (key: string, value: KeyringJson) => {
@ -35,5 +23,25 @@ export const registerAccountStoreHandlers = (): void => {
fileStore.all(collect);
return result;
});
},
'account-store-get': async (key: string) => new Promise((resolve) => {
try {
fileStore.get(key, resolve);
} catch (err) {
resolve(null);
}
}),
'account-store-remove': async (key: string) => new Promise((resolve) =>
fileStore.remove(key, resolve)
),
'account-store-set': async (key: string, value: KeyringJson) => new Promise((resolve) =>
fileStore.set(key, value, resolve)
)
});
export const registerAccountStoreHandlers = (): void => {
const defaultStorePath = path.join(app.getPath('userData'), ACCOUNTS_SUBFOLDER);
const fileStore = new FileStore(defaultStorePath);
registerIpcHandler(accountStoreIpcHandler(fileStore));
};

View File

@ -0,0 +1,7 @@
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export type IpcMainHandler = {
[channel: string]: (...args: any[]) => Promise<void> | any
}

View File

@ -0,0 +1,14 @@
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { ipcMain } from 'electron';
import { IpcMainHandler } from './ipc-main-handler';
export const registerIpcHandler = (ipcHandler: IpcMainHandler): void => {
for (const [channel, listener] of Object.entries(ipcHandler)) {
ipcMain.handle(channel, (_, ...args): void => {
listener(...args);
});
}
};

View File

@ -0,0 +1,90 @@
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { RemoteElectronStore } from './remote-electron-store';
describe('Remote Electron Store', () => {
const accountStore = {
all: jest.fn(),
get: jest.fn(),
remove: jest.fn(),
set: jest.fn()
};
const remoteStore = new RemoteElectronStore(accountStore);
beforeEach(() => {
accountStore.all.mockClear();
accountStore.get.mockClear();
accountStore.remove.mockClear();
accountStore.set.mockClear();
});
describe('all', () => {
it('calls callback for each returned account', async () => {
accountStore.all.mockResolvedValue([{
key: 1,
value: 'a'
}, {
key: 2,
value: 'b'
}]);
const cb = jest.fn();
remoteStore.all(cb);
await Promise.resolve();
expect(cb).nthCalledWith(1, 1, 'a');
expect(cb).nthCalledWith(2, 2, 'b');
});
});
describe('get', () => {
it('calls callback with returned account', async () => {
accountStore.get.mockResolvedValue('a');
const cb = jest.fn();
remoteStore.get('1', cb);
await Promise.resolve();
expect(accountStore.get).toBeCalledWith('1');
expect(cb).toBeCalledWith('a');
});
it('calls callback with null if no accounts found', async () => {
accountStore.get.mockResolvedValue(null);
const cb = jest.fn();
remoteStore.get('1', cb);
await Promise.resolve();
expect(cb).toBeCalledWith(null);
});
});
describe('remove', () => {
it('calls callback after success', async () => {
accountStore.remove.mockResolvedValue(null);
const cb = jest.fn();
remoteStore.remove('1', cb);
await Promise.resolve();
expect(accountStore.remove).toBeCalledWith('1');
expect(cb).toBeCalledTimes(1);
});
});
describe('set', () => {
it('calls callback after success', async () => {
accountStore.set.mockResolvedValue(null);
const cb = jest.fn();
remoteStore.set('1', 'a' as any, cb);
await Promise.resolve();
expect(accountStore.set).toBeCalledWith('1', 'a');
expect(cb).toBeCalledTimes(1);
});
});
});

View File

@ -3,11 +3,17 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringJson, KeyringStore } from '@polkadot/ui-keyring/types';
import { electronMainApi } from '../api/global-exported-api';
import { AccountStoreApi } from '../api/account-store-api';
export class RemoteElectronStore implements KeyringStore {
readonly #accountStore: AccountStoreApi;
constructor (accountStore: AccountStoreApi) {
this.#accountStore = accountStore;
}
all (cb: (key: string, value: KeyringJson) => void): void {
electronMainApi.accountStore.all()
this.#accountStore.all()
.then((result: { key: string, value: KeyringJson }[]) => result.forEach(({ key, value }) => cb(key, value)))
.catch(() => {
throw new Error('error getting all accounts');
@ -15,20 +21,20 @@ export class RemoteElectronStore implements KeyringStore {
}
get (key: string, cb: (value: KeyringJson) => void): void {
electronMainApi.accountStore.get(key)
this.#accountStore.get(key)
.then(cb).catch(() => {
throw new Error('error storing account');
});
}
remove (key: string, cb: (() => void) | undefined): void {
electronMainApi.accountStore.remove(key).then(cb).catch(() => {
this.#accountStore.remove(key).then(cb).catch(() => {
throw new Error('error removing account');
});
}
set (key: string, value: KeyringJson, cb: (() => void) | undefined): void {
electronMainApi.accountStore.set(key, value).then(cb).catch(() => {
this.#accountStore.set(key, value).then(cb).catch(() => {
throw new Error('error saving account');
});
}

View File

@ -3228,6 +3228,7 @@ __metadata:
"@polkadot/react-components": 0.49.0-beta.1
"@types/react": ^16.9.41
"@types/react-dom": ^16.9.8
"@types/tmp": ^0.2.0
babel-loader: ^8.1.0
copy-webpack-plugin: ^6.0.3
electron-log: ^4.2.2
@ -3236,6 +3237,7 @@ __metadata:
react: ^16.13.1
react-dom: ^16.13.1
thread-loader: ^2.1.3
tmp: ^0.2.1
webpack: ^4.43.0
languageName: unknown
linkType: soft
@ -4303,6 +4305,13 @@ __metadata:
languageName: node
linkType: hard
"@types/tmp@npm:^0.2.0":
version: 0.2.0
resolution: "@types/tmp@npm:0.2.0"
checksum: 3/8b3d8ce993db6129112690d2fc4a8615c8073bd47a3dbcb3384563077f6081c560912aa6c53bb46ec4b45fca1e26c2f811273534acdafdcc224fc49a8bd85467
languageName: node
linkType: hard
"@types/uglify-js@npm:*":
version: 3.9.2
resolution: "@types/uglify-js@npm:3.9.2"
@ -22523,6 +22532,15 @@ __metadata:
languageName: node
linkType: hard
"tmp@npm:^0.2.1":
version: 0.2.1
resolution: "tmp@npm:0.2.1"
dependencies:
rimraf: ^3.0.0
checksum: 3/13973825ff1c7aed3359bba97c146c860ebb5b1cbdca88387a2ff8bae704d2478b701cc3adc29b1461be292fed1e4ae56b378b6a0386bbab471ef32860e0a711
languageName: node
linkType: hard
"tmpl@npm:1.0.x":
version: 1.0.4
resolution: "tmpl@npm:1.0.4"