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:
parent
a92b955304
commit
90200ed2f1
@ -11,6 +11,6 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
...base.rules,
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
}
|
||||
};
|
||||
|
||||
@ -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'"
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}'`);
|
||||
|
||||
60
packages/apps-electron/src/main/account-store.spec.ts
Normal file
60
packages/apps-electron/src/main/account-store.spec.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
@ -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));
|
||||
};
|
||||
|
||||
7
packages/apps-electron/src/main/ipc-main-handler.ts
Normal file
7
packages/apps-electron/src/main/ipc-main-handler.ts
Normal 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
|
||||
}
|
||||
14
packages/apps-electron/src/main/register-ipc-handler.ts
Normal file
14
packages/apps-electron/src/main/register-ipc-handler.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user