1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02:00
metamask-extension/app/scripts/controllers/permissions/specifications.test.js
Erik Marks 9830b14786
Make eth_accounts return all permitted accounts (#18516)
* Make eth_accounts return all permitted accounts rather than just the most recently selected one

* fixup! Make eth_accounts return all permitted accounts rather than just the most recently selected one

* Trigger

---------

Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
Co-authored-by: Jiexi Luan <jiexiluan@gmail.com>
2023-06-08 13:01:43 -07:00

366 lines
12 KiB
JavaScript

import { SnapCaveatType } from '@metamask/rpc-methods';
import {
CaveatTypes,
RestrictedMethods,
} from '../../../../shared/constants/permissions';
import {
getCaveatSpecifications,
getPermissionSpecifications,
unrestrictedMethods,
} from './specifications';
// Note: This causes Date.now() to return the number 1.
jest.useFakeTimers('modern').setSystemTime(1);
describe('PermissionController specifications', () => {
describe('caveat specifications', () => {
it('getCaveatSpecifications returns the expected specifications object', () => {
const caveatSpecifications = getCaveatSpecifications({});
expect(Object.keys(caveatSpecifications)).toHaveLength(8);
expect(
caveatSpecifications[CaveatTypes.restrictReturnedAccounts].type,
).toStrictEqual(CaveatTypes.restrictReturnedAccounts);
expect(caveatSpecifications.permittedDerivationPaths.type).toStrictEqual(
SnapCaveatType.PermittedDerivationPaths,
);
expect(caveatSpecifications.permittedCoinTypes.type).toStrictEqual(
SnapCaveatType.PermittedCoinTypes,
);
expect(caveatSpecifications.snapKeyring.type).toStrictEqual(
SnapCaveatType.SnapKeyring,
);
expect(caveatSpecifications.snapCronjob.type).toStrictEqual(
SnapCaveatType.SnapCronjob,
);
expect(caveatSpecifications.transactionOrigin.type).toStrictEqual(
SnapCaveatType.TransactionOrigin,
);
expect(caveatSpecifications.rpcOrigin.type).toStrictEqual(
SnapCaveatType.RpcOrigin,
);
expect(caveatSpecifications.snapIds.type).toStrictEqual(
SnapCaveatType.SnapIds,
);
});
describe('restrictReturnedAccounts', () => {
describe('decorator', () => {
it('only returns array members included in the caveat value', async () => {
const getIdentities = jest.fn();
const { decorator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
const method = async () => ['0x1', '0x2', '0x3'];
const caveat = {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x3'],
};
const decorated = decorator(method, caveat);
expect(await decorated()).toStrictEqual(['0x1', '0x3']);
});
it('returns an empty array if no array members are included in the caveat value', async () => {
const getIdentities = jest.fn();
const { decorator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
const method = async () => ['0x1', '0x2', '0x3'];
const caveat = {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x5'],
};
const decorated = decorator(method, caveat);
expect(await decorated()).toStrictEqual([]);
});
it('returns an empty array if the method result is an empty array', async () => {
const getIdentities = jest.fn();
const { decorator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
const method = async () => [];
const caveat = {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
};
const decorated = decorator(method, caveat);
expect(await decorated()).toStrictEqual([]);
});
});
describe('validator', () => {
it('rejects invalid array values', () => {
const getIdentities = jest.fn();
const { validator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
[null, 'foo', {}, []].forEach((invalidValue) => {
expect(() => validator({ value: invalidValue })).toThrow(
/Expected non-empty array of Ethereum addresses\.$/u,
);
});
});
it('rejects falsy or non-string addresses', () => {
const getIdentities = jest.fn();
const { validator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
[[{}], [[]], [null], ['']].forEach((invalidValue) => {
expect(() => validator({ value: invalidValue })).toThrow(
/Expected an array of Ethereum addresses. Received:/u,
);
});
});
it('rejects addresses that have no corresponding identity', () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x1': true,
'0x3': true,
};
});
const { validator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
expect(() => validator({ value: ['0x1', '0x2', '0x3'] })).toThrow(
/Received unrecognized address:/u,
);
});
});
});
});
describe('permission specifications', () => {
it('getPermissionSpecifications returns the expected specifications object', () => {
const permissionSpecifications = getPermissionSpecifications({});
expect(Object.keys(permissionSpecifications)).toHaveLength(1);
expect(
permissionSpecifications[RestrictedMethods.eth_accounts].targetName,
).toStrictEqual(RestrictedMethods.eth_accounts);
});
describe('eth_accounts', () => {
describe('factory', () => {
it('constructs a valid eth_accounts permission', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { factory } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(
factory(
{ invoker: 'foo.bar', target: 'eth_accounts' },
{ approvedAccounts: ['0x1'] },
),
).toStrictEqual({
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1'],
},
],
date: 1,
id: expect.any(String),
invoker: 'foo.bar',
parentCapability: 'eth_accounts',
});
});
it('throws an error if no approvedAccounts are specified', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { factory } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(() =>
factory(
{ invoker: 'foo.bar', target: 'eth_accounts' },
{}, // no approvedAccounts
),
).toThrow(/No approved accounts specified\.$/u);
});
it('throws an error if any caveats are specified directly', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { factory } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(() =>
factory(
{
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
},
],
invoker: 'foo.bar',
target: 'eth_accounts',
},
{ approvedAccounts: ['0x1'] },
),
).toThrow(/Received unexpected caveats./u);
});
});
describe('methodImplementation', () => {
it('returns the keyring accounts in lastSelected order', async () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x1': {
lastSelected: 1,
},
'0x2': {},
'0x3': {
lastSelected: 3,
},
'0x4': {
lastSelected: 3,
},
};
});
const getAllAccounts = jest
.fn()
.mockImplementationOnce(() => ['0x1', '0x2', '0x3', '0x4']);
const { methodImplementation } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(await methodImplementation()).toStrictEqual([
'0x3',
'0x4',
'0x1',
'0x2',
]);
});
it('throws if a keyring account is missing an address (case 1)', async () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x2': {
lastSelected: 3,
},
'0x3': {
lastSelected: 3,
},
};
});
const getAllAccounts = jest
.fn()
.mockImplementationOnce(() => ['0x1', '0x2', '0x3']);
const { methodImplementation } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
captureKeyringTypesWithMissingIdentities: jest.fn(),
})[RestrictedMethods.eth_accounts];
await expect(() => methodImplementation()).rejects.toThrow(
'Missing identity for address: "0x1".',
);
});
it('throws if a keyring account is missing an address (case 2)', async () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x1': {
lastSelected: 1,
},
'0x3': {
lastSelected: 3,
},
};
});
const getAllAccounts = jest
.fn()
.mockImplementationOnce(() => ['0x1', '0x2', '0x3']);
const { methodImplementation } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
captureKeyringTypesWithMissingIdentities: jest.fn(),
})[RestrictedMethods.eth_accounts];
await expect(() => methodImplementation()).rejects.toThrow(
'Missing identity for address: "0x2".',
);
});
});
describe('validator', () => {
it('accepts valid permissions', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { validator } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(() =>
validator({
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
},
],
date: 1,
id: expect.any(String),
invoker: 'foo.bar',
parentCapability: 'eth_accounts',
}),
).not.toThrow();
});
it('rejects invalid caveats', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { validator } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
[null, [], [1, 2], [{ type: 'foobar' }]].forEach(
(invalidCaveatsValue) => {
expect(() =>
validator({
caveats: invalidCaveatsValue,
date: 1,
id: expect.any(String),
invoker: 'foo.bar',
parentCapability: 'eth_accounts',
}),
).toThrow(/Invalid caveats./u);
},
);
});
});
});
});
describe('unrestricted methods', () => {
it('defines the unrestricted methods', () => {
expect(Array.isArray(unrestrictedMethods)).toBe(true);
expect(Object.isFrozen(unrestrictedMethods)).toBe(true);
});
});
});