Export account (#166)

* setup tests

* export account WIP

* Fix linter errors.

* Frontend test for Export component.

* Test for account export by background handler.

* Add password to export.

* Add simple account export functionality.

* Add dedicated selectors.

* Improve export view layout

* make export textarea bigger

* Fix linter error.

* Update wrapper in test to make it pass.

* test refactoring

* add copying exported JSON to clipboard

* lint fix

* add UI tests

* fix dependencies

* refactoring

* copyright headers; minor refactorings

* make input field blink after entering wrong password

* download exported JSON

* simplify exported JSON download

* removed redundant tests

* disable export functionality for external accounts

* add export restrictions tests

* tests refactoring

* after successful export go back to main screen
This commit is contained in:
Bartłomiej Rutkowski 2019-10-04 17:03:36 +02:00 committed by Jaco Greeff
parent a9b405ca73
commit 29122d1f8f
17 changed files with 431 additions and 9 deletions

View File

@ -0,0 +1,31 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
/* eslint-disable @typescript-eslint/no-explicit-any */
import chrome from 'sinon-chrome';
class MessagingFake {
private listeners: any = [];
get onMessage (): any {
return {
addListener: (cb: any): any => this.listeners.push(cb)
};
}
get onDisconnect (): any {
return {
addListener: (): any => {}
};
}
postMessage (data: any): void {
this.listeners.forEach((cb: any) => cb.call(this, data));
}
}
const messagingFake = new MessagingFake();
chrome.runtime.connect.returns(messagingFake);
export default chrome;

View File

@ -1,3 +1,3 @@
module.exports = {
extends: '@polkadot/dev/config/babel'
extends: '@polkadot/dev-react/config/babel'
};

View File

@ -4,11 +4,15 @@ const config = require('@polkadot/dev/config/jest');
module.exports = Object.assign({}, config, {
moduleNameMapper: {
'@polkadot/extension-(chains|dapp|inject|ui)(.*)$': '<rootDir>/packages/extension-$1/src/$2',
'@polkadot/extension(.*)$': '<rootDir>/packages/extension/src/$1',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
'\\.(css|less)$': 'empty/object'
},
modulePathIgnorePatterns: [
'<rootDir>/packages/extension/build',
'<rootDir>/packages/extension-ui/build'
'<rootDir>/packages/extension-ui/build',
'<rootDir>/packages/extension-chains/build',
'<rootDir>/packages/extension-inject/build',
'<rootDir>/packages/extension-dapp/build'
]
});

View File

@ -28,7 +28,7 @@
"clean": "polkadot-dev-clean-build",
"postinstall": "polkadot-dev-yarn-only",
"start": "yarn watch",
"test": "echo \"no tests... yet\"",
"test": "jest",
"watch": "cd packages/extension && webpack --config webpack.config.js --watch"
},
"devDependencies": {

View File

@ -12,5 +12,11 @@
"@types/react-router-dom": "^5.1.0",
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0"
},
"devDependencies": {
"@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/sinon-chrome": "^2.2.6",
"sinon-chrome": "^3.0.1"
}
}

View File

@ -0,0 +1,42 @@
// Copyright 2019 @polkadot/extension-ui 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 Adapter from 'enzyme-adapter-react-16';
import { configure, mount, ReactWrapper } from 'enzyme';
import { Link } from '@polkadot/extension-ui/components';
import { MemoryRouter } from 'react-router';
import React from 'react';
import Account from './Account';
configure({ adapter: new Adapter() });
describe('Account component', () => {
let wrapper: ReactWrapper;
const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mountAccountComponent = (additionalAccountProperties: any): ReactWrapper => mount(
<MemoryRouter>
<Account
{...{ address: VALID_ADDRESS, ...additionalAccountProperties }}>
</Account>
</MemoryRouter>);
it('shows Export option if account is not external', () => {
wrapper = mountAccountComponent({ isExternal: false });
expect(wrapper.find(Link).length).toBe(3);
expect(wrapper.find(Link).at(0).text()).toContain('Edit');
expect(wrapper.find(Link).at(1).text()).toContain('Export');
expect(wrapper.find(Link).at(2).text()).toContain('Forget');
});
it('does not show Export option if account is external', () => {
wrapper = mountAccountComponent({ isExternal: true });
expect(wrapper.find(Link).length).toBe(2);
expect(wrapper.find(Link).at(0).text()).toContain('Edit');
expect(wrapper.find(Link).at(1).text()).toContain('Forget');
});
});

