Skip to content

Commit bbe558a

Browse files
feat: Allow mocking JSON-RPC implementations (#3667)
Allow mocking full JSON-RPC method implementations by passing functions. Also introduces `mockJsonRpcOnce`, which only mocks the first call to the method. The previous method of specifying mocks is still supported. Closes #2096 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds function-based JSON-RPC mocking and a new mockJsonRpcOnce helper (with queueing), refactoring store/middleware to support per-mock implementations and one-time behavior. > > - **API**: > - Extend `mockJsonRpc` to accept function implementations in `packages/snaps-simulation/src/helpers.ts` and types in `packages/snaps-simulation/src/types.ts`. > - Add `mockJsonRpcOnce` to `Snap`/helpers and expose via `packages/snaps-jest/src/helpers.ts`. > - **Store & Middleware**: > - Refactor mocks state to store `{ implementation, once }` by `id`; remove `getJsonRpcMock`, keep `getJsonRpcMocks` (`packages/snaps-simulation/src/store/mocks.ts`). > - Update middleware to async, iterate mocks, execute implementations, and auto-remove `once` mocks (`packages/snaps-simulation/src/middleware/mock.ts`). > - Introduce `nanoid`-based IDs and allow non-serializable middleware (`serializableCheck: false`). > - **Tests**: > - Add tests for function-based mocks, `mockJsonRpcOnce`, and queued mocks in helpers tests; update middleware/store tests to new structure. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5c765f1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Maarten Zuidhoorn <[email protected]>
1 parent 3754d30 commit bbe558a

File tree

10 files changed

+559
-77
lines changed

10 files changed

+559
-77
lines changed

packages/snaps-jest/src/helpers.test.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,125 @@ describe('installSnap', () => {
993993
await close();
994994
await closeServer();
995995
});
996+
997+
it('mocks a JSON-RPC implementation', async () => {
998+
jest.spyOn(console, 'log').mockImplementation();
999+
1000+
const { snapId, close: closeServer } = await getMockServer({
1001+
sourceCode: `
1002+
module.exports.onRpcRequest = async () => {
1003+
return await ethereum.request({
1004+
method: 'foo',
1005+
});
1006+
};
1007+
`,
1008+
manifest: getSnapManifest({
1009+
initialPermissions: {
1010+
'endowment:ethereum-provider': {},
1011+
},
1012+
}),
1013+
});
1014+
1015+
const { request, close, mockJsonRpc } = await installSnap(snapId);
1016+
const { unmock } = mockJsonRpc(({ method }) => {
1017+
return `${method}_mocked`;
1018+
});
1019+
1020+
const response = await request({
1021+
method: 'foo',
1022+
});
1023+
1024+
expect(response).toStrictEqual(
1025+
expect.objectContaining({
1026+
response: {
1027+
result: 'foo_mocked',
1028+
},
1029+
}),
1030+
);
1031+
1032+
unmock();
1033+
1034+
const unmockedResponse = await request({
1035+
method: 'foo',
1036+
});
1037+
1038+
expect(unmockedResponse).toStrictEqual(
1039+
expect.objectContaining({
1040+
response: {
1041+
error: expect.objectContaining({
1042+
code: -32601,
1043+
message: 'The method "foo" does not exist / is not available.',
1044+
}),
1045+
},
1046+
}),
1047+
);
1048+
1049+
// `close` is deprecated because the Jest environment will automatically
1050+
// close the Snap when the test finishes. However, we still need to close
1051+
// the Snap in this test because it's run outside the Jest environment.
1052+
await close();
1053+
await closeServer();
1054+
});
1055+
});
1056+
1057+
describe('mockJsonRpcOnce', () => {
1058+
it('mocks a JSON-RPC method', async () => {
1059+
jest.spyOn(console, 'log').mockImplementation();
1060+
1061+
const { snapId, close: closeServer } = await getMockServer({
1062+
sourceCode: `
1063+
module.exports.onRpcRequest = async () => {
1064+
return await ethereum.request({
1065+
method: 'foo',
1066+
});
1067+
};
1068+
`,
1069+
manifest: getSnapManifest({
1070+
initialPermissions: {
1071+
'endowment:ethereum-provider': {},
1072+
},
1073+
}),
1074+
});
1075+
1076+
const { request, close, mockJsonRpcOnce } = await installSnap(snapId);
1077+
mockJsonRpcOnce({
1078+
method: 'foo',
1079+
result: 'mock',
1080+
});
1081+
1082+
const response = await request({
1083+
method: 'foo',
1084+
});
1085+
1086+
expect(response).toStrictEqual(
1087+
expect.objectContaining({
1088+
response: {
1089+
result: 'mock',
1090+
},
1091+
}),
1092+
);
1093+
1094+
const unmockedResponse = await request({
1095+
method: 'foo',
1096+
});
1097+
1098+
expect(unmockedResponse).toStrictEqual(
1099+
expect.objectContaining({
1100+
response: {
1101+
error: expect.objectContaining({
1102+
code: -32601,
1103+
message: 'The method "foo" does not exist / is not available.',
1104+
}),
1105+
},
1106+
}),
1107+
);
1108+
1109+
// `close` is deprecated because the Jest environment will automatically
1110+
// close the Snap when the test finishes. However, we still need to close
1111+
// the Snap in this test because it's run outside the Jest environment.
1112+
await close();
1113+
await closeServer();
1114+
});
9961115
});
9971116
});
9981117

