1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/app/scripts/controllers/sign.test.ts
Elliot Winkler 1304ec7af5
Convert shared/constants/metametrics to TS (#18353)
We want to convert NetworkController to TypeScript in order to be able
to compare differences in the controller between in this repo and the
core repo. To do this, however, we need to convert the dependencies of
the controller to TypeScript.

As a part of this effort, this commit converts
`shared/constants/metametrics` to TypeScript. Note that simple objects
have been largely replaced with enums. There are some cases where I even
split up some of these objects into multiple enums.

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
2023-04-03 09:31:04 -06:00

610 lines
19 KiB
TypeScript

import {
MessageManager,
PersonalMessageManager,
TypedMessageManager,
} from '@metamask/message-manager';
import {
AbstractMessage,
OriginalRequest,
} from '@metamask/message-manager/dist/AbstractMessageManager';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import { detectSIWE } from '../../../shared/modules/siwe';
import SignController, {
SignControllerMessenger,
SignControllerOptions,
} from './sign';
jest.mock('@metamask/message-manager', () => ({
MessageManager: jest.fn(),
PersonalMessageManager: jest.fn(),
TypedMessageManager: jest.fn(),
}));
jest.mock('../../../shared/modules/siwe', () => ({
detectSIWE: jest.fn(),
}));
const messageIdMock = '123';
const messageIdMock2 = '456';
const versionMock = '1';
const signatureMock = '0xAABBCC';
const stateMock = { test: 123 };
const securityProviderResponseMock = { test2: 345 };
const messageParamsMock = {
from: '0x123',
origin: 'http://test.com',
data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
metamaskId: messageIdMock,
version: 'V1',
};
const messageParamsMock2 = {
from: '0x124',
origin: 'http://test4.com',
data: '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA',
metamaskId: messageIdMock,
};
const messageMock = {
id: messageIdMock,
time: 123,
status: 'unapproved',
type: 'testType',
rawSig: undefined,
} as any as AbstractMessage;
const coreMessageMock = {
...messageMock,
messageParams: messageParamsMock,
};
const stateMessageMock = {
...messageMock,
msgParams: messageParamsMock,
securityProviderResponse: securityProviderResponseMock,
};
const requestMock = {
origin: 'http://test2.com',
} as OriginalRequest;
const siweMockFound = {
isSIWEMessage: true,
parsedMessage: { domain: 'test.com', test: 'value' },
};
const siweMockNotFound = { isSIWEMessage: false };
const createMessengerMock = () =>
({
registerActionHandler: jest.fn(),
publish: jest.fn(),
call: jest.fn(),
} as any as jest.Mocked<SignControllerMessenger>);
const createMessageManagerMock = <T>() =>
({
getUnapprovedMessages: jest.fn(),
getUnapprovedMessagesCount: jest.fn(),
addUnapprovedMessageAsync: jest.fn(),
approveMessage: jest.fn(),
setMessageStatusSigned: jest.fn(),
rejectMessage: jest.fn(),
subscribe: jest.fn(),
update: jest.fn(),
hub: {
on: jest.fn(),
},
} as any as jest.Mocked<T>);
const createPreferencesControllerMock = () => ({
store: {
getState: jest.fn(),
},
});
const createKeyringControllerMock = () => ({
signMessage: jest.fn(),
signPersonalMessage: jest.fn(),
signTypedMessage: jest.fn(),
});
describe('SignController', () => {
let signController: SignController;
const messageManagerConstructorMock = MessageManager as jest.MockedClass<
typeof MessageManager
>;
const personalMessageManagerConstructorMock =
PersonalMessageManager as jest.MockedClass<typeof PersonalMessageManager>;
const typedMessageManagerConstructorMock =
TypedMessageManager as jest.MockedClass<typeof TypedMessageManager>;
const messageManagerMock = createMessageManagerMock<MessageManager>();
const personalMessageManagerMock =
createMessageManagerMock<PersonalMessageManager>();
const typedMessageManagerMock =
createMessageManagerMock<TypedMessageManager>();
const messengerMock = createMessengerMock();
const preferencesControllerMock = createPreferencesControllerMock();
const keyringControllerMock = createKeyringControllerMock();
const detectSIWEMock = detectSIWE as jest.MockedFunction<typeof detectSIWE>;
const getStateMock = jest.fn();
const securityProviderRequestMock = jest.fn();
const metricsEventMock = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
messageManagerConstructorMock.mockReturnValue(messageManagerMock);
personalMessageManagerConstructorMock.mockReturnValue(
personalMessageManagerMock,
);
typedMessageManagerConstructorMock.mockReturnValue(typedMessageManagerMock);
preferencesControllerMock.store.getState.mockReturnValue({
disabledRpcMethodPreferences: { eth_sign: true },
});
detectSIWEMock.mockReturnValue(siweMockNotFound);
signController = new SignController({
messenger: messengerMock as any,
preferencesController: preferencesControllerMock as any,
keyringController: keyringControllerMock as any,
getState: getStateMock as any,
securityProviderRequest: securityProviderRequestMock as any,
metricsEvent: metricsEventMock as any,
} as SignControllerOptions);
});
describe('unapprovedMsgCount', () => {
it('returns value from message manager getter', () => {
messageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(10);
expect(signController.unapprovedMsgCount).toBe(10);
});
});
describe('unapprovedPersonalMessagesCount', () => {
it('returns value from personal message manager getter', () => {
personalMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(
11,
);
expect(signController.unapprovedPersonalMessagesCount).toBe(11);
});
});
describe('unapprovedTypedMessagesCount', () => {
it('returns value from typed message manager getter', () => {
typedMessageManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(
12,
);
expect(signController.unapprovedTypedMessagesCount).toBe(12);
});
});
describe('resetState', () => {
it('sets state to initial state', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
signController.update(() => ({
unapprovedMsgs: { [messageIdMock]: messageMock } as any,
unapprovedPersonalMsgs: { [messageIdMock]: messageMock } as any,
unapprovedTypedMessages: { [messageIdMock]: messageMock } as any,
unapprovedMsgCount: 1,
unapprovedPersonalMsgCount: 2,
unapprovedTypedMessagesCount: 3,
}));
signController.resetState();
expect(signController.state).toEqual({
unapprovedMsgs: {},
unapprovedPersonalMsgs: {},
unapprovedTypedMessages: {},
unapprovedMsgCount: 0,
unapprovedPersonalMsgCount: 0,
unapprovedTypedMessagesCount: 0,
});
});
});
describe('rejectUnapproved', () => {
beforeEach(() => {
const messages = {
[messageIdMock]: messageMock,
[messageIdMock2]: messageMock,
};
messageManagerMock.getUnapprovedMessages.mockReturnValueOnce(
messages as any,
);
personalMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce(
messages as any,
);
typedMessageManagerMock.getUnapprovedMessages.mockReturnValueOnce(
messages as any,
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
signController.update(() => ({
unapprovedMsgs: messages as any,
unapprovedPersonalMsgs: messages as any,
unapprovedTypedMessages: messages as any,
}));
});
it('rejects all messages in all message managers', () => {
signController.rejectUnapproved('Test Reason');
expect(messageManagerMock.rejectMessage).toHaveBeenCalledTimes(2);
expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock,
);
expect(messageManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock2,
);
expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2);
expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock,
);
expect(personalMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock2,
);
expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledTimes(2);
expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock,
);
expect(typedMessageManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock2,
);
});
it('fires metrics event with reject reason', () => {
signController.rejectUnapproved('Test Reason');
expect(metricsEventMock).toHaveBeenCalledTimes(6);
expect(metricsEventMock).toHaveBeenLastCalledWith({
event: 'Test Reason',
category: MetaMetricsEventCategory.Transactions,
properties: {
action: 'Sign Request',
type: messageMock.type,
},
});
});
});
describe('clearUnapproved', () => {
it('resets state in all message managers', () => {
signController.clearUnapproved();
const defaultState = {
unapprovedMessages: {},
unapprovedMessagesCount: 0,
};
expect(messageManagerMock.update).toHaveBeenCalledTimes(1);
expect(messageManagerMock.update).toHaveBeenCalledWith(defaultState);
expect(personalMessageManagerMock.update).toHaveBeenCalledTimes(1);
expect(personalMessageManagerMock.update).toHaveBeenCalledWith(
defaultState,
);
expect(typedMessageManagerMock.update).toHaveBeenCalledTimes(1);
expect(typedMessageManagerMock.update).toHaveBeenCalledWith(defaultState);
});
});
describe('newUnsignedMessage', () => {
it('throws if eth_sign disabled in preferences', async () => {
preferencesControllerMock.store.getState.mockReturnValueOnce({
disabledRpcMethodPreferences: { eth_sign: false },
});
await expect(
signController.newUnsignedMessage(messageParamsMock, requestMock),
).rejects.toThrowError(
'eth_sign has been disabled. You must enable it in the advanced settings',
);
});
it('throws if data has wrong length', async () => {
await expect(
signController.newUnsignedMessage(
{ ...messageParamsMock, data: '0xFF' },
requestMock,
),
).rejects.toThrowError('eth_sign requires 32 byte message hash');
});
it('adds message to message manager', async () => {
await signController.newUnsignedMessage(messageParamsMock, requestMock);
expect(
messageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledTimes(1);
expect(messageManagerMock.addUnapprovedMessageAsync).toHaveBeenCalledWith(
messageParamsMock,
requestMock,
);
});
});
describe('newUnsignedPersonalMessage', () => {
it('adds message to personal message manager', async () => {
await signController.newUnsignedPersonalMessage(
messageParamsMock,
requestMock,
);
expect(
personalMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledTimes(1);
expect(
personalMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledWith(
expect.objectContaining(messageParamsMock),
requestMock,
);
});
it('adds message to personal message manager including Ethereum sign in data', async () => {
detectSIWEMock.mockReturnValueOnce(siweMockFound);
await signController.newUnsignedPersonalMessage(
messageParamsMock,
requestMock,
);
expect(
personalMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledTimes(1);
expect(
personalMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledWith(
{
...messageParamsMock,
siwe: siweMockFound,
},
requestMock,
);
});
});
describe('newUnsignedTypedMessage', () => {
it('adds message to typed message manager', async () => {
signController.newUnsignedTypedMessage(
messageParamsMock,
requestMock,
versionMock,
);
expect(
typedMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledTimes(1);
expect(
typedMessageManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledWith(messageParamsMock, versionMock, requestMock);
});
});
describe.each([
['signMessage', messageManagerMock],
['signPersonalMessage', personalMessageManagerMock],
['signTypedMessage', typedMessageManagerMock],
])('%s', (signMethodName, messageManager) => {
beforeEach(() => {
messageManager.approveMessage.mockResolvedValueOnce(messageParamsMock2);
keyringControllerMock[signMethodName].mockResolvedValueOnce(
signatureMock,
);
});
it('approves message and signs', async () => {
await signController[signMethodName](messageParamsMock);
const keyringControllerExtraArgs =
signMethodName === 'signTypedMessage'
? [{ version: messageParamsMock.version }]
: [];
expect(keyringControllerMock[signMethodName]).toHaveBeenCalledTimes(1);
expect(keyringControllerMock[signMethodName]).toHaveBeenCalledWith(
messageParamsMock2,
...keyringControllerExtraArgs,
);
expect(messageManager.setMessageStatusSigned).toHaveBeenCalledTimes(1);
expect(messageManager.setMessageStatusSigned).toHaveBeenCalledWith(
messageParamsMock2.metamaskId,
signatureMock,
);
});
it('returns current state', async () => {
getStateMock.mockReturnValueOnce(stateMock);
expect(await signController[signMethodName](messageParamsMock)).toEqual(
stateMock,
);
});
it('accepts approval', async () => {
await signController[signMethodName](messageParamsMock);
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:acceptRequest',
messageParamsMock.metamaskId,
);
});
it('rejects message on error', async () => {
keyringControllerMock[signMethodName].mockReset();
keyringControllerMock[signMethodName].mockRejectedValue(
new Error('Test Error'),
);
await expect(
signController[signMethodName](messageParamsMock),
).rejects.toThrow('Test Error');
expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1);
expect(messageManager.rejectMessage).toHaveBeenCalledWith(
messageParamsMock.metamaskId,
);
});
it('rejects approval on error', async () => {
keyringControllerMock[signMethodName].mockReset();
keyringControllerMock[signMethodName].mockRejectedValue(
new Error('Test Error'),
);
await expect(
signController[signMethodName](messageParamsMock),
).rejects.toThrow('Test Error');
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:rejectRequest',
messageParamsMock.metamaskId,
'Cancel',
);
});
});
describe.each([
['cancelMessage', messageManagerMock],
['cancelPersonalMessage', personalMessageManagerMock],
['cancelTypedMessage', typedMessageManagerMock],
])('%s', (cancelMethodName, messageManager) => {
it('rejects message using message manager', async () => {
signController[cancelMethodName](messageIdMock);
expect(messageManager.rejectMessage).toHaveBeenCalledTimes(1);
expect(messageManager.rejectMessage).toHaveBeenCalledWith(
messageParamsMock.metamaskId,
);
});
it('rejects approval using approval controller', async () => {
signController[cancelMethodName](messageIdMock);
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:rejectRequest',
messageParamsMock.metamaskId,
'Cancel',
);
});
});
describe('message manager events', () => {
it.each([
['message manager', messageManagerMock],
['personal message manager', personalMessageManagerMock],
['typed message manager', typedMessageManagerMock],
])('bubbles update badge event from %s', (_, messageManager) => {
const mockListener = jest.fn();
signController.hub.on('updateBadge', mockListener);
messageManager.hub.on.mock.calls[0][1]();
expect(mockListener).toHaveBeenCalledTimes(1);
});
it.each([
['message manager', messageManagerMock, 'eth_sign'],
['personal message manager', personalMessageManagerMock, 'personal_sign'],
['typed message manager', typedMessageManagerMock, 'eth_signTypedData'],
])(
'requires approval on unapproved message event from %s',
(_, messageManager, methodName) => {
messengerMock.call.mockResolvedValueOnce({});
messageManager.hub.on.mock.calls[1][1](messageParamsMock);
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:addRequest',
{
id: messageIdMock,
origin: messageParamsMock.origin,
type: methodName,
},
true,
);
},
);
it('updates state on message manager state change', async () => {
securityProviderRequestMock.mockResolvedValue(
securityProviderResponseMock,
);
await messageManagerMock.subscribe.mock.calls[0][0]({
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
unapprovedMessagesCount: 3,
});
expect(await signController.state).toEqual({
unapprovedMsgs: { [messageIdMock]: stateMessageMock as any },
unapprovedPersonalMsgs: {},
unapprovedTypedMessages: {},
unapprovedMsgCount: 3,
unapprovedPersonalMsgCount: 0,
unapprovedTypedMessagesCount: 0,
});
});
it('updates state on personal message manager state change', async () => {
securityProviderRequestMock.mockResolvedValue(
securityProviderResponseMock,
);
await personalMessageManagerMock.subscribe.mock.calls[0][0]({
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
unapprovedMessagesCount: 4,
});
expect(await signController.state).toEqual({
unapprovedMsgs: {},
unapprovedPersonalMsgs: { [messageIdMock]: stateMessageMock as any },
unapprovedTypedMessages: {},
unapprovedMsgCount: 0,
unapprovedPersonalMsgCount: 4,
unapprovedTypedMessagesCount: 0,
});
});
it('updates state on typed message manager state change', async () => {
securityProviderRequestMock.mockResolvedValue(
securityProviderResponseMock,
);
await typedMessageManagerMock.subscribe.mock.calls[0][0]({
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
unapprovedMessagesCount: 5,
});
expect(await signController.state).toEqual({
unapprovedMsgs: {},
unapprovedPersonalMsgs: {},
unapprovedTypedMessages: { [messageIdMock]: stateMessageMock as any },
unapprovedMsgCount: 0,
unapprovedPersonalMsgCount: 0,
unapprovedTypedMessagesCount: 5,
});
});
});
});