@prosopo/provider-mock 2.8.141 → 2.8.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +8 -6
  2. package/.turbo/turbo-build$colon$tsc.log +26 -26
  3. package/.turbo/turbo-build.log +9 -7
  4. package/CHANGELOG.md +62 -0
  5. package/Dockerfile +11 -0
  6. package/dist/api.d.ts +35 -2
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/api.js +97 -67
  9. package/dist/api.js.map +1 -1
  10. package/dist/cjs/api.cjs +105 -66
  11. package/dist/cjs/db.cjs +16 -7
  12. package/dist/cjs/isMain.cjs +16 -0
  13. package/dist/cjs/start.cjs +59 -16
  14. package/dist/cjs/verify.cjs +39 -0
  15. package/dist/db.d.ts +8 -4
  16. package/dist/db.d.ts.map +1 -1
  17. package/dist/db.js +16 -7
  18. package/dist/db.js.map +1 -1
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +5 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/isMain.d.ts +2 -0
  24. package/dist/isMain.d.ts.map +1 -0
  25. package/dist/isMain.js +16 -0
  26. package/dist/isMain.js.map +1 -0
  27. package/dist/start.d.ts +17 -0
  28. package/dist/start.d.ts.map +1 -1
  29. package/dist/start.js +54 -17
  30. package/dist/start.js.map +1 -1
  31. package/dist/tests/db.unit.test.d.ts +2 -0
  32. package/dist/tests/db.unit.test.d.ts.map +1 -0
  33. package/dist/tests/db.unit.test.js +184 -0
  34. package/dist/tests/db.unit.test.js.map +1 -0
  35. package/dist/tests/fixtures.d.ts +33 -0
  36. package/dist/tests/fixtures.d.ts.map +1 -0
  37. package/dist/tests/fixtures.js +55 -0
  38. package/dist/tests/fixtures.js.map +1 -0
  39. package/dist/tests/handlers.unit.test.d.ts +2 -0
  40. package/dist/tests/handlers.unit.test.d.ts.map +1 -0
  41. package/dist/tests/handlers.unit.test.js +204 -0
  42. package/dist/tests/handlers.unit.test.js.map +1 -0
  43. package/dist/tests/providerMock.test-d.d.ts +2 -0
  44. package/dist/tests/providerMock.test-d.d.ts.map +1 -0
  45. package/dist/tests/providerMock.test-d.js +88 -0
  46. package/dist/tests/providerMock.test-d.js.map +1 -0
  47. package/dist/tests/start.unit.test.d.ts +2 -0
  48. package/dist/tests/start.unit.test.d.ts.map +1 -0
  49. package/dist/tests/start.unit.test.js +205 -0
  50. package/dist/tests/start.unit.test.js.map +1 -0
  51. package/dist/tests/verify.unit.test.d.ts +2 -0
  52. package/dist/tests/verify.unit.test.d.ts.map +1 -0
  53. package/dist/tests/verify.unit.test.js +63 -0
  54. package/dist/tests/verify.unit.test.js.map +1 -0
  55. package/dist/tests/wiring.unit.test.d.ts +2 -0
  56. package/dist/tests/wiring.unit.test.d.ts.map +1 -0
  57. package/dist/tests/wiring.unit.test.js +181 -0
  58. package/dist/tests/wiring.unit.test.js.map +1 -0
  59. package/dist/verify.d.ts +13 -0
  60. package/dist/verify.d.ts.map +1 -0
  61. package/dist/verify.js +34 -0
  62. package/dist/verify.js.map +1 -0
  63. package/package.json +19 -11
  64. package/src/api.ts +170 -79
  65. package/src/db.ts +39 -12
  66. package/src/index.ts +18 -0
  67. package/src/isMain.ts +37 -0
  68. package/src/start.ts +92 -18
  69. package/src/tests/db.unit.test.ts +287 -0
  70. package/src/tests/fixtures.ts +117 -0
  71. package/src/tests/handlers.unit.test.ts +325 -0
  72. package/src/tests/providerMock.test-d.ts +165 -0
  73. package/src/tests/start.unit.test.ts +291 -0
  74. package/src/tests/verify.unit.test.ts +143 -0
  75. package/src/tests/wiring.unit.test.ts +293 -0
  76. package/src/verify.ts +63 -0
  77. package/tsconfig.tsbuildinfo +1 -1
  78. package/vite.test.config.ts +32 -0
