@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,325 @@
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 { ProsopoApiError } from "@prosopo/common";
16
+ import { beforeEach, describe, expect, test, vi } from "vitest";
17
+ import {
18
+ type ApiNext,
19
+ createTestHandler,
20
+ createVerifyHandler,
21
+ } from "../api.js";
22
+ import {
23
+ NOT_VERIFIED_MESSAGE,
24
+ TEST_COMMITMENT_ID,
25
+ VERIFIED_MESSAGE,
26
+ } from "../verify.js";
27
+ import {
28
+ type DepsMock,
29
+ type ResponseMock,
30
+ createBody,
31
+ createDeps,
32
+ createOutput,
33
+ createRequest,
34
+ createResponse,
35
+ } from "./fixtures.js";
36
+
37
+ let mocks: DepsMock;
38
+ let response: ResponseMock;
39
+ let next: ApiNext;
40
+ let nextCalls: unknown[];
41
+
42
+ beforeEach(() => {
43
+ mocks = createDeps();
44
+ response = createResponse();
45
+ nextCalls = [];
46
+ next = (error: unknown): void => {
47
+ nextCalls.push(error);
48
+ };
49
+ });
50
+
51
+ const errorCode = (error: unknown): unknown =>
52
+ error instanceof ProsopoApiError ? error.context?.code : undefined;
53
+
54
+ describe("the verify handler", () => {
55
+ test("approves a token carrying a fixture", async () => {
56
+ await createVerifyHandler(mocks.deps)(createRequest(), response.res, next);
57
+ expect(response.json).toHaveBeenCalledWith({
58
+ status: `translated:${VERIFIED_MESSAGE}`,
59
+ verified: true,
60
+ commitmentId: TEST_COMMITMENT_ID,
61
+ });
62
+ expect(nextCalls).toEqual([]);
63
+ });
64
+
65
+ test("rejects a token carrying nothing known", async () => {
66
+ mocks.decodeToken.mockReturnValue(
67
+ createOutput({ user: "nobody", dapp: "nowhere" }),
68
+ );
69
+ await createVerifyHandler(mocks.deps)(createRequest(), response.res, next);
70
+ expect(response.json).toHaveBeenCalledWith({
71
+ status: `translated:${NOT_VERIFIED_MESSAGE}`,
72
+ verified: false,
73
+ });
74
+ });
75
+
76
+ test("a rejection response has no commitmentId key at all", async () => {
77
+ // Not even set to undefined: JSON.stringify would drop it, but a caller
78
+ // reading the object directly would see the key and could misread it.
79
+ mocks.decodeToken.mockReturnValue(
80
+ createOutput({ user: "nobody", dapp: "nowhere" }),
81
+ );
82
+ await createVerifyHandler(mocks.deps)(createRequest(), response.res, next);
83
+ const body = response.json.mock.calls[0]?.[0];
84
+ expect(body).toBeDefined();
85
+ expect(Object.keys(body as object)).toEqual(["status", "verified"]);
86
+ });
87
+
88
+ test("decodes the token that was sent, not some other field", async () => {
89
+ await createVerifyHandler(mocks.deps)(
90
+ createRequest({ body: createBody({ token: "0xabc123" }) }),
91
+ response.res,
92
+ next,
93
+ );
94
+ expect(mocks.decodeToken).toHaveBeenCalledWith("0xabc123");
95
+ });
96
+
97
+ test("translates the status message through the request", async () => {
98
+ const t = vi.fn<(key: string) => string>(() => "Vérifié");
99
+ await createVerifyHandler(mocks.deps)(
100
+ createRequest({ t }),
101
+ response.res,
102
+ next,
103
+ );
104
+ expect(t).toHaveBeenCalledWith(VERIFIED_MESSAGE);
105
+ expect(response.json).toHaveBeenCalledWith(
106
+ expect.objectContaining({ status: "Vérifié" }),
107
+ );
108
+ });
109
+
110
+ describe("a body that does not parse", () => {
111
+ const badBodies: Array<[string, unknown]> = [
112
+ ["missing entirely", undefined],
113
+ ["not an object", "token=0x1"],
114
+ ["null", null],
115
+ ["empty", {}],
116
+ ["missing the token", createBody({ token: undefined })],
117
+ ["missing the signature", createBody({ dappSignature: undefined })],
118
+ ["a token that is not hex", createBody({ token: "deadbeef" })],
119
+ ["a token that is the empty string", createBody({ token: "" })],
120
+ ["a token of the wrong type", createBody({ token: 42 })],
121
+ [
122
+ "a token longer than INPUT_LIMITS.TOKEN",
123
+ createBody({ token: `0x${"a".repeat(131072)}` }),
124
+ ],
125
+ ];
126
+
127
+ for (const [description, body] of badBodies) {
128
+ test(`is a 400 when it is ${description}`, async () => {
129
+ await createVerifyHandler(mocks.deps)(
130
+ createRequest({ body }),
131
+ response.res,
132
+ next,
133
+ );
134
+ expect(nextCalls).toHaveLength(1);
135
+ expect(nextCalls[0]).toBeInstanceOf(ProsopoApiError);
136
+ expect(errorCode(nextCalls[0])).toBe(400);
137
+ expect(response.json).not.toHaveBeenCalled();
138
+ });
139
+ }
140
+
141
+ test("never reaches the decoder", async () => {
142
+ await createVerifyHandler(mocks.deps)(
143
+ createRequest({ body: {} }),
144
+ response.res,
145
+ next,
146
+ );
147
+ expect(mocks.decodeToken).not.toHaveBeenCalled();
148
+ });
149
+ });
150
+
151
+ test("a token that cannot be decoded is a 500", async () => {
152
+ mocks.decodeToken.mockImplementation(() => {
153
+ throw new Error("bad codec");
154
+ });
155
+ await createVerifyHandler(mocks.deps)(createRequest(), response.res, next);
156
+ expect(errorCode(nextCalls[0])).toBe(500);
157
+ expect(response.json).not.toHaveBeenCalled();
158
+ });
159
+
160
+ test("a failure inside the translator is a 500, not a crash", async () => {
161
+ // req.t is added by the i18n middleware; if it is not mounted the handler
162
+ // must still answer the request rather than reject.
163
+ await createVerifyHandler(mocks.deps)(
164
+ createRequest({
165
+ t: (): string => {
166
+ throw new Error("no i18n middleware");
167
+ },
168
+ }),
169
+ response.res,
170
+ next,
171
+ );
172
+ expect(errorCode(nextCalls[0])).toBe(500);
173
+ });
174
+
175
+ test("calls next exactly once, never alongside a response", async () => {
176
+ mocks.decodeToken.mockImplementation(() => {
177
+ throw new Error("bad codec");
178
+ });
179
+ await createVerifyHandler(mocks.deps)(createRequest(), response.res, next);
180
+ expect(nextCalls).toHaveLength(1);
181
+ expect(response.json).not.toHaveBeenCalled();
182
+ });
183
+
184
+ test("resolves rather than rejecting, whatever happens", async () => {
185
+ // An express handler that rejects becomes an unhandled rejection: the
186
+ // request hangs until the client times out.
187
+ mocks.decodeToken.mockImplementation(() => {
188
+ throw new Error("bad codec");
189
+ });
190
+ await expect(
191
+ createVerifyHandler(mocks.deps)(createRequest(), response.res, next),
192
+ ).resolves.toBeUndefined();
193
+ });
194
+
195
+ test("a thrown non-Error is still reported as a 500", async () => {
196
+ mocks.decodeToken.mockImplementation(() => {
197
+ throw "not an error";
198
+ });
199
+ await createVerifyHandler(mocks.deps)(createRequest(), response.res, next);
200
+ expect(errorCode(nextCalls[0])).toBe(500);
201
+ });
202
+ });
203
+
204
+ describe("the test handler", () => {
205
+ test("returns the fingerprint and the user agent", async () => {
206
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
207
+ expect(response.json).toHaveBeenCalledWith({
208
+ ja4: "t13d1516h2_8daaf6152771_b0da82dd1658",
209
+ ua: "Mozilla/5.0",
210
+ });
211
+ });
212
+
213
+ test("stores the fingerprint against the user agent", async () => {
214
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
215
+ expect(mocks.database.addOrUpdateJA4Record).toHaveBeenCalledWith({
216
+ ja4_fingerprint: "t13d1516h2_8daaf6152771_b0da82dd1658",
217
+ user_agent_string: "Mozilla/5.0",
218
+ });
219
+ });
220
+
221
+ test("connects before writing and closes afterwards", async () => {
222
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
223
+ expect(mocks.database.connect).toHaveBeenCalledOnce();
224
+ expect(mocks.database.close).toHaveBeenCalledOnce();
225
+ expect(mocks.database.connect.mock.invocationCallOrder[0]).toBeLessThan(
226
+ mocks.database.addOrUpdateJA4Record.mock.invocationCallOrder[0] ?? 0,
227
+ );
228
+ });
229
+
230
+ test("a request with no user agent stores the empty string", async () => {
231
+ // The unique index covers (fingerprint, user agent), so undefined would
232
+ // make every anonymous client collide on a null key.
233
+ await createTestHandler(mocks.deps)(
234
+ createRequest({ headers: {} }),
235
+ response.res,
236
+ );
237
+ expect(mocks.database.addOrUpdateJA4Record).toHaveBeenCalledWith(
238
+ expect.objectContaining({ user_agent_string: "" }),
239
+ );
240
+ });
241
+
242
+ test("an empty user agent header is stored as the empty string", async () => {
243
+ await createTestHandler(mocks.deps)(
244
+ createRequest({ headers: { "user-agent": "" } }),
245
+ response.res,
246
+ );
247
+ expect(mocks.database.addOrUpdateJA4Record).toHaveBeenCalledWith(
248
+ expect.objectContaining({ user_agent_string: "" }),
249
+ );
250
+ });
251
+
252
+ test("closes the connection even when the write fails", async () => {
253
+ // This is the leak: close used to sit after the write, so a failing write
254
+ // left the connection open for the life of the process.
255
+ mocks.database.addOrUpdateJA4Record.mockRejectedValue(
256
+ new Error("duplicate key"),
257
+ );
258
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
259
+ expect(mocks.database.close).toHaveBeenCalledOnce();
260
+ expect(response.status).toHaveBeenCalledWith(500);
261
+ });
262
+
263
+ test("closes the connection even when connecting fails", async () => {
264
+ mocks.database.connect.mockRejectedValue(new Error("mongo is down"));
265
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
266
+ expect(mocks.database.close).toHaveBeenCalledOnce();
267
+ expect(mocks.database.addOrUpdateJA4Record).not.toHaveBeenCalled();
268
+ expect(response.status).toHaveBeenCalledWith(500);
269
+ });
270
+
271
+ test("does not open a connection when fingerprinting fails", async () => {
272
+ mocks.getJA4.mockRejectedValue(new Error("no ClientHello"));
273
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
274
+ expect(mocks.database.connect).not.toHaveBeenCalled();
275
+ expect(mocks.database.close).not.toHaveBeenCalled();
276
+ expect(response.status).toHaveBeenCalledWith(500);
277
+ });
278
+
279
+ test("a failure to close is itself reported, not swallowed", async () => {
280
+ mocks.database.close.mockRejectedValue(new Error("close failed"));
281
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
282
+ expect(response.status).toHaveBeenCalledWith(500);
283
+ expect(response.json).not.toHaveBeenCalled();
284
+ });
285
+
286
+ test("sends a body with the 500, so the client sees a reason", async () => {
287
+ mocks.getJA4.mockRejectedValue(new Error("no ClientHello"));
288
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
289
+ expect(response.send).toHaveBeenCalledWith(
290
+ "Failed to record the caller's JA4 fingerprint.",
291
+ );
292
+ });
293
+
294
+ test("resolves rather than rejecting when the database is down", async () => {
295
+ mocks.database.connect.mockRejectedValue(new Error("mongo is down"));
296
+ await expect(
297
+ createTestHandler(mocks.deps)(createRequest(), response.res),
298
+ ).resolves.toBeUndefined();
299
+ });
300
+
301
+ test("a thrown non-Error is still a 500", async () => {
302
+ mocks.getJA4.mockRejectedValue("not an error");
303
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
304
+ expect(response.status).toHaveBeenCalledWith(500);
305
+ });
306
+
307
+ test("passes the logger to the fingerprinter", async () => {
308
+ await createTestHandler(mocks.deps)(createRequest(), response.res);
309
+ expect(mocks.getJA4).toHaveBeenCalledWith(
310
+ expect.objectContaining({ "user-agent": "Mozilla/5.0" }),
311
+ mocks.deps.logger,
312
+ );
313
+ });
314
+
315
+ test("a duplicate write does not stop the next request working", async () => {
316
+ mocks.database.addOrUpdateJA4Record.mockRejectedValueOnce(
317
+ new Error("duplicate key"),
318
+ );
319
+ const handler = createTestHandler(mocks.deps);
320
+ await handler(createRequest(), response.res);
321
+ const second = createResponse();
322
+ await handler(createRequest(), second.res);
323
+ expect(second.json).toHaveBeenCalledOnce();
324
+ });
325
+ });
@@ -0,0 +1,165 @@
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 type { IncomingHttpHeaders } from "node:http";
16
+ import type { ProcaptchaOutput } from "@prosopo/types";
17
+ import type { RequestHandler, Router } from "express";
18
+ import { assertType, describe, expectTypeOf, test } from "vitest";
19
+ import {
20
+ type ApiNext,
21
+ type ApiRequest,
22
+ type ApiResponse,
23
+ type RouterDeps,
24
+ createDatabase,
25
+ createTestHandler,
26
+ createVerifyHandler,
27
+ defaultRouterDeps,
28
+ prosopoRouter,
29
+ toRequestHandler,
30
+ } from "../api.js";
31
+ import type { JA4Data, JA4Database, JA4Store } from "../db.js";
32
+ import { isMain } from "../isMain.js";
33
+ import type { VerificationOutcome } from "../verify.js";
34
+ import { verifyProcaptchaOutput } from "../verify.js";
35
+
36
+ describe("the handlers", () => {
37
+ test("are usable as express middleware once adapted", () => {
38
+ // The narrow request and response types exist so the handlers can be
39
+ // called with plain objects in tests; toRequestHandler is the only place
40
+ // that has to know they are really express objects.
41
+ assertType<RequestHandler>(
42
+ toRequestHandler(createVerifyHandler(defaultRouterDeps())),
43
+ );
44
+ assertType<RequestHandler>(
45
+ toRequestHandler(createTestHandler(defaultRouterDeps())),
46
+ );
47
+ });
48
+
49
+ test("a handler that does not take a next is still adaptable", () => {
50
+ // The test route answers everything itself, so it must not be forced to
51
+ // declare a parameter it never uses.
52
+ expectTypeOf(toRequestHandler).toBeCallableWith(
53
+ async (): Promise<void> => undefined,
54
+ );
55
+ });
56
+
57
+ test("resolve rather than returning a response object", () => {
58
+ // Returning the response would let a handler accidentally reply twice.
59
+ expectTypeOf(createVerifyHandler).returns.returns.toEqualTypeOf<
60
+ Promise<void>
61
+ >();
62
+ expectTypeOf(createTestHandler).returns.returns.toEqualTypeOf<
63
+ Promise<void>
64
+ >();
65
+ });
66
+
67
+ test("treat the request body as unknown until it is parsed", () => {
68
+ // Typing it as the parsed shape would let the schema check be skipped.
69
+ expectTypeOf<ApiRequest["body"]>().toEqualTypeOf<unknown>();
70
+ expectTypeOf<ApiRequest["headers"]>().toEqualTypeOf<IncomingHttpHeaders>();
71
+ expectTypeOf<ApiRequest["t"]>().toEqualTypeOf<(key: string) => string>();
72
+ });
73
+
74
+ test("can only send an object as json", () => {
75
+ expectTypeOf<ApiResponse["json"]>().parameter(0).toEqualTypeOf<object>();
76
+ // @ts-expect-error a bare string is not a json body
77
+ assertType<Parameters<ApiResponse["json"]>[0]>("done");
78
+ });
79
+
80
+ test("pass an unknown error to next, not a fixed error class", () => {
81
+ expectTypeOf<ApiNext>().toEqualTypeOf<(error: unknown) => void>();
82
+ });
83
+ });
84
+
85
+ describe("RouterDeps", () => {
86
+ test("depends on the narrow store, not the mongo class", () => {
87
+ // A test double must be able to satisfy it without a mongo connection.
88
+ expectTypeOf<RouterDeps["db"]>().toEqualTypeOf<JA4Store>();
89
+ expectTypeOf<JA4Database>().toExtend<JA4Store>();
90
+ });
91
+
92
+ test("the store's write can report that nothing was found", () => {
93
+ expectTypeOf<JA4Store["addOrUpdateJA4Record"]>()
94
+ .parameter(0)
95
+ .toEqualTypeOf<JA4Data>();
96
+ expectTypeOf<JA4Store["connect"]>().toEqualTypeOf<() => Promise<void>>();
97
+ expectTypeOf<JA4Store["close"]>().toEqualTypeOf<() => Promise<void>>();
98
+ });
99
+
100
+ test("a record needs a fingerprint and a user agent, and nothing else", () => {
101
+ assertType<JA4Data>({ ja4_fingerprint: "fp", user_agent_string: "ua" });
102
+ // @ts-expect-error the fingerprint is what the record is keyed by
103
+ assertType<JA4Data>({ user_agent_string: "ua" });
104
+ // @ts-expect-error the user agent is the other half of the unique index
105
+ assertType<JA4Data>({ ja4_fingerprint: "fp" });
106
+ });
107
+
108
+ test("every dependency is required", () => {
109
+ const partial: Pick<RouterDeps, "logger"> = { logger: null as never };
110
+ // @ts-expect-error db, getJA4 and decodeToken are not optional
111
+ assertType<RouterDeps>(partial);
112
+ });
113
+
114
+ test("defaultRouterDeps takes no arguments and supplies them all", () => {
115
+ expectTypeOf(defaultRouterDeps).toEqualTypeOf<() => RouterDeps>();
116
+ });
117
+ });
118
+
119
+ describe("the router", () => {
120
+ test("can be built with or without dependencies", () => {
121
+ expectTypeOf(prosopoRouter).returns.toEqualTypeOf<Router>();
122
+ assertType<Router>(prosopoRouter());
123
+ assertType<Router>(prosopoRouter(defaultRouterDeps()));
124
+ });
125
+
126
+ test("createDatabase reads an environment, not a config object", () => {
127
+ expectTypeOf(createDatabase).returns.toEqualTypeOf<JA4Database>();
128
+ expectTypeOf(createDatabase)
129
+ .parameter(0)
130
+ .toEqualTypeOf<NodeJS.ProcessEnv | undefined>();
131
+ });
132
+ });
133
+
134
+ describe("verifyProcaptchaOutput", () => {
135
+ test("needs only the three fields it inspects", () => {
136
+ // Requiring a whole ProcaptchaOutput would force callers to invent a
137
+ // signature and a timestamp that have no bearing on the answer.
138
+ expectTypeOf(verifyProcaptchaOutput)
139
+ .parameter(0)
140
+ .toEqualTypeOf<
141
+ Pick<ProcaptchaOutput, "user" | "dapp" | "commitmentId">
142
+ >();
143
+ assertType<VerificationOutcome>(
144
+ verifyProcaptchaOutput({ user: "u", dapp: "d", commitmentId: undefined }),
145
+ );
146
+ });
147
+
148
+ test("reports a commitment only when there is one", () => {
149
+ expectTypeOf<VerificationOutcome["commitmentId"]>().toEqualTypeOf<
150
+ string | undefined
151
+ >();
152
+ expectTypeOf<VerificationOutcome["verified"]>().toEqualTypeOf<boolean>();
153
+ expectTypeOf<
154
+ VerificationOutcome["statusMessage"]
155
+ >().toEqualTypeOf<string>();
156
+ });
157
+ });
158
+
159
+ describe("isMain", () => {
160
+ test("the entrypoint is optional and defaults to argv", () => {
161
+ expectTypeOf(isMain).toBeCallableWith("file:///a.js");
162
+ expectTypeOf(isMain).toBeCallableWith("file:///a.js", "/a.js");
163
+ expectTypeOf(isMain).returns.toEqualTypeOf<boolean>();
164
+ });
165
+ });