diff --git a/.eslintrc.js b/.eslintrc.js index a75ca6fdc7..f4dd8b2a6b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,6 +11,6 @@ module.exports = { }, rules: { ...base.rules, - '@typescript-eslint/no-explicit-any': 'off' + '@typescript-eslint/no-explicit-any': 'off', } }; diff --git a/package.json b/package.json index 7417198055..117123bade 100644 --- a/package.json +++ b/package.json @@ -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'" diff --git a/packages/apps-electron/package.json b/packages/apps-electron/package.json index 98ca827018..da66926e62 100644 --- a/packages/apps-electron/package.json +++ b/packages/apps-electron/package.json @@ -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" } } diff --git a/packages/apps-electron/src/index.tsx b/packages/apps-electron/src/index.tsx index e03cfbb37e..827a37f1cc 100644 --- a/packages/apps-electron/src/index.tsx +++ b/packages/apps-electron/src/index.tsx @@ -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}'`); diff --git a/packages/apps-electron/src/main/account-store.spec.ts b/packages/apps-electron/src/main/account-store.spec.ts new file mode 100644 index 0000000000..d6f754ef59 --- /dev/null +++ b/packages/apps-electron/src/main/account-store.spec.ts @@ -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([]); + }); +}); diff --git a/packages/apps-electron/src/main/account-store.ts b/packages/apps-electron/src/main/account-store.ts index 419fe6d4c0..d842b52092 100644 --- a/packages/apps-electron/src/main/account-store.ts +++ b/packages/apps-electron/src/main/account-store.ts @@ -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)); }; diff --git a/packages/apps-electron/src/main/ipc-main-handler.ts b/packages/apps-electron/src/main/ipc-main-handler.ts new file mode 100644 index 0000000000..9b752118b6 --- /dev/null +++ b/packages/apps-electron/src/main/ipc-main-handler.ts @@ -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 | any +} diff --git a/packages/apps-electron/src/main/register-ipc-handler.ts b/packages/apps-electron/src/main/register-ipc-handler.ts new file mode 100644 index 0000000000..938657bf72 --- /dev/null +++ b/packages/apps-electron/src/main/register-ipc-handler.ts @@ -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); + }); + } +}; diff --git a/packages/apps-electron/src/renderer/remote-electron-store.spec.ts b/packages/apps-electron/src/renderer/remote-electron-store.spec.ts new file mode 100644 index 0000000000..c546c67cef --- /dev/null +++ b/packages/apps-electron/src/renderer/remote-electron-store.spec.ts @@ -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); + }); + }); +}); diff --git a/packages/apps-electron/src/renderer/remote-electron-store.ts b/packages/apps-electron/src/renderer/remote-electron-store.ts index 0f62112427..879ecadac5 100644 --- a/packages/apps-electron/src/renderer/remote-electron-store.ts +++ b/packages/apps-electron/src/renderer/remote-electron-store.ts @@ -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'); }); } diff --git a/yarn.lock b/yarn.lock index d2bfcc0509..acd9dfcd22 100644 --- a/yarn.lock +++ b/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"