import { strict as assert } from 'assert'; import { find } from 'lodash'; import sinon from 'sinon'; import { METADATA_STORE_KEY, METADATA_CACHE_MAX_SIZE, } from '../../../../../app/scripts/controllers/permissions/enums'; import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'; import { getRequestUserApprovalHelper, grantPermissions } from './helpers'; import { constants, getters, getNotifyDomain, getNotifyAllDomains, getPermControllerOpts, } from './mocks'; 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'); }); }); });