Skip to content

Commit

Permalink
Add onNameLookup to snaps-jest (#2857)
Browse files Browse the repository at this point in the history
Add possibility to test `onNameLookup` handler with `snaps-jest`.

Fixes: #2789
  • Loading branch information
david0xd authored Oct 24, 2024
1 parent c441eb5 commit b2043ad
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 27 deletions.
35 changes: 8 additions & 27 deletions packages/examples/packages/name-lookup/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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',
Expand All @@ -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();
});
});
43 changes: 43 additions & 0 deletions packages/snaps-jest/src/helpers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions packages/snaps-jest/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export async function installSnap<
onKeyringRequest,
onInstall,
onUpdate,
onNameLookup,
mockJsonRpc,
close,
} = await getEnvironment().installSnap(...resolvedOptions);
Expand All @@ -196,6 +197,7 @@ export async function installSnap<
onKeyringRequest,
onInstall,
onUpdate,
onNameLookup,
mockJsonRpc,
close: async () => {
log('Closing execution service.');
Expand Down
83 changes: 83 additions & 0 deletions packages/snaps-simulation/src/helpers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
34 changes: 34 additions & 0 deletions packages/snaps-simulation/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { addJsonRpcMock, removeJsonRpcMock } from './store';
import {
assertIsResponseWithInterface,
JsonRpcMockOptionsStruct,
NameLookupOptionsStruct,
SignatureOptionsStruct,
TransactionOptionsStruct,
} from './structs';
import type {
CronjobOptions,
JsonRpcMockOptions,
KeyringOptions,
NameLookupOptions,
RequestOptions,
SignatureOptions,
SnapRequest,
Expand Down Expand Up @@ -140,6 +142,15 @@ export type SnapHelpers = {
*/
onUpdate(request?: Pick<RequestOptions, 'origin'>): SnapRequest;

/**
* Get the response from the Snap's `onNameLookup` handler.
*
* @returns The response.
*/
onNameLookup(
request: NameLookupOptions,
): Promise<SnapResponseWithoutInterface>;

/**
* 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.
Expand Down Expand Up @@ -314,6 +325,29 @@ export function getHelpers({
});
},

onNameLookup: async (
nameLookupOptions: NameLookupOptions,
): Promise<SnapResponseWithoutInterface> => {
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<SnapResponseWithInterface> => {
Expand Down
70 changes: 70 additions & 0 deletions packages/snaps-simulation/src/structs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
});
});
28 changes: 28 additions & 0 deletions packages/snaps-simulation/src/structs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit b2043ad

Please sign in to comment.