From b2043ad79e7167a56c62af5cacb08a508ed08c8d Mon Sep 17 00:00:00 2001 From: David Drazic Date: Thu, 24 Oct 2024 15:15:42 +0200 Subject: [PATCH] Add onNameLookup to snaps-jest (#2857) Add possibility to test `onNameLookup` handler with `snaps-jest`. Fixes: https://github.com/MetaMask/snaps/issues/2789 --- .../packages/name-lookup/src/index.test.ts | 35 ++------ packages/snaps-jest/src/helpers.test.tsx | 43 ++++++++++ packages/snaps-jest/src/helpers.ts | 2 + .../snaps-simulation/src/helpers.test.tsx | 83 +++++++++++++++++++ packages/snaps-simulation/src/helpers.ts | 34 ++++++++ .../snaps-simulation/src/structs.test.tsx | 70 ++++++++++++++++ packages/snaps-simulation/src/structs.ts | 28 +++++++ packages/snaps-simulation/src/types.ts | 19 +++++ 8 files changed, 287 insertions(+), 27 deletions(-) diff --git a/packages/examples/packages/name-lookup/src/index.test.ts b/packages/examples/packages/name-lookup/src/index.test.ts index dbd671defc..c8c322f009 100644 --- a/packages/examples/packages/name-lookup/src/index.test.ts +++ b/packages/examples/packages/name-lookup/src/index.test.ts @@ -1,8 +1,7 @@ -import { describe, it } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; +import { installSnap } from '@metamask/snaps-jest'; import type { ChainId } from '@metamask/snaps-sdk'; -import { onNameLookup } from '.'; - const DOMAIN_MOCK = 'test.domain'; const ADDRESS_MOCK = '0xc0ffee254729296a45a3885639AC7E10F9d54979'; const CHAIN_ID_MOCK = 'eip155:1' as ChainId; @@ -14,7 +13,10 @@ describe('onNameLookup', () => { chainId: CHAIN_ID_MOCK, }; - expect(await onNameLookup(request)).toStrictEqual({ + const { onNameLookup } = await installSnap(); + const response = await onNameLookup(request); + + expect(response).toRespondWith({ resolvedAddresses: [ { resolvedAddress: '0xc0ffee254729296a45a3885639AC7E10F9d54979', @@ -31,33 +33,12 @@ describe('onNameLookup', () => { chainId: CHAIN_ID_MOCK, }; - expect(await onNameLookup(request)).toStrictEqual({ - resolvedDomains: [ - { resolvedDomain: 'c0f.1.test.domain', protocol: 'test protocol' }, - ], - }); - }); - - it('returns resolved domain if address and domain', async () => { - const request = { - address: ADDRESS_MOCK, - domain: DOMAIN_MOCK, - chainId: CHAIN_ID_MOCK, - } as any; + const { onNameLookup } = await installSnap(); - expect(await onNameLookup(request)).toStrictEqual({ + expect(await onNameLookup(request)).toRespondWith({ resolvedDomains: [ { resolvedDomain: 'c0f.1.test.domain', protocol: 'test protocol' }, ], }); }); - - it('returns null if no domain or address', async () => { - const request = { - chainId: CHAIN_ID_MOCK, - }; - - // @ts-expect-error - Testing invalid request. - expect(await onNameLookup(request)).toBeNull(); - }); }); diff --git a/packages/snaps-jest/src/helpers.test.tsx b/packages/snaps-jest/src/helpers.test.tsx index e010e6c016..684490c54a 100644 --- a/packages/snaps-jest/src/helpers.test.tsx +++ b/packages/snaps-jest/src/helpers.test.tsx @@ -675,6 +675,49 @@ describe('installSnap', () => { }); }); + describe('onNameLookup', () => { + it('sends a name lookup request and returns the result', async () => { + jest.spyOn(console, 'log').mockImplementation(); + const MOCK_DOMAIN = 'test.domain'; + + const { snapId, close: closeServer } = await getMockServer({ + sourceCode: ` + module.exports.onNameLookup = async (request) => { + return { + resolvedAddress: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + protocol: 'test protocol', + domainName: request.domain, + }; + }; + `, + }); + + const { onNameLookup, close } = await installSnap(snapId); + const response = await onNameLookup({ + chainId: 'eip155:1', + domain: MOCK_DOMAIN, + }); + + expect(response).toStrictEqual( + expect.objectContaining({ + response: { + result: { + resolvedAddress: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + protocol: 'test protocol', + domainName: MOCK_DOMAIN, + }, + }, + }), + ); + + // `close` is deprecated because the Jest environment will automatically + // close the Snap when the test finishes. However, we still need to close + // the Snap in this test because it's run outside the Jest environment. + await close(); + await closeServer(); + }); + }); + describe('runCronjob', () => { it('runs a cronjob and returns the result', async () => { jest.spyOn(console, 'log').mockImplementation(); diff --git a/packages/snaps-jest/src/helpers.ts b/packages/snaps-jest/src/helpers.ts index fe1e12c060..a4d3d38d33 100644 --- a/packages/snaps-jest/src/helpers.ts +++ b/packages/snaps-jest/src/helpers.ts @@ -181,6 +181,7 @@ export async function installSnap< onKeyringRequest, onInstall, onUpdate, + onNameLookup, mockJsonRpc, close, } = await getEnvironment().installSnap(...resolvedOptions); @@ -196,6 +197,7 @@ export async function installSnap< onKeyringRequest, onInstall, onUpdate, + onNameLookup, mockJsonRpc, close: async () => { log('Closing execution service.'); diff --git a/packages/snaps-simulation/src/helpers.test.tsx b/packages/snaps-simulation/src/helpers.test.tsx index b492af9c94..e8f83c4646 100644 --- a/packages/snaps-simulation/src/helpers.test.tsx +++ b/packages/snaps-simulation/src/helpers.test.tsx @@ -409,6 +409,89 @@ describe('helpers', () => { }); }); + describe('onNameLookup', () => { + it('sends a domain name lookup request and returns the result', async () => { + jest.spyOn(console, 'log').mockImplementation(); + const MOCK_DOMAIN = 'test.domain'; + + const { snapId, close: closeServer } = await getMockServer({ + sourceCode: ` + module.exports.onNameLookup = async (request) => { + return { + resolvedAddress: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + protocol: 'test protocol', + domainName: request.domain, + }; + }; + `, + }); + + const { onNameLookup, close } = await installSnap(snapId); + const response = await onNameLookup({ + chainId: 'eip155:1', + domain: MOCK_DOMAIN, + }); + + expect(response).toStrictEqual( + expect.objectContaining({ + response: { + result: { + resolvedAddress: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + protocol: 'test protocol', + domainName: MOCK_DOMAIN, + }, + }, + }), + ); + + // `close` is deprecated because the Jest environment will automatically + // close the Snap when the test finishes. However, we still need to close + // the Snap in this test because it's run outside the Jest environment. + await close(); + await closeServer(); + }); + + it('sends an address lookup request and returns the result', async () => { + jest.spyOn(console, 'log').mockImplementation(); + const MOCK_ADDRESS = '0xc0ffee254729296a45a3885639AC7E10F9d54979'; + const MOCK_DOMAIN = 'test.domain'; + + const { snapId, close: closeServer } = await getMockServer({ + sourceCode: ` + module.exports.onNameLookup = async (request) => { + return { + resolvedDomain: 'test.domain', + protocol: 'test protocol', + }; + }; + `, + }); + + const { onNameLookup, close } = await installSnap(snapId); + const response = await onNameLookup({ + chainId: 'eip155:1', + address: MOCK_ADDRESS, + }); + + expect(response).toStrictEqual( + expect.objectContaining({ + response: { + result: { + resolvedDomain: MOCK_DOMAIN, + protocol: 'test protocol', + }, + }, + }), + ); + + // `close` is deprecated because the Jest environment will automatically + // close the Snap when the test finishes. However, we still need to close + // the Snap in this test because it's run outside the Jest environment. + await close(); + await closeServer(); + }); + }); + describe('runCronjob', () => { it('runs a cronjob and returns the result', async () => { jest.spyOn(console, 'log').mockImplementation(); diff --git a/packages/snaps-simulation/src/helpers.ts b/packages/snaps-simulation/src/helpers.ts index 6dfe5ebbcc..639b119a77 100644 --- a/packages/snaps-simulation/src/helpers.ts +++ b/packages/snaps-simulation/src/helpers.ts @@ -10,6 +10,7 @@ import { addJsonRpcMock, removeJsonRpcMock } from './store'; import { assertIsResponseWithInterface, JsonRpcMockOptionsStruct, + NameLookupOptionsStruct, SignatureOptionsStruct, TransactionOptionsStruct, } from './structs'; @@ -17,6 +18,7 @@ import type { CronjobOptions, JsonRpcMockOptions, KeyringOptions, + NameLookupOptions, RequestOptions, SignatureOptions, SnapRequest, @@ -140,6 +142,15 @@ export type SnapHelpers = { */ onUpdate(request?: Pick): SnapRequest; + /** + * Get the response from the Snap's `onNameLookup` handler. + * + * @returns The response. + */ + onNameLookup( + request: NameLookupOptions, + ): Promise; + /** * Mock a JSON-RPC request. This will cause the snap to respond with the * specified response when a request with the specified method is sent. @@ -314,6 +325,29 @@ export function getHelpers({ }); }, + onNameLookup: async ( + nameLookupOptions: NameLookupOptions, + ): Promise => { + log('Requesting name lookup %o.', nameLookupOptions); + + const params = create(nameLookupOptions, NameLookupOptionsStruct); + + const response = await handleRequest({ + snapId, + store, + executionService, + controllerMessenger, + runSaga, + handler: HandlerType.OnNameLookup, + request: { + method: '', + params, + }, + }); + + return response; + }, + onSignature: async ( request: unknown, ): Promise => { diff --git a/packages/snaps-simulation/src/structs.test.tsx b/packages/snaps-simulation/src/structs.test.tsx index cc88a27a1d..702170a925 100644 --- a/packages/snaps-simulation/src/structs.test.tsx +++ b/packages/snaps-simulation/src/structs.test.tsx @@ -2,8 +2,10 @@ import { Box, Text } from '@metamask/snaps-sdk/jsx'; import { create } from '@metamask/superstruct'; import { + BaseNameLookupOptionsStruct, InterfaceStruct, JsonRpcMockOptionsStruct, + NameLookupOptionsStruct, SignatureOptionsStruct, SnapOptionsStruct, SnapResponseStruct, @@ -358,3 +360,71 @@ describe('SnapResponseStruct', () => { expect(() => create(value, SnapResponseStruct)).toThrow(); }); }); + +describe('BaseNameLookupOptionsStruct', () => { + it('accepts a valid object', () => { + const options = create( + { + chainId: 'eip155:1', + }, + BaseNameLookupOptionsStruct, + ); + + expect(options).toStrictEqual({ + chainId: 'eip155:1', + }); + }); + + it.each(INVALID_VALUES)('throws for invalid value: %p', (value) => { + // eslint-disable-next-line jest/require-to-throw-message + expect(() => create(value, BaseNameLookupOptionsStruct)).toThrow(); + }); +}); + +describe('NameLookupOptionsStruct', () => { + it('accepts a valid object for domain lookup', () => { + const options = create( + { + chainId: 'eip155:1', + domain: 'test.domain', + }, + NameLookupOptionsStruct, + ); + + expect(options).toStrictEqual({ + chainId: 'eip155:1', + domain: 'test.domain', + }); + }); + + it('accepts a valid object for address lookup', () => { + const options = create( + { + chainId: 'eip155:1', + address: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + }, + NameLookupOptionsStruct, + ); + + expect(options).toStrictEqual({ + chainId: 'eip155:1', + address: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + }); + }); + + it('throws when trying to use both, address and domain', () => { + const options = { + chainId: 'eip155:1', + address: '0xc0ffee254729296a45a3885639AC7E10F9d54979', + domain: 'test.domain', + }; + expect(() => create(options, NameLookupOptionsStruct)).toThrow( + 'Expected the value to satisfy a union of `object | object`, but received: [object Object]', + ); + }); + + it.each(INVALID_VALUES)('throws for invalid value: %p', (value) => { + // eslint-disable-next-line jest/require-to-throw-message + expect(() => create(value, NameLookupOptionsStruct)).toThrow(); + }); +}); diff --git a/packages/snaps-simulation/src/structs.ts b/packages/snaps-simulation/src/structs.ts index a9a51351d3..de055ed215 100644 --- a/packages/snaps-simulation/src/structs.ts +++ b/packages/snaps-simulation/src/structs.ts @@ -190,6 +190,34 @@ export const SignatureOptionsStruct = object({ ), }); +export const BaseNameLookupOptionsStruct = object({ + /** + * The CAIP-2 chain ID. Defaults to `eip155:1`. + */ + chainId: defaulted(string(), 'eip155:1'), +}); + +export const NameLookupOptionsStruct = union([ + assign( + BaseNameLookupOptionsStruct, + object({ + /** + * Address to lookup. + */ + address: string(), + }), + ), + assign( + BaseNameLookupOptionsStruct, + object({ + /** + * Domain name to lookup. + */ + domain: string(), + }), + ), +]); + export const SnapOptionsStruct = object({ /** * The timeout in milliseconds to use for requests to the snap. Defaults to diff --git a/packages/snaps-simulation/src/types.ts b/packages/snaps-simulation/src/types.ts index 7707775213..21bc37c7ea 100644 --- a/packages/snaps-simulation/src/types.ts +++ b/packages/snaps-simulation/src/types.ts @@ -5,6 +5,7 @@ import type { Infer } from '@metamask/superstruct'; import type { Json, JsonRpcId, JsonRpcParams } from '@metamask/utils'; import type { + NameLookupOptionsStruct, SignatureOptionsStruct, SnapOptionsStruct, SnapResponseStruct, @@ -67,6 +68,15 @@ export type TransactionOptions = Infer; */ export type KeyringOptions = RequestOptions; +/** + * The options to use for name lookup requests. + * + * @property chainId - Chain ID. + * @property domain - Domain name to lookup and resolve. + * @property address - Address to lookup and resolve. + */ +export type NameLookupOptions = Infer; + /** * The options to use for signature requests. * @@ -434,6 +444,15 @@ export type Snap = { */ onUpdate(request?: Pick): SnapRequest; + /** + * Get the response from the Snap's `onNameLookup` handler. + * + * @returns The response. + */ + onNameLookup( + nameLookupRequest: NameLookupOptions, + ): Promise; + /** * Mock a JSON-RPC request. This will cause the snap to respond with the * specified response when a request with the specified method is sent.