@prosopo/provider-mock 2.8.143 → 2.8.145
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.
- package/.turbo/turbo-build$colon$cjs.log +8 -6
- package/.turbo/turbo-build$colon$tsc.log +26 -26
- package/.turbo/turbo-build.log +9 -7
- package/CHANGELOG.md +52 -0
- package/Dockerfile +11 -0
- package/dist/api.d.ts +35 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +97 -67
- package/dist/api.js.map +1 -1
- package/dist/cjs/api.cjs +105 -66
- package/dist/cjs/db.cjs +16 -7
- package/dist/cjs/isMain.cjs +16 -0
- package/dist/cjs/start.cjs +59 -16
- package/dist/cjs/verify.cjs +39 -0
- package/dist/db.d.ts +8 -4
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +16 -7
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/isMain.d.ts +2 -0
- package/dist/isMain.d.ts.map +1 -0
- package/dist/isMain.js +16 -0
- package/dist/isMain.js.map +1 -0
- package/dist/start.d.ts +17 -0
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +54 -17
- package/dist/start.js.map +1 -1
- package/dist/tests/db.unit.test.d.ts +2 -0
- package/dist/tests/db.unit.test.d.ts.map +1 -0
- package/dist/tests/db.unit.test.js +184 -0
- package/dist/tests/db.unit.test.js.map +1 -0
- package/dist/tests/fixtures.d.ts +33 -0
- package/dist/tests/fixtures.d.ts.map +1 -0
- package/dist/tests/fixtures.js +55 -0
- package/dist/tests/fixtures.js.map +1 -0
- package/dist/tests/handlers.unit.test.d.ts +2 -0
- package/dist/tests/handlers.unit.test.d.ts.map +1 -0
- package/dist/tests/handlers.unit.test.js +204 -0
- package/dist/tests/handlers.unit.test.js.map +1 -0
- package/dist/tests/providerMock.test-d.d.ts +2 -0
- package/dist/tests/providerMock.test-d.d.ts.map +1 -0
- package/dist/tests/providerMock.test-d.js +88 -0
- package/dist/tests/providerMock.test-d.js.map +1 -0
- package/dist/tests/start.unit.test.d.ts +2 -0
- package/dist/tests/start.unit.test.d.ts.map +1 -0
- package/dist/tests/start.unit.test.js +205 -0
- package/dist/tests/start.unit.test.js.map +1 -0
- package/dist/tests/verify.unit.test.d.ts +2 -0
- package/dist/tests/verify.unit.test.d.ts.map +1 -0
- package/dist/tests/verify.unit.test.js +63 -0
- package/dist/tests/verify.unit.test.js.map +1 -0
- package/dist/tests/wiring.unit.test.d.ts +2 -0
- package/dist/tests/wiring.unit.test.d.ts.map +1 -0
- package/dist/tests/wiring.unit.test.js +181 -0
- package/dist/tests/wiring.unit.test.js.map +1 -0
- package/dist/verify.d.ts +13 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +34 -0
- package/dist/verify.js.map +1 -0
- package/package.json +20 -12
- package/src/api.ts +170 -79
- package/src/db.ts +39 -12
- package/src/index.ts +18 -0
- package/src/isMain.ts +37 -0
- package/src/start.ts +92 -18
- package/src/tests/db.unit.test.ts +287 -0
- package/src/tests/fixtures.ts +117 -0
- package/src/tests/handlers.unit.test.ts +325 -0
- package/src/tests/providerMock.test-d.ts +165 -0
- package/src/tests/start.unit.test.ts +291 -0
- package/src/tests/verify.unit.test.ts +143 -0
- package/src/tests/wiring.unit.test.ts +293 -0
- package/src/verify.ts +63 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +32 -0
|
@@ -0,0 +1,293 @@
|
|
|
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 path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { ClientApiPaths } from "@prosopo/types";
|
|
18
|
+
import type { Request, Response } from "express";
|
|
19
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
20
|
+
import {
|
|
21
|
+
type ApiNext,
|
|
22
|
+
type ApiRequest,
|
|
23
|
+
type ApiResponse,
|
|
24
|
+
DEFAULT_MONGO_AUTH_SOURCE,
|
|
25
|
+
DEFAULT_MONGO_DBNAME,
|
|
26
|
+
DEFAULT_MONGO_URL,
|
|
27
|
+
type RouterDeps,
|
|
28
|
+
createDatabase,
|
|
29
|
+
defaultRouterDeps,
|
|
30
|
+
prosopoRouter,
|
|
31
|
+
toRequestHandler,
|
|
32
|
+
} from "../api.js";
|
|
33
|
+
import { JA4Database } from "../db.js";
|
|
34
|
+
import { isMain } from "../isMain.js";
|
|
35
|
+
import { defaultStartDeps } from "../start.js";
|
|
36
|
+
import { createDeps, createRequest, createResponse } from "./fixtures.js";
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("createDatabase", () => {
|
|
43
|
+
test("falls back to a local mongo when nothing is configured", () => {
|
|
44
|
+
const db: JA4Database = createDatabase({});
|
|
45
|
+
expect(db).toBeInstanceOf(JA4Database);
|
|
46
|
+
expect(db.url).toContain("localhost:27017");
|
|
47
|
+
expect(db.url).toContain(`authSource=${DEFAULT_MONGO_AUTH_SOURCE}`);
|
|
48
|
+
expect(db.dbname).toBe(DEFAULT_MONGO_DBNAME);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("uses the configured connection", () => {
|
|
52
|
+
const db = createDatabase({
|
|
53
|
+
MONGO_URL: "mongodb://mongo:27018",
|
|
54
|
+
MONGO_DBNAME: "other",
|
|
55
|
+
MONGO_AUTH_SOURCE: "users",
|
|
56
|
+
});
|
|
57
|
+
expect(db.url).toContain("mongo:27018");
|
|
58
|
+
expect(db.dbname).toBe("other");
|
|
59
|
+
expect(db.url).toContain("authSource=users");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("an empty value is treated as unset, not as an empty url", () => {
|
|
63
|
+
// An empty MONGO_URL in a compose file is how "not configured" is usually
|
|
64
|
+
// spelt; passing it through would produce an unparseable url.
|
|
65
|
+
const db = createDatabase({
|
|
66
|
+
MONGO_URL: "",
|
|
67
|
+
MONGO_DBNAME: "",
|
|
68
|
+
MONGO_AUTH_SOURCE: "",
|
|
69
|
+
});
|
|
70
|
+
expect(db.url).toContain(DEFAULT_MONGO_URL.replace("mongodb://", ""));
|
|
71
|
+
expect(db.dbname).toBe(DEFAULT_MONGO_DBNAME);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("credentials are kept out of the logged url", () => {
|
|
75
|
+
const db = createDatabase({
|
|
76
|
+
MONGO_URL: "mongodb://user:secret@mongo:27017",
|
|
77
|
+
});
|
|
78
|
+
expect(db.safeURL).not.toContain("secret");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("defaults to the process environment", () => {
|
|
82
|
+
expect(createDatabase()).toBeInstanceOf(JA4Database);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("does not connect on construction", () => {
|
|
86
|
+
// Building the router must not need mongo to be up, or the mock would
|
|
87
|
+
// refuse to start before its database container was ready.
|
|
88
|
+
const connect = vi.spyOn(JA4Database.prototype, "connect");
|
|
89
|
+
createDatabase({});
|
|
90
|
+
expect(connect).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("defaultRouterDeps", () => {
|
|
95
|
+
test("supplies every dependency the handlers use", () => {
|
|
96
|
+
const deps: RouterDeps = defaultRouterDeps();
|
|
97
|
+
expect(deps.db).toBeInstanceOf(JA4Database);
|
|
98
|
+
expect(typeof deps.getJA4).toBe("function");
|
|
99
|
+
expect(typeof deps.decodeToken).toBe("function");
|
|
100
|
+
expect(typeof deps.logger.error).toBe("function");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("builds a fresh set each time", () => {
|
|
104
|
+
expect(defaultRouterDeps().db).not.toBe(defaultRouterDeps().db);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
interface RouteLayer {
|
|
109
|
+
route?: {
|
|
110
|
+
path: string;
|
|
111
|
+
methods: Record<string, boolean>;
|
|
112
|
+
stack: { name: string }[];
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const routesOf = (router: ReturnType<typeof prosopoRouter>): string[] => {
|
|
117
|
+
// express types its stack as ILayer, which does not describe route.methods.
|
|
118
|
+
const layers: RouteLayer[] = router.stack.map(
|
|
119
|
+
(layer: unknown): RouteLayer => layer as RouteLayer,
|
|
120
|
+
);
|
|
121
|
+
return layers
|
|
122
|
+
.map((layer) =>
|
|
123
|
+
layer.route === undefined
|
|
124
|
+
? undefined
|
|
125
|
+
: `${Object.keys(layer.route.methods).join(",")} ${layer.route.path}`,
|
|
126
|
+
)
|
|
127
|
+
.filter((route): route is string => route !== undefined);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
describe("prosopoRouter", () => {
|
|
131
|
+
test("mounts the verify and test routes, and nothing else", () => {
|
|
132
|
+
const router = prosopoRouter(createDeps().deps);
|
|
133
|
+
expect(routesOf(router)).toEqual([
|
|
134
|
+
`post ${ClientApiPaths.VerifyImageCaptchaSolutionDapp}`,
|
|
135
|
+
"get /test",
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("builds its own dependencies when it is given none", () => {
|
|
140
|
+
expect(routesOf(prosopoRouter())).toHaveLength(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("puts a rate limiter in front of every route", () => {
|
|
144
|
+
// Both routes reach the database unauthenticated, so neither may be
|
|
145
|
+
// mounted with the handler alone.
|
|
146
|
+
const layers: RouteLayer[] = prosopoRouter(createDeps().deps).stack.map(
|
|
147
|
+
(layer: unknown): RouteLayer => layer as RouteLayer,
|
|
148
|
+
);
|
|
149
|
+
const routes = layers
|
|
150
|
+
.map((layer) => layer.route)
|
|
151
|
+
.filter((route): route is NonNullable<RouteLayer["route"]> => !!route);
|
|
152
|
+
expect(routes).toHaveLength(2);
|
|
153
|
+
for (const route of routes) {
|
|
154
|
+
expect(route.stack.length).toBeGreaterThanOrEqual(2);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("does not touch the database while being built", async () => {
|
|
159
|
+
const mocks = createDeps();
|
|
160
|
+
prosopoRouter(mocks.deps);
|
|
161
|
+
expect(mocks.database.connect).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("toRequestHandler", () => {
|
|
166
|
+
test("passes express's own request, response and next straight through", () => {
|
|
167
|
+
const seen: unknown[] = [];
|
|
168
|
+
const handler = vi.fn<
|
|
169
|
+
(req: ApiRequest, res: ApiResponse, next: ApiNext) => Promise<void>
|
|
170
|
+
>(async (req, res, next) => {
|
|
171
|
+
seen.push(req, res, next);
|
|
172
|
+
});
|
|
173
|
+
const req = createRequest();
|
|
174
|
+
const response = createResponse();
|
|
175
|
+
const next: ApiNext = () => undefined;
|
|
176
|
+
toRequestHandler(handler)(
|
|
177
|
+
req as unknown as Request,
|
|
178
|
+
response.res as unknown as Response,
|
|
179
|
+
next,
|
|
180
|
+
);
|
|
181
|
+
expect(seen).toEqual([req, response.res, next]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("returns void, so express does not wait on the promise", () => {
|
|
185
|
+
// Express ignores a returned promise; the adapter must not hand it one and
|
|
186
|
+
// pretend otherwise.
|
|
187
|
+
const handler = async (): Promise<void> => undefined;
|
|
188
|
+
expect(
|
|
189
|
+
toRequestHandler(handler)(
|
|
190
|
+
createRequest() as unknown as Request,
|
|
191
|
+
createResponse().res as unknown as Response,
|
|
192
|
+
(): void => undefined,
|
|
193
|
+
),
|
|
194
|
+
).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a rejecting handler is reported to next, not left unhandled", async () => {
|
|
198
|
+
// Express ignores the returned promise, so without this the request would
|
|
199
|
+
// hang until the client gave up and the process would log an unhandled
|
|
200
|
+
// rejection.
|
|
201
|
+
const failure = new Error("boom");
|
|
202
|
+
const handler = async (): Promise<void> => {
|
|
203
|
+
throw failure;
|
|
204
|
+
};
|
|
205
|
+
const errors: unknown[] = [];
|
|
206
|
+
toRequestHandler(handler)(
|
|
207
|
+
createRequest() as unknown as Request,
|
|
208
|
+
createResponse().res as unknown as Response,
|
|
209
|
+
(error: unknown): void => {
|
|
210
|
+
errors.push(error);
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
214
|
+
expect(errors).toEqual([failure]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("isMain", () => {
|
|
219
|
+
test("is true for the module node was asked to run", () => {
|
|
220
|
+
expect(
|
|
221
|
+
isMain(`file://${path.resolve("/tmp/start.js")}`, "/tmp/start.js"),
|
|
222
|
+
).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("is true for a relative entrypoint, as `node ./dist/start.js` gives", () => {
|
|
226
|
+
// argv[1] is whatever was typed. Comparing it unresolved against the
|
|
227
|
+
// always-absolute module url meant the server never started when it was
|
|
228
|
+
// launched with a relative path.
|
|
229
|
+
expect(
|
|
230
|
+
isMain(`file://${path.resolve("dist/start.js")}`, "./dist/start.js"),
|
|
231
|
+
).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("is false for a module that was merely imported", () => {
|
|
235
|
+
expect(isMain("file:///tmp/api.js", "/tmp/start.js")).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("is false when there is no entrypoint at all", () => {
|
|
239
|
+
// argv[1] is absent when node is run with -e or from a REPL; assuming a
|
|
240
|
+
// match there would start a server inside somebody else's process.
|
|
241
|
+
// Passing undefined explicitly would fall back to the default, so the
|
|
242
|
+
// only way to exercise this is to take argv[1] away.
|
|
243
|
+
const [, entrypoint] = process.argv;
|
|
244
|
+
process.argv.splice(1, 1);
|
|
245
|
+
try {
|
|
246
|
+
expect(isMain("file:///tmp/start.js")).toBe(false);
|
|
247
|
+
} finally {
|
|
248
|
+
if (entrypoint !== undefined) {
|
|
249
|
+
process.argv.splice(1, 0, entrypoint);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("reads the entrypoint from argv when it is not given one", () => {
|
|
255
|
+
expect(isMain(import.meta.url)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("defaultStartDeps", () => {
|
|
260
|
+
test("creates a real express app", () => {
|
|
261
|
+
const app = defaultStartDeps().createApp();
|
|
262
|
+
expect(typeof app.use).toBe("function");
|
|
263
|
+
expect(typeof app.listen).toBe("function");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("its exit ends the process with the code it is given", () => {
|
|
267
|
+
const exit = vi
|
|
268
|
+
.spyOn(process, "exit")
|
|
269
|
+
.mockImplementation((): never => undefined as never);
|
|
270
|
+
defaultStartDeps().exit(1);
|
|
271
|
+
expect(exit).toHaveBeenCalledWith(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("its i18n resolves to a middleware function", async () => {
|
|
275
|
+
expect(typeof (await defaultStartDeps().i18n())).toBe("function");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("the package entrypoint", () => {
|
|
280
|
+
test("importing the package does not start the api", () => {
|
|
281
|
+
// start.ts used to call startApi() at module scope, so importing
|
|
282
|
+
// anything from the package started a server. It is now behind
|
|
283
|
+
// isMain(import.meta.url), and under a test runner argv[1] is the
|
|
284
|
+
// runner rather than start.ts, so the guard does not fire.
|
|
285
|
+
//
|
|
286
|
+
// Asserting the guard rather than probing the port: the port test only
|
|
287
|
+
// held while nothing else was bound, and CI runs the real provider
|
|
288
|
+
// alongside the suite.
|
|
289
|
+
const startUrl = new URL("../start.js", import.meta.url).href;
|
|
290
|
+
expect(isMain(startUrl)).toBe(false);
|
|
291
|
+
expect(isMain(startUrl, fileURLToPath(startUrl))).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
});
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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 { ProcaptchaOutput } from "@prosopo/types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The fixtures this mock provider treats as already verified.
|
|
19
|
+
*
|
|
20
|
+
* They are the accounts and commitment the integration suites sign with; any
|
|
21
|
+
* token carrying one of them is approved without a captcha ever being solved,
|
|
22
|
+
* which is the whole point of the mock.
|
|
23
|
+
*/
|
|
24
|
+
export const TEST_COMMITMENT_ID = "0x123456789test";
|
|
25
|
+
export const TEST_ACCOUNT = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
|
|
26
|
+
export const TEST_DAPP = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM";
|
|
27
|
+
|
|
28
|
+
export const VERIFIED_MESSAGE = "API.USER_VERIFIED";
|
|
29
|
+
export const NOT_VERIFIED_MESSAGE = "API.USER_NOT_VERIFIED";
|
|
30
|
+
|
|
31
|
+
/** The body of a verification response, before translation of the status. */
|
|
32
|
+
export interface VerificationOutcome {
|
|
33
|
+
statusMessage: string;
|
|
34
|
+
verified: boolean;
|
|
35
|
+
commitmentId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decide whether a decoded token counts as verified.
|
|
40
|
+
*
|
|
41
|
+
* Any one of the three fixtures matching is enough — a token only carries the
|
|
42
|
+
* fields the client filled in, so requiring all three would never approve.
|
|
43
|
+
* Empty strings are not matches: an absent field decodes to "" and must not be
|
|
44
|
+
* able to satisfy a comparison against a fixture.
|
|
45
|
+
*/
|
|
46
|
+
export const verifyProcaptchaOutput = (
|
|
47
|
+
output: Pick<ProcaptchaOutput, "user" | "dapp" | "commitmentId">,
|
|
48
|
+
): VerificationOutcome => {
|
|
49
|
+
const matched =
|
|
50
|
+
output.user === TEST_ACCOUNT ||
|
|
51
|
+
output.commitmentId === TEST_COMMITMENT_ID ||
|
|
52
|
+
output.dapp === TEST_DAPP;
|
|
53
|
+
|
|
54
|
+
if (!matched) {
|
|
55
|
+
return { statusMessage: NOT_VERIFIED_MESSAGE, verified: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
statusMessage: VERIFIED_MESSAGE,
|
|
60
|
+
verified: true,
|
|
61
|
+
commitmentId: TEST_COMMITMENT_ID,
|
|
62
|
+
};
|
|
63
|
+
};
|