1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-27 04:46:10 +01:00
metamask-extension/app/scripts/controllers/permissions/permissions-controller.test.js
2021-03-16 16:00:08 -05:00

1563 lines
48 KiB
JavaScript

import { strict as assert } from 'assert';
import { find } from 'lodash';
import sinon from 'sinon';
import {
constants,
getters,
getNotifyDomain,
getNotifyAllDomains,
getPermControllerOpts,
} from '../../../../test/mocks/permission-controller';
import {
getRequestUserApprovalHelper,
grantPermissions,
} from '../../../../test/helpers/permission-controller-helpers';
import { METADATA_STORE_KEY, METADATA_CACHE_MAX_SIZE } from './enums';
import { PermissionsController } from '.';
const { ERRORS, NOTIFICATIONS, PERMS } = getters;
const {
ALL_ACCOUNTS,
ACCOUNTS,
DUMMY_ACCOUNT,
DOMAINS,
PERM_NAMES,
REQUEST_IDS,
EXTRA_ACCOUNT,
} = constants;
const initNotifications = () => {
return Object.values(DOMAINS).reduce((acc, domain) => {
acc[domain.origin] = [];
return acc;
}, {});
};
const initPermController = (notifications = initNotifications()) => {
return new PermissionsController({
...getPermControllerOpts(),
notifyDomain: getNotifyDomain(notifications),
notifyAllDomains: getNotifyAllDomains(notifications),
});
};
describe('permissions controller', function () {
describe('constructor', function () {
it('throws on undefined argument', function () {
assert.throws(
() => new PermissionsController(),
'should throw on undefined argument',
);
});
});
describe('getAccounts', function () {
let permController;
beforeEach(function () {
permController = initPermController();
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
});
it('gets permitted accounts for permitted origins', async function () {
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
const bAccounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'first origin should have correct accounts',
);
assert.deepEqual(
bAccounts,
[ACCOUNTS.b.primary],
'second origin should have correct accounts',
);
});
it('does not get accounts for unpermitted origins', async function () {
const cAccounts = await permController.getAccounts(DOMAINS.c.origin);
assert.deepEqual(cAccounts, [], 'origin should have no accounts');
});
it('does not handle "metamask" origin as special case', async function () {
const metamaskAccounts = await permController.getAccounts('metamask');
assert.deepEqual(metamaskAccounts, [], 'origin should have no accounts');
});
});
describe('hasPermission', function () {
it('returns correct values', async function () {
const permController = initPermController();
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.test_method(),
);
assert.ok(
permController.hasPermission(DOMAINS.a.origin, 'eth_accounts'),
'should return true for granted permission',
);
assert.ok(
permController.hasPermission(DOMAINS.b.origin, 'test_method'),
'should return true for granted permission',
);
assert.ok(
!permController.hasPermission(DOMAINS.a.origin, 'test_method'),
'should return false for non-granted permission',
);
assert.ok(
!permController.hasPermission(DOMAINS.b.origin, 'eth_accounts'),
'should return true for non-granted permission',
);
assert.ok(
!permController.hasPermission('foo', 'eth_accounts'),
'should return false for unknown origin',
);
assert.ok(
!permController.hasPermission(DOMAINS.b.origin, 'foo'),
'should return false for unknown permission',
);
});
});
describe('clearPermissions', function () {
it('notifies all appropriate domains and removes permissions', async function () {
const notifications = initNotifications();
const permController = initPermController(notifications);
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
);
let aAccounts = await permController.getAccounts(DOMAINS.a.origin);
let bAccounts = await permController.getAccounts(DOMAINS.b.origin);
let cAccounts = await permController.getAccounts(DOMAINS.c.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'first origin should have correct accounts',
);
assert.deepEqual(
bAccounts,
[ACCOUNTS.b.primary],
'second origin should have correct accounts',
);
assert.deepEqual(
cAccounts,
[ACCOUNTS.c.primary],
'third origin should have correct accounts',
);
permController.clearPermissions();
Object.keys(notifications).forEach((origin) => {
assert.deepEqual(
notifications[origin],
[NOTIFICATIONS.removedAccounts()],
'origin should have single metamask_accountsChanged:[] notification',
);
});
aAccounts = await permController.getAccounts(DOMAINS.a.origin);
bAccounts = await permController.getAccounts(DOMAINS.b.origin);
cAccounts = await permController.getAccounts(DOMAINS.c.origin);
assert.deepEqual(aAccounts, [], 'first origin should have no accounts');
assert.deepEqual(bAccounts, [], 'second origin should have no accounts');
assert.deepEqual(cAccounts, [], 'third origin should have no accounts');
Object.keys(notifications).forEach((origin) => {
assert.deepEqual(
permController.permissions.getPermissionsForDomain(origin),
[],
'origin should have no permissions',
);
});
assert.deepEqual(
Object.keys(permController.permissions.getDomains()),
[],
'all domains should be deleted',
);
});
});
describe('removePermissionsFor', function () {
let permController, notifications;
beforeEach(function () {
notifications = initNotifications();
permController = initPermController(notifications);
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
});
it('removes permissions for multiple domains', async function () {
let aAccounts = await permController.getAccounts(DOMAINS.a.origin);
let bAccounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'first origin should have correct accounts',
);
assert.deepEqual(
bAccounts,
[ACCOUNTS.b.primary],
'second origin should have correct accounts',
);
permController.removePermissionsFor({
[DOMAINS.a.origin]: [PERM_NAMES.eth_accounts],
[DOMAINS.b.origin]: [PERM_NAMES.eth_accounts],
});
aAccounts = await permController.getAccounts(DOMAINS.a.origin);
bAccounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(aAccounts, [], 'first origin should have no accounts');
assert.deepEqual(bAccounts, [], 'second origin should have no accounts');
assert.deepEqual(
notifications[DOMAINS.a.origin],
[NOTIFICATIONS.removedAccounts()],
'first origin should have correct notification',
);
assert.deepEqual(
notifications[DOMAINS.b.origin],
[NOTIFICATIONS.removedAccounts()],
'second origin should have correct notification',
);
assert.deepEqual(
Object.keys(permController.permissions.getDomains()),
[],
'all domains should be deleted',
);
});
it('only removes targeted permissions from single domain', async function () {
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.test_method(),
);
let bPermissions = permController.permissions.getPermissionsForDomain(
DOMAINS.b.origin,
);
assert.ok(
bPermissions.length === 2 &&
find(bPermissions, { parentCapability: PERM_NAMES.eth_accounts }) &&
find(bPermissions, { parentCapability: PERM_NAMES.test_method }),
'origin should have correct permissions',
);
permController.removePermissionsFor({
[DOMAINS.b.origin]: [PERM_NAMES.test_method],
});
bPermissions = permController.permissions.getPermissionsForDomain(
DOMAINS.b.origin,
);
assert.ok(
bPermissions.length === 1 &&
find(bPermissions, { parentCapability: PERM_NAMES.eth_accounts }),
'only targeted permission should have been removed',
);
});
it('removes permissions for a single domain, without affecting another', async function () {
permController.removePermissionsFor({
[DOMAINS.b.origin]: [PERM_NAMES.eth_accounts],
});
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
const bAccounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'first origin should have correct accounts',
);
assert.deepEqual(bAccounts, [], 'second origin should have no accounts');
assert.deepEqual(
notifications[DOMAINS.a.origin],
[],
'first origin should have no notifications',
);
assert.deepEqual(
notifications[DOMAINS.b.origin],
[NOTIFICATIONS.removedAccounts()],
'second origin should have correct notification',
);
assert.deepEqual(
Object.keys(permController.permissions.getDomains()),
[DOMAINS.a.origin],
'only first origin should remain',
);
});
it('send notification but does not affect permissions for unknown domain', async function () {
// it knows nothing of this origin
permController.removePermissionsFor({
[DOMAINS.c.origin]: [PERM_NAMES.eth_accounts],
});
assert.deepEqual(
notifications[DOMAINS.c.origin],
[NOTIFICATIONS.removedAccounts()],
'unknown origin should have notification',
);
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
const bAccounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'first origin should have correct accounts',
);
assert.deepEqual(
bAccounts,
[ACCOUNTS.b.primary],
'second origin should have correct accounts',
);
assert.deepEqual(
Object.keys(permController.permissions.getDomains()),
[DOMAINS.a.origin, DOMAINS.b.origin],
'should have correct domains',
);
});
});
describe('validatePermittedAccounts', function () {
let permController;
beforeEach(function () {
permController = initPermController();
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
});
it('throws error on non-array accounts', async function () {
await assert.throws(
() => permController.validatePermittedAccounts(undefined),
ERRORS.validatePermittedAccounts.invalidParam(),
'should throw on undefined',
);
await assert.throws(
() => permController.validatePermittedAccounts(false),
ERRORS.validatePermittedAccounts.invalidParam(),
'should throw on false',
);
await assert.throws(
() => permController.validatePermittedAccounts(true),
ERRORS.validatePermittedAccounts.invalidParam(),
'should throw on true',
);
await assert.throws(
() => permController.validatePermittedAccounts({}),
ERRORS.validatePermittedAccounts.invalidParam(),
'should throw on non-array object',
);
});
it('throws error on empty array of accounts', async function () {
await assert.throws(
() => permController.validatePermittedAccounts([]),
ERRORS.validatePermittedAccounts.invalidParam(),
'should throw on empty array',
);
});
it('throws error if any account value is not in keyring', async function () {
const keyringAccounts = await permController.getKeyringAccounts();
await assert.throws(
() => permController.validatePermittedAccounts([DUMMY_ACCOUNT]),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account',
);
await assert.throws(
() =>
permController.validatePermittedAccounts(
keyringAccounts.concat(DUMMY_ACCOUNT),
),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account with other accounts',
);
});
it('succeeds if all accounts are in keyring', async function () {
const keyringAccounts = await permController.getKeyringAccounts();
await assert.doesNotThrow(
() => permController.validatePermittedAccounts(keyringAccounts),
'should not throw on all keyring accounts',
);
await assert.doesNotThrow(
() => permController.validatePermittedAccounts([keyringAccounts[0]]),
'should not throw on single keyring account',
);
await assert.doesNotThrow(
() => permController.validatePermittedAccounts([keyringAccounts[1]]),
'should not throw on single keyring account',
);
});
});
describe('addPermittedAccount', function () {
let permController, notifications;
beforeEach(function () {
notifications = initNotifications();
permController = initPermController(notifications);
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
});
it('should throw if account is not a string', async function () {
await assert.rejects(
() => permController.addPermittedAccount(DOMAINS.a.origin, {}),
ERRORS.validatePermittedAccounts.nonKeyringAccount({}),
'should throw on non-string account param',
);
});
it('should throw if given account is not in keyring', async function () {
await assert.rejects(
() =>
permController.addPermittedAccount(DOMAINS.a.origin, DUMMY_ACCOUNT),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account',
);
});
it('should throw if origin is invalid', async function () {
await assert.rejects(
() => permController.addPermittedAccount(false, EXTRA_ACCOUNT),
ERRORS.addPermittedAccount.invalidOrigin(),
'should throw on invalid origin',
);
});
it('should throw if origin lacks any permissions', async function () {
await assert.rejects(
() =>
permController.addPermittedAccount(DOMAINS.c.origin, EXTRA_ACCOUNT),
ERRORS.addPermittedAccount.invalidOrigin(),
'should throw on origin without permissions',
);
});
it('should throw if origin lacks eth_accounts permission', async function () {
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.test_method(),
);
await assert.rejects(
() =>
permController.addPermittedAccount(DOMAINS.c.origin, EXTRA_ACCOUNT),
ERRORS.addPermittedAccount.noEthAccountsPermission(),
'should throw on origin without eth_accounts permission',
);
});
it('should throw if account is already permitted', async function () {
await assert.rejects(
() =>
permController.addPermittedAccount(
DOMAINS.a.origin,
ACCOUNTS.a.permitted[0],
),
ERRORS.addPermittedAccount.alreadyPermitted(),
'should throw if account is already permitted',
);
});
it('should successfully add permitted account', async function () {
await permController.addPermittedAccount(DOMAINS.a.origin, EXTRA_ACCOUNT);
const accounts = await permController._getPermittedAccounts(
DOMAINS.a.origin,
);
assert.deepEqual(
accounts,
[...ACCOUNTS.a.permitted, EXTRA_ACCOUNT],
'origin should have correct accounts',
);
assert.deepEqual(
notifications[DOMAINS.a.origin][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'origin should have correct notification',
);
});
});
describe('removePermittedAccount', function () {
let permController, notifications;
beforeEach(function () {
notifications = initNotifications();
permController = initPermController(notifications);
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
});
it('should throw if account is not a string', async function () {
await assert.rejects(
() => permController.removePermittedAccount(DOMAINS.a.origin, {}),
ERRORS.validatePermittedAccounts.nonKeyringAccount({}),
'should throw on non-string account param',
);
});
it('should throw if given account is not in keyring', async function () {
await assert.rejects(
() =>
permController.removePermittedAccount(
DOMAINS.a.origin,
DUMMY_ACCOUNT,
),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account',
);
});
it('should throw if origin is invalid', async function () {
await assert.rejects(
() => permController.removePermittedAccount(false, EXTRA_ACCOUNT),
ERRORS.removePermittedAccount.invalidOrigin(),
'should throw on invalid origin',
);
});
it('should throw if origin lacks any permissions', async function () {
await assert.rejects(
() =>
permController.removePermittedAccount(
DOMAINS.c.origin,
EXTRA_ACCOUNT,
),
ERRORS.removePermittedAccount.invalidOrigin(),
'should throw on origin without permissions',
);
});
it('should throw if origin lacks eth_accounts permission', async function () {
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.test_method(),
);
await assert.rejects(
() =>
permController.removePermittedAccount(
DOMAINS.c.origin,
EXTRA_ACCOUNT,
),
ERRORS.removePermittedAccount.noEthAccountsPermission(),
'should throw on origin without eth_accounts permission',
);
});
it('should throw if account is not permitted', async function () {
await assert.rejects(
() =>
permController.removePermittedAccount(
DOMAINS.b.origin,
ACCOUNTS.c.permitted[0],
),
ERRORS.removePermittedAccount.notPermitted(),
'should throw if account is not permitted',
);
});
it('should successfully remove permitted account', async function () {
await permController.removePermittedAccount(
DOMAINS.a.origin,
ACCOUNTS.a.permitted[1],
);
const accounts = await permController._getPermittedAccounts(
DOMAINS.a.origin,
);
assert.deepEqual(
accounts,
ACCOUNTS.a.permitted.filter((acc) => acc !== ACCOUNTS.a.permitted[1]),
'origin should have correct accounts',
);
assert.deepEqual(
notifications[DOMAINS.a.origin][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'origin should have correct notification',
);
});
it('should remove eth_accounts permission if removing only permitted account', async function () {
await permController.removePermittedAccount(
DOMAINS.b.origin,
ACCOUNTS.b.permitted[0],
);
const accounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(accounts, [], 'origin should have no accounts');
const permission = await permController.permissions.getPermission(
DOMAINS.b.origin,
PERM_NAMES.eth_accounts,
);
assert.equal(
permission,
undefined,
'origin should not have eth_accounts permission',
);
assert.deepEqual(
notifications[DOMAINS.b.origin][0],
NOTIFICATIONS.removedAccounts(),
'origin should have correct notification',
);
});
});
describe('removeAllAccountPermissions', function () {
let permController, notifications;
beforeEach(function () {
notifications = initNotifications();
permController = initPermController(notifications);
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
);
});
it('should throw if account is not a string', async function () {
await assert.rejects(
() => permController.removeAllAccountPermissions({}),
ERRORS.validatePermittedAccounts.nonKeyringAccount({}),
'should throw on non-string account param',
);
});
it('should throw if given account is not in keyring', async function () {
await assert.rejects(
() => permController.removeAllAccountPermissions(DUMMY_ACCOUNT),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account',
);
});
it('should remove permitted account from single origin', async function () {
await permController.removeAllAccountPermissions(ACCOUNTS.a.permitted[1]);
const accounts = await permController._getPermittedAccounts(
DOMAINS.a.origin,
);
assert.deepEqual(
accounts,
ACCOUNTS.a.permitted.filter((acc) => acc !== ACCOUNTS.a.permitted[1]),
'origin should have correct accounts',
);
assert.deepEqual(
notifications[DOMAINS.a.origin][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'origin should have correct notification',
);
});
it('should permitted account from multiple origins', async function () {
await permController.removeAllAccountPermissions(ACCOUNTS.b.permitted[0]);
const bAccounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(bAccounts, [], 'first origin should no accounts');
const cAccounts = await permController.getAccounts(DOMAINS.c.origin);
assert.deepEqual(cAccounts, [], 'second origin no accounts');
assert.deepEqual(
notifications[DOMAINS.b.origin][0],
NOTIFICATIONS.removedAccounts(),
'first origin should have correct notification',
);
assert.deepEqual(
notifications[DOMAINS.c.origin][0],
NOTIFICATIONS.removedAccounts(),
'second origin should have correct notification',
);
});
it('should remove eth_accounts permission if removing only permitted account', async function () {
await permController.removeAllAccountPermissions(ACCOUNTS.b.permitted[0]);
const accounts = await permController.getAccounts(DOMAINS.b.origin);
assert.deepEqual(accounts, [], 'origin should have no accounts');
const permission = await permController.permissions.getPermission(
DOMAINS.b.origin,
PERM_NAMES.eth_accounts,
);
assert.equal(
permission,
undefined,
'origin should not have eth_accounts permission',
);
assert.deepEqual(
notifications[DOMAINS.b.origin][0],
NOTIFICATIONS.removedAccounts(),
'origin should have correct notification',
);
});
});
describe('finalizePermissionsRequest', function () {
let permController;
beforeEach(function () {
permController = initPermController();
});
it('throws on non-keyring accounts', async function () {
await assert.rejects(
permController.finalizePermissionsRequest(
PERMS.requests.eth_accounts(),
[DUMMY_ACCOUNT],
),
ERRORS.validatePermittedAccounts.nonKeyringAccount(DUMMY_ACCOUNT),
'should throw on non-keyring account',
);
});
it('adds caveat to eth_accounts permission', async function () {
const perm = await permController.finalizePermissionsRequest(
PERMS.requests.eth_accounts(),
ACCOUNTS.a.permitted,
);
assert.deepEqual(
perm,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
});
it('replaces caveat of eth_accounts permission', async function () {
const perm = await permController.finalizePermissionsRequest(
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
ACCOUNTS.b.permitted,
);
assert.deepEqual(
perm,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
'permission should have correct caveat',
);
});
it('handles non-eth_accounts permission', async function () {
const perm = await permController.finalizePermissionsRequest(
PERMS.finalizedRequests.test_method(),
ACCOUNTS.b.permitted,
);
assert.deepEqual(
perm,
PERMS.finalizedRequests.test_method(),
'permission should have correct caveat',
);
});
});
describe('preferences state update', function () {
let permController, notifications, preferences, identities;
beforeEach(function () {
identities = ALL_ACCOUNTS.reduce((identitiesAcc, account) => {
identitiesAcc[account] = {};
return identitiesAcc;
}, {});
preferences = {
getState: sinon.stub(),
subscribe: sinon.stub(),
};
preferences.getState.returns({
identities,
selectedAddress: DUMMY_ACCOUNT,
});
notifications = initNotifications();
permController = new PermissionsController({
...getPermControllerOpts(),
notifyDomain: getNotifyDomain(notifications),
notifyAllDomains: getNotifyAllDomains(notifications),
preferences,
});
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.eth_accounts([
...ACCOUNTS.a.permitted,
EXTRA_ACCOUNT,
]),
);
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
});
it('should throw if given invalid account', async function () {
assert(preferences.subscribe.calledOnce);
assert(preferences.subscribe.firstCall.args.length === 1);
const onPreferencesUpdate = preferences.subscribe.firstCall.args[0];
await assert.rejects(
() => onPreferencesUpdate({ selectedAddress: {} }),
ERRORS._handleAccountSelected.invalidParams(),
'should throw if account is not a string',
);
});
it('should do nothing if account not permitted for any origins', async function () {
assert(preferences.subscribe.calledOnce);
assert(preferences.subscribe.firstCall.args.length === 1);
const onPreferencesUpdate = preferences.subscribe.firstCall.args[0];
await onPreferencesUpdate({ selectedAddress: DUMMY_ACCOUNT });
assert.deepEqual(
notifications[DOMAINS.b.origin],
[],
'should not have emitted notification',
);
assert.deepEqual(
notifications[DOMAINS.c.origin],
[],
'should not have emitted notification',
);
});
it('should emit notification if account already first in array for each connected site', async function () {
identities[ACCOUNTS.a.permitted[0]] = { lastSelected: 1000 };
assert(preferences.subscribe.calledOnce);
assert(preferences.subscribe.firstCall.args.length === 1);
const onPreferencesUpdate = preferences.subscribe.firstCall.args[0];
await onPreferencesUpdate({ selectedAddress: ACCOUNTS.a.permitted[0] });
assert.deepEqual(
notifications[DOMAINS.b.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary])],
'should not have emitted notification',
);
assert.deepEqual(
notifications[DOMAINS.c.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary])],
'should not have emitted notification',
);
});
it('should emit notification just for connected domains', async function () {
identities[EXTRA_ACCOUNT] = { lastSelected: 1000 };
assert(preferences.subscribe.calledOnce);
assert(preferences.subscribe.firstCall.args.length === 1);
const onPreferencesUpdate = preferences.subscribe.firstCall.args[0];
await onPreferencesUpdate({ selectedAddress: EXTRA_ACCOUNT });
assert.deepEqual(
notifications[DOMAINS.b.origin],
[NOTIFICATIONS.newAccounts([EXTRA_ACCOUNT])],
'should have emitted notification',
);
assert.deepEqual(
notifications[DOMAINS.c.origin],
[],
'should not have emitted notification',
);
});
it('should emit notification for multiple connected domains', async function () {
identities[ACCOUNTS.a.permitted[1]] = { lastSelected: 1000 };
assert(preferences.subscribe.calledOnce);
assert(preferences.subscribe.firstCall.args.length === 1);
const onPreferencesUpdate = preferences.subscribe.firstCall.args[0];
await onPreferencesUpdate({ selectedAddress: ACCOUNTS.a.permitted[1] });
assert.deepEqual(
notifications[DOMAINS.b.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.a.permitted[1]])],
'should have emitted notification',
);
assert.deepEqual(
notifications[DOMAINS.c.origin],
[NOTIFICATIONS.newAccounts([ACCOUNTS.c.primary])],
'should have emitted notification',
);
});
});
describe('approvePermissionsRequest', function () {
let permController, requestUserApproval;
beforeEach(function () {
permController = initPermController();
requestUserApproval = getRequestUserApprovalHelper(permController);
});
it('does nothing if called on non-existing request', async function () {
sinon.spy(permController, 'finalizePermissionsRequest');
const request = PERMS.approvedRequest(REQUEST_IDS.a, null);
await assert.doesNotReject(
permController.approvePermissionsRequest(request, null),
'should not throw on non-existing request',
);
assert.ok(
permController.finalizePermissionsRequest.notCalled,
'should not call finalizePermissionRequest',
);
});
it('rejects request with bad accounts param', async function () {
const request = PERMS.approvedRequest(
REQUEST_IDS.a,
PERMS.requests.eth_accounts(),
);
const rejectionPromise = assert.rejects(
requestUserApproval(REQUEST_IDS.a),
ERRORS.validatePermittedAccounts.invalidParam(),
'should reject with "null" accounts',
);
await permController.approvePermissionsRequest(request, null);
await rejectionPromise;
});
it('rejects request with no permissions', async function () {
const request = PERMS.approvedRequest(REQUEST_IDS.a, {});
const requestRejection = assert.rejects(
requestUserApproval(REQUEST_IDS.a),
ERRORS.approvePermissionsRequest.noPermsRequested(),
'should reject if no permissions in request',
);
await permController.approvePermissionsRequest(
request,
ACCOUNTS.a.permitted,
);
await requestRejection;
});
it('approves valid request', async function () {
const request = PERMS.approvedRequest(
REQUEST_IDS.a,
PERMS.requests.eth_accounts(),
);
let perms;
const requestApproval = assert.doesNotReject(async () => {
perms = await requestUserApproval(REQUEST_IDS.a);
}, 'should not reject single valid request');
await permController.approvePermissionsRequest(
request,
ACCOUNTS.a.permitted,
);
await requestApproval;
assert.deepEqual(
perms,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
'should produce expected approved permissions',
);
});
it('approves valid requests regardless of order', async function () {
const request1 = PERMS.approvedRequest(
REQUEST_IDS.a,
PERMS.requests.eth_accounts(),
);
const request2 = PERMS.approvedRequest(
REQUEST_IDS.b,
PERMS.requests.eth_accounts(),
);
const request3 = PERMS.approvedRequest(
REQUEST_IDS.c,
PERMS.requests.eth_accounts(),
);
let perms1, perms2;
const approval1 = assert.doesNotReject(async () => {
perms1 = await requestUserApproval(REQUEST_IDS.a, DOMAINS.a.origin);
}, 'should not reject request');
const approval2 = assert.doesNotReject(async () => {
perms2 = await requestUserApproval(REQUEST_IDS.b, DOMAINS.b.origin);
}, 'should not reject request');
// approve out of order
await permController.approvePermissionsRequest(
request2,
ACCOUNTS.b.permitted,
);
// add a non-existing request to the mix
await permController.approvePermissionsRequest(
request3,
ACCOUNTS.c.permitted,
);
await permController.approvePermissionsRequest(
request1,
ACCOUNTS.a.permitted,
);
await approval1;
await approval2;
assert.deepEqual(
perms1,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
'first request should produce expected approved permissions',
);
assert.deepEqual(
perms2,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.b.permitted),
'second request should produce expected approved permissions',
);
});
});
describe('rejectPermissionsRequest', function () {
let permController, requestUserApproval;
beforeEach(async function () {
permController = initPermController();
requestUserApproval = getRequestUserApprovalHelper(permController);
});
it('does nothing if called on non-existing request', async function () {
permController.approvals.add = sinon.fake.throws(
new Error('should not call add'),
);
await assert.doesNotReject(
permController.rejectPermissionsRequest(REQUEST_IDS.a),
'should not throw on non-existing request',
);
});
it('rejects single existing request', async function () {
const requestRejection = assert.rejects(
requestUserApproval(REQUEST_IDS.a),
ERRORS.rejectPermissionsRequest.rejection(),
'should reject with expected error',
);
await permController.rejectPermissionsRequest(REQUEST_IDS.a);
await requestRejection;
});
it('rejects requests regardless of order', async function () {
const requestRejection1 = assert.rejects(
requestUserApproval(REQUEST_IDS.b, DOMAINS.b.origin),
ERRORS.rejectPermissionsRequest.rejection(),
'should reject with expected error',
);
const requestRejection2 = assert.rejects(
requestUserApproval(REQUEST_IDS.c, DOMAINS.c.origin),
ERRORS.rejectPermissionsRequest.rejection(),
'should reject with expected error',
);
// reject out of order
await permController.rejectPermissionsRequest(REQUEST_IDS.c);
// add a non-existing request to the mix
await permController.rejectPermissionsRequest(REQUEST_IDS.a);
await permController.rejectPermissionsRequest(REQUEST_IDS.b);
await requestRejection1;
await requestRejection2;
});
});
// see permissions-middleware-test for testing the middleware itself
describe('createMiddleware', function () {
let permController, clock;
beforeEach(function () {
permController = initPermController();
clock = sinon.useFakeTimers(1);
});
afterEach(function () {
clock.restore();
});
it('should throw on bad origin', function () {
assert.throws(
() => permController.createMiddleware({ origin: {} }),
ERRORS.createMiddleware.badOrigin(),
'should throw expected error',
);
assert.throws(
() => permController.createMiddleware({ origin: '' }),
ERRORS.createMiddleware.badOrigin(),
'should throw expected error',
);
assert.throws(
() => permController.createMiddleware({}),
ERRORS.createMiddleware.badOrigin(),
'should throw expected error',
);
});
it('should create a middleware', function () {
let middleware;
assert.doesNotThrow(() => {
middleware = permController.createMiddleware({
origin: DOMAINS.a.origin,
});
}, 'should not throw');
assert.equal(typeof middleware, 'function', 'should return function');
});
it('should create a middleware with extensionId', function () {
const extensionId = 'fooExtension';
let middleware;
assert.doesNotThrow(() => {
middleware = permController.createMiddleware({
origin: DOMAINS.a.origin,
extensionId,
});
}, 'should not throw');
assert.equal(typeof middleware, 'function', 'should return function');
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
assert.deepEqual(
metadataStore[DOMAINS.a.origin],
{ extensionId, lastUpdated: 1 },
'metadata should be stored',
);
});
});
describe('notifyAccountsChanged', function () {
let notifications, permController;
beforeEach(function () {
notifications = initNotifications();
permController = initPermController(notifications);
sinon.spy(permController.permissionsLog, 'updateAccountsHistory');
});
it('notifyAccountsChanged records history and sends notification', async function () {
sinon.spy(permController, '_isUnlocked');
permController.notifyAccountsChanged(
DOMAINS.a.origin,
ACCOUNTS.a.permitted,
);
assert.ok(
permController._isUnlocked.calledOnce,
'_isUnlocked should have been called once',
);
assert.ok(
permController.permissionsLog.updateAccountsHistory.calledOnce,
'permissionsLog.updateAccountsHistory should have been called once',
);
assert.deepEqual(
notifications[DOMAINS.a.origin],
[NOTIFICATIONS.newAccounts(ACCOUNTS.a.permitted)],
'origin should have correct notification',
);
});
it('notifyAccountsChanged does nothing if _isUnlocked returns false', async function () {
permController._isUnlocked = sinon.fake.returns(false);
permController.notifyAccountsChanged(
DOMAINS.a.origin,
ACCOUNTS.a.permitted,
);
assert.ok(
permController._isUnlocked.calledOnce,
'_isUnlocked should have been called once',
);
assert.ok(
permController.permissionsLog.updateAccountsHistory.notCalled,
'permissionsLog.updateAccountsHistory should not have been called',
);
});
it('notifyAccountsChanged throws on invalid origin', async function () {
assert.throws(
() => permController.notifyAccountsChanged(4, ACCOUNTS.a.permitted),
ERRORS.notifyAccountsChanged.invalidOrigin(4),
'should throw expected error for non-string origin',
);
assert.throws(
() => permController.notifyAccountsChanged('', ACCOUNTS.a.permitted),
ERRORS.notifyAccountsChanged.invalidOrigin(''),
'should throw expected error for empty string origin',
);
});
it('notifyAccountsChanged throws on invalid accounts', async function () {
assert.throws(
() => permController.notifyAccountsChanged(DOMAINS.a.origin, 4),
ERRORS.notifyAccountsChanged.invalidAccounts(),
'should throw expected error for truthy non-array accounts',
);
assert.throws(
() => permController.notifyAccountsChanged(DOMAINS.a.origin, null),
ERRORS.notifyAccountsChanged.invalidAccounts(),
'should throw expected error for falsy non-array accounts',
);
});
});
describe('addDomainMetadata', function () {
let permController, clock;
function getMockMetadata(size) {
const dummyData = {};
for (let i = 0; i < size; i++) {
const key = i.toString();
dummyData[key] = {};
}
return dummyData;
}
beforeEach(function () {
permController = initPermController();
permController._setDomainMetadata = sinon.fake();
clock = sinon.useFakeTimers(1);
});
afterEach(function () {
clock.restore();
});
it('calls setter function with expected new state when adding domain', function () {
permController.store.getState = sinon.fake.returns({
[METADATA_STORE_KEY]: {
[DOMAINS.a.origin]: {
foo: 'bar',
},
},
});
permController.addDomainMetadata(DOMAINS.b.origin, { foo: 'bar' });
assert.ok(
permController.store.getState.called,
'should have called store.getState',
);
assert.equal(
permController._setDomainMetadata.getCalls().length,
1,
'should have called _setDomainMetadata once',
);
assert.deepEqual(permController._setDomainMetadata.lastCall.args, [
{
[DOMAINS.a.origin]: {
foo: 'bar',
},
[DOMAINS.b.origin]: {
foo: 'bar',
host: DOMAINS.b.host,
lastUpdated: 1,
},
},
]);
});
it('calls setter function with expected new states when updating existing domain', function () {
permController.store.getState = sinon.fake.returns({
[METADATA_STORE_KEY]: {
[DOMAINS.a.origin]: {
foo: 'bar',
},
[DOMAINS.b.origin]: {
bar: 'baz',
},
},
});
permController.addDomainMetadata(DOMAINS.b.origin, { foo: 'bar' });
assert.ok(
permController.store.getState.called,
'should have called store.getState',
);
assert.equal(
permController._setDomainMetadata.getCalls().length,
1,
'should have called _setDomainMetadata once',
);
assert.deepEqual(permController._setDomainMetadata.lastCall.args, [
{
[DOMAINS.a.origin]: {
foo: 'bar',
},
[DOMAINS.b.origin]: {
foo: 'bar',
bar: 'baz',
host: DOMAINS.b.host,
lastUpdated: 1,
},
},
]);
});
it('pops metadata on add when too many origins are pending', function () {
sinon.spy(permController._pendingSiteMetadata, 'delete');
const mockMetadata = getMockMetadata(METADATA_CACHE_MAX_SIZE);
const expectedDeletedOrigin = Object.keys(mockMetadata)[0];
permController.store.getState = sinon.fake.returns({
[METADATA_STORE_KEY]: { ...mockMetadata },
});
// populate permController._pendingSiteMetadata, as though these origins
// were actually added
Object.keys(mockMetadata).forEach((origin) => {
permController._pendingSiteMetadata.add(origin);
});
permController.addDomainMetadata(DOMAINS.a.origin, { foo: 'bar' });
assert.ok(
permController.store.getState.called,
'should have called store.getState',
);
const expectedMetadata = {
...mockMetadata,
[DOMAINS.a.origin]: {
foo: 'bar',
host: DOMAINS.a.host,
lastUpdated: 1,
},
};
delete expectedMetadata[expectedDeletedOrigin];
assert.ok(
permController._pendingSiteMetadata.delete.calledOnceWithExactly(
expectedDeletedOrigin,
),
'should have called _pendingSiteMetadata.delete once',
);
assert.equal(
permController._setDomainMetadata.getCalls().length,
1,
'should have called _setDomainMetadata once',
);
assert.deepEqual(permController._setDomainMetadata.lastCall.args, [
expectedMetadata,
]);
});
});
describe('_trimDomainMetadata', function () {
const permController = initPermController();
it('trims domain metadata for domains without permissions', function () {
const metadataArg = {
[DOMAINS.a.origin]: {},
[DOMAINS.b.origin]: {},
};
permController.permissions.getDomains = sinon.fake.returns({
[DOMAINS.a.origin]: {},
});
const metadataResult = permController._trimDomainMetadata(metadataArg);
assert.equal(
permController.permissions.getDomains.getCalls().length,
1,
'should have called permissions.getDomains once',
);
assert.deepEqual(
metadataResult,
{
[DOMAINS.a.origin]: {},
},
'should have produced expected state',
);
});
});
describe('miscellanea and edge cases', function () {
it('requestAccountsPermissionWithId calls _requestPermissions and notifyAccounts', function (done) {
const notifications = initNotifications();
const permController = initPermController(notifications);
const _requestPermissions = sinon
.stub(permController, '_requestPermissions')
.resolves();
const notifyAccountsChanged = sinon
.stub(permController, 'notifyAccountsChanged')
.callsFake(() => {
assert.ok(
notifyAccountsChanged.calledOnceWithExactly('example.com', []),
);
notifyAccountsChanged.restore();
_requestPermissions.restore();
done();
});
permController.requestAccountsPermissionWithId('example.com');
});
it('requestAccountsPermissionWithId calls _requestAccountsPermission with an explicit request ID', function (done) {
const permController = initPermController();
const _requestPermissions = sinon
.stub(permController, '_requestPermissions')
.resolves();
const onResolved = async () => {
assert.ok(
_requestPermissions.calledOnceWithExactly(
sinon.match.object.and(sinon.match.has('origin')),
{ eth_accounts: {} },
sinon.match.string.and(sinon.match.truthy),
),
);
_requestPermissions.restore();
// eslint-disable-next-line no-use-before-define
notifyAccountsChanged.restore();
done();
};
const notifyAccountsChanged = sinon
.stub(permController, 'notifyAccountsChanged')
.callsFake(onResolved);
permController.requestAccountsPermissionWithId('example.com');
});
});
});