View File

@ -16,7 +16,7 @@ interface Props extends AccountJson {
className?: string;
}
function Account ({ address, className }: Props): React.ReactElement<Props> {
function Account ({ address, className, isExternal }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
const [isEditing, setEditing] = useState(false);
const [editedName, setName] = useState<string | null>(null);
@ -50,6 +50,7 @@ function Account ({ address, className }: Props): React.ReactElement<Props> {
)}
<ActionBar>
<Link onClick={_toggleEdit}>Edit</Link>
{!isExternal && <Link to={`/account/export/${address}`}>Export</Link>}
<Link to={`/account/forget/${address}`}>Forget</Link>
</ActionBar>
</Address>

View File

@ -0,0 +1,55 @@
// Copyright 2019 @polkadot/extension-ui 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 { act } from 'react-dom/test-utils';
import Adapter from 'enzyme-adapter-react-16';
import { configure, mount, ReactWrapper } from 'enzyme';
import { MemoryRouter, Route } from 'react-router';
import React from 'react';
import { Button } from '../components';
import Export from './Export';
import { exportAccount } from '../messaging';
configure({ adapter: new Adapter() });
jest.mock('../messaging');
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const flushAllPromises = (): Promise<void> => new Promise(resolve => setImmediate(resolve));
describe('Export component', () => {
let wrapper: ReactWrapper;
const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5';
const enterPassword = (password = 'any password'): void => {
wrapper.find('[data-export-password] input').simulate('change', { target: { value: password } });
};
beforeEach(() => {
(exportAccount as jest.Mock).mockResolvedValue({ exportedJson: '{ "meta": { "name": "account_name" } }' });
wrapper = mount(<MemoryRouter
initialEntries={ [`/account/export/${VALID_ADDRESS}`] }>
<Route path='/account/export/:address' component={ Export }/>
</MemoryRouter>);
});
it('creates export message on button press', async () => {
enterPassword('passw0rd');
wrapper.find('[data-export-button] button').simulate('click');
await act(flushAllPromises);
expect(exportAccount).toHaveBeenCalledWith(VALID_ADDRESS, 'passw0rd');
});
it('button is disabled before any password is typed', () => {
expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
});
it('button is enabled after password is typed', async () => {
enterPassword();
await act(flushAllPromises);
expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
});
});

View File

@ -0,0 +1,63 @@
// Copyright 2019 @polkadot/extension-ui 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 React, { useContext, useState } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { ActionContext, Address, Button, Header, Input, Tip } from '../components';
import { exportAccount } from '../messaging';
import { Back } from '../partials';
const MIN_LENGTH = 6;
type Props = RouteComponentProps<{ address: string }>;
function Export ({ match: { params: { address } } }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
const [pass, setPass] = useState('');
const [wrongPasswordHighlight, setWrongPasswordHighlight] = useState(false);
const _onExportButtonClick = (): Promise<void> =>
exportAccount(address, pass)
.then(({ exportedJson }) => {
const element = document.createElement('a');
element.href = `data:text/plain;charset=utf-8,${exportedJson}`;
element.download = `${JSON.parse(exportedJson).meta.name}_exported_account_${Date.now()}.json`;
element.click();
onAction('/');
})
.catch((error: Error) => {
console.error(error);
setWrongPasswordHighlight(true);
setTimeout(() => setWrongPasswordHighlight(false), 100);
});
return (
<div>
<Header label='export account' />
<Back />
<Address address={address}>
<Tip header='danger' type='error'>You are exporting your account. Keep it safe and don&apos;t share it with anyone.</Tip>
<Input
isError={pass.length < MIN_LENGTH || wrongPasswordHighlight}
label='password for this account'
onChange={setPass}
type='password'
data-export-password
/>
<Button
isDisabled={pass.length === 0}
isDanger
label='I want to export this account'
onClick={_onExportButtonClick}
className='export-button'
data-export-button
/>
</Address>
</div>
);
}
export default withRouter(Export);

View File

@ -15,6 +15,7 @@ import { subscribeAccounts, subscribeAuthorize, subscribeSigning } from '../mess
import Accounts from './Accounts';
import Authorize from './Authorize';
import Create from './Create';
import Export from './Export';
import Forget from './Forget';
import ImportQr from './ImportQr';
import ImportSeed from './ImportSeed';
@ -85,6 +86,7 @@ export default function Popup (): React.ReactElement<{}> {
<Switch>
<Route path='/account/create' component={Create} />
<Route path='/account/forget/:address' component={Forget} />
<Route path='/account/export/:address' component={Export} />
<Route path='/account/import-qr' component={ImportQr} />
<Route path='/account/import-seed' component={ImportSeed} />
<Route path='/settings' component={Settings} />

View File

@ -13,12 +13,13 @@ interface Props {
isError?: boolean;
isFocussed?: boolean;
isReadOnly?: boolean;
rowsCount?: number;
label: string;
onChange?: (value: string) => void;
value?: string;
}
function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value }: Props): React.ReactElement<Props> {
function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value, rowsCount }: Props): React.ReactElement<Props> {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>): void => {
onChange && onChange(value.trim());
};
@ -33,6 +34,7 @@ function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value }
onChange={_onChange}
readOnly={isReadOnly}
value={value}
rows={rowsCount || 2}
/>
</Label>
);