packages/snaps-jest/src/helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export async function installSnap<
211211
onProtocolRequest,
212212
onClientRequest,
213213
mockJsonRpc,
214+
mockJsonRpcOnce,
214215
close,
215216
} = await getEnvironment().installSnap(...resolvedOptions);
216217
/* eslint-enable @typescript-eslint/unbound-method */
@@ -233,6 +234,7 @@ export async function installSnap<
233234
onProtocolRequest,
234235
onClientRequest,
235236
mockJsonRpc,
237+
mockJsonRpcOnce,
236238
close: async () => {
237239
log('Closing execution service.');
238240
logInfo(

packages/snaps-simulation/src/helpers.test.tsx

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,5 +875,205 @@ describe('helpers', () => {
875875
await close();
876876
await closeServer();
877877
});
878+
879+
it('mocks a JSON-RPC implementation', async () => {
880+
jest.spyOn(console, 'log').mockImplementation();
881+
882+
const { snapId, close: closeServer } = await getMockServer({
883+
sourceCode: `
884+
module.exports.onRpcRequest = async () => {
885+
return await ethereum.request({
886+
method: 'foo',
887+
});
888+
};
889+
`,
890+
manifest: getSnapManifest({
891+
initialPermissions: {
892+
'endowment:ethereum-provider': {},
893+
},
894+
}),
895+
});
896+
897+
const { request, close, mockJsonRpc } = await installSnap(snapId);
898+
const { unmock } = mockJsonRpc(({ method }) => {
899+
return `${method}_mocked`;
900+
});
901+
902+
const response = await request({
903+
method: 'foo',
904+
});
905+
906+
expect(response).toStrictEqual(
907+
expect.objectContaining({
908+
response: {
909+
result: 'foo_mocked',
910+
},
911+
}),
912+
);
913+
914+
unmock();
915+
916+
const unmockedResponse = await request({
917+
method: 'foo',
918+
});
919+
920+
expect(unmockedResponse).toStrictEqual(
921+
expect.objectContaining({
922+
response: {
923+
error: expect.objectContaining({
924+
code: -32601,
925+
message: 'The method "foo" does not exist / is not available.',
926+
}),
927+
},
928+
}),
929+
);
930+
931+
// `close` is deprecated because the Jest environment will automatically
932+
// close the Snap when the test finishes. However, we still need to close
933+
// the Snap in this test because it's run outside the Jest environment.
934+
await close();
935+
await closeServer();
936+
});
937+
});
938+
939+
describe('mockJsonRpcOnce', () => {
940+
it('mocks a JSON-RPC method once', async () => {
941+
jest.spyOn(console, 'log').mockImplementation();
942+
943+
const { snapId, close: closeServer } = await getMockServer({
944+
sourceCode: `
945+
module.exports.onRpcRequest = async () => {
946+
return await ethereum.request({
947+
method: 'foo',
948+
});
949+
};
950+
`,
951+
manifest: getSnapManifest({
952+
initialPermissions: {
953+
'endowment:ethereum-provider': {},
954+
},
955+
}),
956+
});
957+
958+
const { request, close, mockJsonRpcOnce } = await installSnap(snapId);
959+
mockJsonRpcOnce({
960+
method: 'foo_2',
961+
result: 'invalid_mock',
962+
});
963+
964+
mockJsonRpcOnce({
965+
method: 'foo',
966+
result: 'mock',
967+
});
968+
969+
const response = await request({
970+
method: 'foo',
971+
});
972+
973+
expect(response).toStrictEqual(
974+
expect.objectContaining({
975+
response: {
976+
result: 'mock',
977+
},
978+
}),
979+
);
980+
981+
const unmockedResponse = await request({
982+
method: 'foo',
983+
});
984+
985+
expect(unmockedResponse).toStrictEqual(
986+
expect.objectContaining({
987+
response: {
988+
error: expect.objectContaining({
989+
code: -32601,
990+
message: 'The method "foo" does not exist / is not available.',
991+
}),
992+
},
993+
}),
994+
);
995+
996+
// `close` is deprecated because the Jest environment will automatically
997+
// close the Snap when the test finishes. However, we still need to close
998+
// the Snap in this test because it's run outside the Jest environment.
999+
await close();
1000+
await closeServer();
1001+
});
1002+
1003+
it('supports queueing JSON-RPC mocks', async () => {
1004+
jest.spyOn(console, 'log').mockImplementation();
1005+
1006+
const { snapId, close: closeServer } = await getMockServer({
1007+
sourceCode: `
1008+
module.exports.onRpcRequest = async () => {
1009+
return await ethereum.request({
1010+
method: 'foo',
1011+
});
1012+
};
1013+
`,
1014+
manifest: getSnapManifest({
1015+
initialPermissions: {
1016+
'endowment:ethereum-provider': {},
1017+
},
1018+
}),
1019+
});
1020+
1021+
const { request, close, mockJsonRpcOnce } = await installSnap(snapId);
1022+
1023+
mockJsonRpcOnce({
1024+
method: 'foo',
1025+
result: 'mock',
1026+
});
1027+
1028+
mockJsonRpcOnce({
1029+
method: 'foo',
1030+
result: 'mock2',
1031+
});
1032+
1033+
const response1 = await request({
1034+
method: 'foo',
1035+
});
1036+
1037+
expect(response1).toStrictEqual(
1038+
expect.objectContaining({
1039+
response: {
1040+
result: 'mock',
1041+
},
1042+
}),
1043+
);
1044+
1045+
const response2 = await request({
1046+
method: 'foo',
1047+
});
1048+
1049+
expect(response2).toStrictEqual(
1050+
expect.objectContaining({
1051+
response: {
1052+
result: 'mock2',
1053+
},
1054+
}),
1055+
);
1056+
1057+
const unmockedResponse = await request({
1058+
method: 'foo',
1059+
});
1060+
1061+
expect(unmockedResponse).toStrictEqual(
1062+
expect.objectContaining({
1063+
response: {
1064+
error: expect.objectContaining({
1065+
code: -32601,
1066+
message: 'The method "foo" does not exist / is not available.',
1067+
}),
1068+
},
1069+
}),
1070+
);
1071+
1072+
// `close` is deprecated because the Jest environment will automatically
1073+
// close the Snap when the test finishes. However, we still need to close
1074+
// the Snap in this test because it's run outside the Jest environment.
1075+
await close();
1076+
await closeServer();
1077+
});
8781078
});
8791079
});

0 commit comments

Comments
 (0)