@@ -0,0 +1,291 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { EventEmitter } from "node:events";
16
+ import type { Server } from "node:http";
17
+ import { getLogger } from "@prosopo/logger";
18
+ import type { Express, RequestHandler } from "express";
19
+ import {
20
+ type Mock,
21
+ afterEach,
22
+ beforeEach,
23
+ describe,
24
+ expect,
25
+ test,
26
+ vi,
27
+ } from "vitest";
28
+ import {
29
+ DEFAULT_API_PORT,
30
+ type StartDeps,
31
+ defaultStartDeps,
32
+ main,
33
+ readPort,
34
+ startApi,
35
+ } from "../start.js";
36
+
37
+ interface AppMock {
38
+ use: Mock<(handler: unknown) => Express>;
39
+ listen: Mock<(port: number) => Server>;
40
+ /** Make the next listen() report a failed bind instead of a live socket. */
41
+ failWith: (error: Error) => void;
42
+ app: Express;
43
+ }
44
+
45
+ const createAppMock = (): AppMock => {
46
+ const use = vi.fn<(handler: unknown) => Express>();
47
+ // startApi waits on the server's events, so the mock has to be a real
48
+ // emitter rather than a callback: a bind failure only ever arrives as an
49
+ // "error" event.
50
+ let failure: Error | undefined;
51
+ const listen = vi.fn<(port: number) => Server>(() => {
52
+ const server = new EventEmitter();
53
+ setImmediate(() => {
54
+ server.emit(failure ? "error" : "listening", failure);
55
+ });
56
+ return server as unknown as Server;
57
+ });
58
+ const app = { use, listen } as unknown as Express;
59
+ use.mockReturnValue(app);
60
+ return {
61
+ use,
62
+ listen,
63
+ failWith: (error: Error): void => {
64
+ failure = error;
65
+ },
66
+ app,
67
+ };
68
+ };
69
+
70
+ interface StartMocks {
71
+ deps: StartDeps;
72
+ app: AppMock;
73
+ i18n: Mock<() => Promise<RequestHandler>>;
74
+ router: Mock<() => ReturnType<StartDeps["router"]>>;
75
+ exit: Mock<(code: number) => void>;
76
+ }
77
+
78
+ const createStartDeps = (overrides: Partial<StartDeps> = {}): StartMocks => {
79
+ const app = createAppMock();
80
+ const middleware: RequestHandler = (_req, _res, next): void => {
81
+ next();
82
+ };
83
+ const i18n = vi.fn<() => Promise<RequestHandler>>(async () => middleware);
84
+ const router = vi.fn<() => ReturnType<StartDeps["router"]>>(
85
+ () => middleware as unknown as ReturnType<StartDeps["router"]>,
86
+ );
87
+ const exit = vi.fn<(code: number) => void>();
88
+ return {
89
+ app,
90
+ i18n,
91
+ router,
92
+ exit,
93
+ deps: {
94
+ createApp: (): Express => app.app,
95
+ i18n,
96
+ router,
97
+ logger: getLogger("fatal", "provider-mock:test"),
98
+ port: DEFAULT_API_PORT,
99
+ exit,
100
+ ...overrides,
101
+ },
102
+ };
103
+ };
104
+
105
+ const saved: Record<string, string | undefined> = {};
106
+
107
+ const setEnv = (name: string, value: string | undefined): void => {
108
+ if (!(name in saved)) {
109
+ saved[name] = process.env[name];
110
+ }
111
+ if (value === undefined) {
112
+ Reflect.deleteProperty(process.env, name);
113
+ } else {
114
+ process.env[name] = value;
115
+ }
116
+ };
117
+
118
+ afterEach(() => {
119
+ for (const [name, value] of Object.entries(saved)) {
120
+ if (value === undefined) {
121
+ Reflect.deleteProperty(process.env, name);
122
+ } else {
123
+ process.env[name] = value;
124
+ }
125
+ }
126
+ vi.restoreAllMocks();
127
+ });
128
+
129
+ describe("readPort", () => {
130
+ test("defaults when nothing is set", () => {
131
+ expect(readPort({})).toBe(DEFAULT_API_PORT);
132
+ });
133
+
134
+ test("uses the configured port", () => {
135
+ expect(readPort({ PROVIDER_MOCK_PORT: "3000" })).toBe(3000);
136
+ });
137
+
138
+ test("port 0 means let the OS choose, and is honoured", () => {
139
+ expect(readPort({ PROVIDER_MOCK_PORT: "0" })).toBe(0);
140
+ });
141
+
142
+ test("the highest port is allowed", () => {
143
+ expect(readPort({ PROVIDER_MOCK_PORT: "65535" })).toBe(65535);
144
+ });
145
+
146
+ const rejected: string[] = [
147
+ "",
148
+ " ",
149
+ "http",
150
+ "80.5",
151
+ "-1",
152
+ "65536",
153
+ "NaN",
154
+ "Infinity",
155
+ ];
156
+
157
+ for (const value of rejected) {
158
+ test(`falls back to the default for ${JSON.stringify(value)}`, () => {
159
+ // A value listen() cannot use would either throw at startup or bind
160
+ // somewhere unexpected; the default is the safer answer.
161
+ expect(readPort({ PROVIDER_MOCK_PORT: value })).toBe(DEFAULT_API_PORT);
162
+ });
163
+ }
164
+ });
165
+
166
+ describe("startApi", () => {
167
+ let mocks: StartMocks;
168
+
169
+ beforeEach(() => {
170
+ mocks = createStartDeps();
171
+ });
172
+
173
+ test("listens on the configured port", async () => {
174
+ mocks = createStartDeps({ port: 4321 });
175
+ await startApi(mocks.deps);
176
+ expect(mocks.app.listen).toHaveBeenCalledWith(4321);
177
+ });
178
+
179
+ test("mounts cors, the body parser, i18n, the router and the error handler", async () => {
180
+ await startApi(mocks.deps);
181
+ expect(mocks.app.use).toHaveBeenCalledTimes(5);
182
+ });
183
+
184
+ test("mounts the error handler last, or it would catch nothing", async () => {
185
+ await startApi(mocks.deps);
186
+ const handlers = mocks.app.use.mock.calls.map((call) => call[0]);
187
+ expect(handlers).toHaveLength(5);
188
+ expect(handlers.at(-1)).toBeInstanceOf(Function);
189
+ });
190
+
191
+ test("mounts i18n before the router, so req.t exists in the handlers", async () => {
192
+ await startApi(mocks.deps);
193
+ const middleware = await mocks.i18n.mock.results[0]?.value;
194
+ const handlers = mocks.app.use.mock.calls.map((call) => call[0]);
195
+ expect(handlers.indexOf(middleware)).toBeLessThan(handlers.length - 1);
196
+ expect(mocks.i18n).toHaveBeenCalledOnce();
197
+ expect(mocks.router).toHaveBeenCalledOnce();
198
+ });
199
+
200
+ test("returns the app, so a caller can shut it down", async () => {
201
+ expect(await startApi(mocks.deps)).toBe(mocks.app.app);
202
+ });
203
+
204
+ test("a failure to load translations stops startup", async () => {
205
+ mocks.i18n.mockRejectedValue(new Error("no locales"));
206
+ await expect(startApi(mocks.deps)).rejects.toThrow("no locales");
207
+ expect(mocks.app.listen).not.toHaveBeenCalled();
208
+ });
209
+
210
+ test("a router that cannot be built stops startup", async () => {
211
+ mocks.router.mockImplementation(() => {
212
+ throw new Error("bad mongo url");
213
+ });
214
+ await expect(startApi(mocks.deps)).rejects.toThrow("bad mongo url");
215
+ expect(mocks.app.listen).not.toHaveBeenCalled();
216
+ });
217
+
218
+ test("a port already in use is propagated", async () => {
219
+ // The bind failure arrives on the server's error event well after listen()
220
+ // has returned, so startApi has to wait for it rather than resolve first.
221
+ mocks.app.failWith(new Error("EADDRINUSE"));
222
+ await expect(startApi(mocks.deps)).rejects.toThrow("EADDRINUSE");
223
+ });
224
+
225
+ test("does not resolve before the socket is listening", async () => {
226
+ // Resolving early would let a caller — or a container healthcheck — treat
227
+ // the mock as up while nothing is bound yet.
228
+ let listening = false;
229
+ mocks.app.listen.mockImplementation(() => {
230
+ const server = new EventEmitter();
231
+ setImmediate(() => {
232
+ listening = true;
233
+ server.emit("listening");
234
+ });
235
+ return server as unknown as Server;
236
+ });
237
+ await startApi(mocks.deps);
238
+ expect(listening).toBe(true);
239
+ });
240
+ });
241
+
242
+ describe("main", () => {
243
+ test("starts the api and does not exit", async () => {
244
+ const mocks = createStartDeps();
245
+ await main(mocks.deps);
246
+ expect(mocks.app.listen).toHaveBeenCalledOnce();
247
+ expect(mocks.exit).not.toHaveBeenCalled();
248
+ });
249
+
250
+ test("exits non-zero when startup fails", async () => {
251
+ // Resolving quietly would leave a container marked healthy with nothing
252
+ // listening in it.
253
+ const mocks = createStartDeps();
254
+ mocks.i18n.mockRejectedValue(new Error("no locales"));
255
+ await main(mocks.deps);
256
+ expect(mocks.exit).toHaveBeenCalledWith(1);
257
+ });
258
+
259
+ test("does not reject, so the exit code is the one it chose", async () => {
260
+ const mocks = createStartDeps();
261
+ mocks.i18n.mockRejectedValue(new Error("no locales"));
262
+ await expect(main(mocks.deps)).resolves.toBeUndefined();
263
+ });
264
+
265
+ test("survives a thrown non-Error", async () => {
266
+ const mocks = createStartDeps();
267
+ mocks.i18n.mockRejectedValue("no locales");
268
+ await main(mocks.deps);
269
+ expect(mocks.exit).toHaveBeenCalledWith(1);
270
+ });
271
+ });
272
+
273
+ describe("defaultStartDeps", () => {
274
+ test("takes the port from the environment", () => {
275
+ setEnv("PROVIDER_MOCK_PORT", "5555");
276
+ expect(defaultStartDeps().port).toBe(5555);
277
+ });
278
+
279
+ test("defaults the port when the environment says nothing", () => {
280
+ setEnv("PROVIDER_MOCK_PORT", undefined);
281
+ expect(defaultStartDeps().port).toBe(DEFAULT_API_PORT);
282
+ });
283
+
284
+ test("supplies every dependency startApi needs", () => {
285
+ const deps: StartDeps = defaultStartDeps();
286
+ expect(typeof deps.createApp).toBe("function");
287
+ expect(typeof deps.i18n).toBe("function");
288
+ expect(deps.router).toBeInstanceOf(Function);
289
+ expect(typeof deps.exit).toBe("function");
290
+ });
291
+ });
@@ -0,0 +1,143 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { describe, expect, test } from "vitest";
16
+ import {
17
+ NOT_VERIFIED_MESSAGE,
18
+ TEST_ACCOUNT,
19
+ TEST_COMMITMENT_ID,
20
+ TEST_DAPP,
21
+ VERIFIED_MESSAGE,
22
+ type VerificationOutcome,
23
+ verifyProcaptchaOutput,
24
+ } from "../verify.js";
25
+ import { createOutput } from "./fixtures.js";
26
+
27
+ describe("verifyProcaptchaOutput", () => {
28
+ test("approves the test account", () => {
29
+ const outcome: VerificationOutcome = verifyProcaptchaOutput(
30
+ createOutput({ user: TEST_ACCOUNT, dapp: "other" }),
31
+ );
32
+ expect(outcome).toEqual({
33
+ statusMessage: VERIFIED_MESSAGE,
34
+ verified: true,
35
+ commitmentId: TEST_COMMITMENT_ID,
36
+ });
37
+ });
38
+
39
+ test("approves the test dapp", () => {
40
+ expect(
41
+ verifyProcaptchaOutput(createOutput({ user: "other", dapp: TEST_DAPP }))
42
+ .verified,
43
+ ).toBe(true);
44
+ });
45
+
46
+ test("approves the test commitment", () => {
47
+ expect(
48
+ verifyProcaptchaOutput(
49
+ createOutput({
50
+ user: "other",
51
+ dapp: "other",
52
+ commitmentId: TEST_COMMITMENT_ID,
53
+ }),
54
+ ).verified,
55
+ ).toBe(true);
56
+ });
57
+
58
+ test("any one fixture is enough — a token never carries all three", () => {
59
+ // Requiring every field to match would approve nothing at all, since
60
+ // commitmentId is optional in a decoded token.
61
+ const outcome = verifyProcaptchaOutput(
62
+ createOutput({
63
+ user: TEST_ACCOUNT,
64
+ dapp: "other",
65
+ commitmentId: "other",
66
+ }),
67
+ );
68
+ expect(outcome.verified).toBe(true);
69
+ });
70
+
71
+ test("rejects a token that matches nothing", () => {
72
+ expect(
73
+ verifyProcaptchaOutput(
74
+ createOutput({ user: "someone", dapp: "elsewhere" }),
75
+ ),
76
+ ).toEqual({ statusMessage: NOT_VERIFIED_MESSAGE, verified: false });
77
+ });
78
+
79
+ test("a rejection carries no commitment id", () => {
80
+ // Returning the test commitment alongside verified:false would let a
81
+ // caller that only reads commitmentId treat a rejection as a pass.
82
+ expect(
83
+ verifyProcaptchaOutput(
84
+ createOutput({ user: "someone", dapp: "elsewhere" }),
85
+ ).commitmentId,
86
+ ).toBeUndefined();
87
+ });
88
+
89
+ test("empty fields do not match", () => {
90
+ // An absent field decodes to "", which must not satisfy a comparison.
91
+ expect(
92
+ verifyProcaptchaOutput(
93
+ createOutput({ user: "", dapp: "", commitmentId: "" }),
94
+ ).verified,
95
+ ).toBe(false);
96
+ });
97
+
98
+ test("an absent commitment id does not match", () => {
99
+ expect(
100
+ verifyProcaptchaOutput(
101
+ createOutput({ user: "a", dapp: "b", commitmentId: undefined }),
102
+ ).verified,
103
+ ).toBe(false);
104
+ });
105
+
106
+ test("matching is exact, not a prefix", () => {
107
+ expect(
108
+ verifyProcaptchaOutput(
109
+ createOutput({ user: `${TEST_ACCOUNT}x`, dapp: "other" }),
110
+ ).verified,
111
+ ).toBe(false);
112
+ expect(
113
+ verifyProcaptchaOutput(
114
+ createOutput({ user: TEST_ACCOUNT.slice(0, -1), dapp: "other" }),
115
+ ).verified,
116
+ ).toBe(false);
117
+ });
118
+
119
+ test("matching is case sensitive", () => {
120
+ expect(
121
+ verifyProcaptchaOutput(
122
+ createOutput({ user: TEST_ACCOUNT.toLowerCase(), dapp: "other" }),
123
+ ).verified,
124
+ ).toBe(false);
125
+ });
126
+
127
+ test("the fixtures are the ones the integration suites sign with", () => {
128
+ // Pinned: changing either address silently stops every integration test
129
+ // from being approved.
130
+ expect(TEST_ACCOUNT).toBe(
131
+ "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
132
+ );
133
+ expect(TEST_DAPP).toBe("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM");
134
+ expect(TEST_COMMITMENT_ID).toBe("0x123456789test");
135
+ });
136
+
137
+ test("does not mutate the output it was given", () => {
138
+ const output = createOutput();
139
+ const before = structuredClone(output);
140
+ verifyProcaptchaOutput(output);
141
+ expect(output).toEqual(before);
142
+ });
143
+ });