View File

@ -0,0 +1,24 @@
// Copyright 2019 @polkadot/extension-ui 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 Adapter from 'enzyme-adapter-react-16';
import { configure } from 'enzyme';
import extensionizer from 'extensionizer';
import { exportAccount } from './messaging';
configure({ adapter: new Adapter() });
describe('messaging sends message to background via extension port for', () => {
test('exportAccount', () => {
const callback = jest.fn();
extensionizer.runtime.connect().onMessage.addListener(callback);
exportAccount('HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', 'passw0rd');
expect(callback).toHaveBeenCalledWith(expect.objectContaining({
message: 'pri(accounts.export)',
request: { address: 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5', password: 'passw0rd' }
}));
});
});

View File

@ -66,6 +66,10 @@ export async function forgetAccount (address: string): Promise<boolean> {
return sendMessage('pri(accounts.forget)', { address });
}
export async function exportAccount (address: string, password: string): Promise<{ exportedJson: string }> {
return sendMessage('pri(accounts.export)', { address, password });
}
export async function rejectAuthRequest (id: string): Promise<boolean> {
return sendMessage('pri(authorize.reject)', { id });
}

View File

@ -0,0 +1,28 @@
import Extension from '@polkadot/extension/background/handlers/Extension';
import keyring from '@polkadot/ui-keyring';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import State from '@polkadot/extension/background/handlers/State';
import { ResponseAccountExport } from '@polkadot/extension/background/types';
import ExtensionStore from '@polkadot/ui-keyring/stores/Extension';
describe('Extension', () => {
async function createExtension (): Promise<Extension> {
await cryptoWaitReady();
keyring.loadAll({ store: new ExtensionStore() });
return new Extension(new State());
}
test('exports account from keyring', async () => {
const extension = await createExtension();
const { pair: { address } } = keyring.addUri(
'seed sock milk update focus rotate barely fade car face mechanic mercy', 'passw0rd'
);
const result = await extension.handle('id', 'pri(accounts.export)', {
address,
password: 'passw0rd'
}, {} as chrome.runtime.Port) as ResponseAccountExport;
expect(result.exportedJson).toContain(address);
expect(result.exportedJson).toContain('"encoded"');
});
});

View File

@ -3,7 +3,28 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { AccountJson, AuthorizeRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAuthorizeApprove, RequestAuthorizeReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, ResponseSeedCreate, RequestSeedValidate, ResponseSeedValidate, RequestAccountForget, SigningRequest, RequestTypes, ResponseTypes, MessageTypes } from '../types';
import {
AccountJson,
AuthorizeRequest,
RequestAccountCreateExternal,
RequestAccountCreateSuri,
RequestAccountEdit,
RequestAuthorizeApprove,
RequestAuthorizeReject,
RequestSigningApprovePassword,
RequestSigningApproveSignature,
RequestSigningCancel,
RequestSeedCreate,
ResponseSeedCreate,
RequestSeedValidate,
ResponseSeedValidate,
RequestAccountForget,
SigningRequest,
RequestTypes,
ResponseTypes,
MessageTypes,
RequestAccountExport, ResponseAccountExport
} from '../types';
import extension from 'extensionizer';
import keyring from '@polkadot/ui-keyring';
@ -60,6 +81,10 @@ export default class Extension {
return true;
}
private accountsExport ({ address, password }: RequestAccountExport): ResponseAccountExport {
return { exportedJson: JSON.stringify(keyring.backupAccount(keyring.getPair(address), password), null, 2) };
}
// FIXME This looks very much like what we have in Tabs
private accountsSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(accounts.subscribe)'>(id, port);
@ -233,6 +258,9 @@ export default class Extension {
case 'pri(accounts.forget)':
return this.accountsForget(request as RequestAccountForget);
case 'pri(accounts.export)':
return this.accountsExport(request as RequestAccountExport);
case 'pri(accounts.edit)':
return this.accountsEdit(request as RequestAccountEdit);

View File

@ -47,6 +47,7 @@ export interface RequestSignatures {
'pri(accounts.create.suri)': [RequestAccountCreateSuri, boolean];
'pri(accounts.edit)': [RequestAccountEdit, boolean];
'pri(accounts.forget)': [RequestAccountForget, boolean];
'pri(accounts.export)': [RequestAccountExport, ResponseAccountExport];
'pri(accounts.subscribe)': [RequestAccountSubscribe, boolean, AccountJson[]];
'pri(authorize.approve)': [RequestAuthorizeApprove, boolean];
'pri(authorize.reject)': [RequestAuthorizeReject, boolean];
@ -120,6 +121,11 @@ export interface RequestAccountForget {
address: string;
}
export interface RequestAccountExport {
address: string;
password: string;
}
export type RequestAccountList = null;
export type RequestAccountSubscribe = null;
@ -193,6 +199,10 @@ export interface ResponseSeedValidate {
suri: string;
}
export interface ResponseAccountExport {
exportedJson: string;
}
// Subscriptions
export type SubscriptionMessageTypes = NoUndefinedValues<{

128
yarn.lock
View File

@ -2264,6 +2264,35 @@
resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-0.14.1.tgz#f4923bba22d7c68a4be3575ba27790947b212633"
integrity sha512-Xng7L2Z8TNZa/5g6pot4O06Jf0ohQRZdvfl8eQL+E/L2mcqJYC1IjkMxJBSBuQEV7hisWzh9mHOy5WCcgPk29Q==
"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393"
integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==
dependencies:
type-detect "4.0.8"
"@sinonjs/formatio@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.1.tgz#52310f2f9bcbc67bdac18c94ad4901b95fde267e"
integrity sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==
dependencies:
"@sinonjs/commons" "^1"
"@sinonjs/samsam" "^3.1.0"
"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3":
version "3.3.3"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a"
integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
dependencies:
"@sinonjs/commons" "^1.3.0"
array-from "^2.1.1"
lodash "^4.17.15"
"@sinonjs/text-encoding@^0.7.1":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
"@types/babel__core@^7.1.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
@ -2325,7 +2354,14 @@
dependencies:
"@types/base-x" "*"
"@types/chrome@^0.0.89":
"@types/cheerio@*":
version "0.22.13"
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6"
integrity sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA==
dependencies:
"@types/node" "*"
"@types/chrome@*", "@types/chrome@^0.0.89":
version "0.0.89"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.89.tgz#0d5baef543aaa0715b345b57b86cba6ca708b524"
integrity sha512-nWgL4U38SnIuMpG2rKMCGDknfZb193KFY15imqHUVFF4IJAVXvdEJOOgtcX9v2XsAaZiz2UUOInvWk5x1DhENQ==
@ -2351,6 +2387,21 @@
dependencies:
"@types/color-convert" "*"
"@types/enzyme-adapter-react-16@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5"
integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg==
dependencies:
"@types/enzyme" "*"
"@types/enzyme@*", "@types/enzyme@^3.10.3":
version "3.10.3"
resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.3.tgz#02b6c5ac7d0472005944a652e79045e2f6c66804"
integrity sha512-f/Kcb84sZOSZiBPCkr4He9/cpuSLcKRyQaEE20Q30Prx0Dn6wcyMAWI0yofL6yvd9Ht9G7EVkQeRqK0n5w8ILw==
dependencies:
"@types/cheerio" "*"
"@types/react" "*"
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -2564,6 +2615,19 @@
dependencies:
"@types/node" "*"
"@types/sinon-chrome@^2.2.6":
version "2.2.6"
resolved "https://registry.yarnpkg.com/@types/sinon-chrome/-/sinon-chrome-2.2.6.tgz#0ce2b8caea8d6ec0a2f9e20ca72c820b590bef81"
integrity sha512-R4FM7ywHxsqkHQlGdM42etVouSEUxZTUyXAheAyEe19DOGkEwNk7bIWi9aGc6WToIkQcFk/3O9aXCQh1YbgcMQ==
dependencies:
"@types/chrome" "*"
"@types/sinon" "*"
"@types/sinon@*":
version "7.0.13"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313"
integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung==
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@ -3326,6 +3390,11 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
array-from@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
array-ify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
@ -5350,6 +5419,11 @@ diff-sequences@^24.9.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
diff@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@ -8615,6 +8689,11 @@ jsx-ast-utils@^2.1.0:
array-includes "^3.0.3"
object.assign "^4.1.0"
just-extend@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc"
integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==
keygrip@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
@ -9011,7 +9090,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1:
lodash@^4.15.0, lodash@^4.16.3, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.1:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -9031,6 +9110,11 @@ loglevelnext@^3.0.0:
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==
lolex@^4.1.0, lolex@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7"
integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -9730,6 +9814,17 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
nise@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652"
integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==
dependencies:
"@sinonjs/formatio" "^3.2.1"
"@sinonjs/text-encoding" "^0.7.1"
just-extend "^4.0.2"
lolex "^4.1.0"
path-to-regexp "^1.7.0"
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
@ -12427,6 +12522,28 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
sinon-chrome@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/sinon-chrome/-/sinon-chrome-3.0.1.tgz#9fb14c230fa0959cb280f9f589e9eda8ccfda1d9"
integrity sha512-NTEFhyuiWEMnRmIqldUiA2DhKn2EqnZxyEk5Ez5rBXj+Nl54aJ0MEmF4wjltrxecxd8zlNLxyE0HyLabev9JsQ==
dependencies:
lodash "^4.16.3"
sinon "^7.2.3"
urijs "^1.18.2"
sinon@^7.2.3:
version "7.4.2"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.4.2.tgz#ecd54158fef2fcfbdb231a3fa55140e8cb02ad6c"
integrity sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ==
dependencies:
"@sinonjs/commons" "^1.4.0"
"@sinonjs/formatio" "^3.2.1"
"@sinonjs/samsam" "^3.3.3"
diff "^3.5.0"
lolex "^4.2.0"
nise "^1.5.2"
supports-color "^5.5.0"
sisteransi@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb"
@ -13401,7 +13518,7 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
type-detect@^4.0.0, type-detect@^4.0.5:
type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
@ -13640,6 +13757,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
urijs@^1.18.2:
version "1.19.1"
resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a"
integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==
urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"