@prosopo/client-example-server 2.7.171 → 2.7.172
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 +2 -5
- package/.turbo/turbo-build$colon$tsc.log +21 -24
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +19 -0
- package/LICENSE +201 -0
- package/README.md +6 -6
- package/package.json +81 -80
- package/env.development +0 -10
- package/env.production +0 -2
- package/env.test +0 -10
- package/src/app.ts +0 -210
- package/src/controllers/auth.ts +0 -396
- package/src/models/user.ts +0 -45
- package/src/routes/routes.ts +0 -57
- package/src/tests/app.unit.test.ts +0 -428
- package/src/tests/appEntrypoint.unit.test.ts +0 -174
- package/src/tests/auth.unit.test.ts +0 -915
- package/src/tests/authHarness.ts +0 -131
- package/src/tests/clientExampleServer.test-d.ts +0 -146
- package/src/tests/routes.unit.test.ts +0 -166
- package/src/tests/utils.unit.test.ts +0 -169
- package/src/utils/connection.ts +0 -41
- package/src/utils/database.ts +0 -22
- package/tsconfig.cjs.json +0 -38
- package/tsconfig.json +0 -44
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
|
@@ -1,428 +0,0 @@
|
|
|
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 fs from "node:fs";
|
|
16
|
-
import type { Server } from "node:http";
|
|
17
|
-
import type { AddressInfo } from "node:net";
|
|
18
|
-
import os from "node:os";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import type { ProsopoServerConfigOutput } from "@prosopo/types";
|
|
21
|
-
import express from "express";
|
|
22
|
-
import type { Connection } from "mongoose";
|
|
23
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
24
|
-
import {
|
|
25
|
-
DEFAULT_PORT,
|
|
26
|
-
ProsopoVerificationType,
|
|
27
|
-
createApp,
|
|
28
|
-
isDevelopmentOrTest,
|
|
29
|
-
main,
|
|
30
|
-
resolvePort,
|
|
31
|
-
resolveVerifyEndpoint,
|
|
32
|
-
resolveVerifyType,
|
|
33
|
-
startServer,
|
|
34
|
-
} from "../app.js";
|
|
35
|
-
import { serverConfig } from "./authHarness.js";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* main() opens a mongo connection and reads the deployment config; both are
|
|
39
|
-
* replaced. The express wiring, the endpoint/port resolution and the HTTP vs
|
|
40
|
-
* HTTPS decision are exercised for real.
|
|
41
|
-
*/
|
|
42
|
-
const mocks = vi.hoisted(() => ({
|
|
43
|
-
memoryUris: [] as string[],
|
|
44
|
-
memorySetup: vi.fn(),
|
|
45
|
-
connections: [] as string[],
|
|
46
|
-
routesArgs: [] as unknown[][],
|
|
47
|
-
config: {} as ProsopoServerConfigOutput,
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
vi.mock("../utils/database.js", () => ({
|
|
51
|
-
default: () => {
|
|
52
|
-
mocks.memorySetup();
|
|
53
|
-
return Promise.resolve("mongodb://memory:27017/");
|
|
54
|
-
},
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
vi.mock("../utils/connection.js", () => ({
|
|
58
|
-
default: (uri: string) => {
|
|
59
|
-
mocks.connections.push(uri);
|
|
60
|
-
return {} as unknown as Connection;
|
|
61
|
-
},
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
vi.mock("../routes/routes.js", () => ({
|
|
65
|
-
default: (...args: unknown[]) => {
|
|
66
|
-
mocks.routesArgs.push(args);
|
|
67
|
-
return express.Router();
|
|
68
|
-
},
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
vi.mock("@prosopo/server", async (importOriginal) => {
|
|
72
|
-
const actual = await importOriginal<typeof import("@prosopo/server")>();
|
|
73
|
-
return {
|
|
74
|
-
...actual,
|
|
75
|
-
getServerConfig: () => mocks.config,
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
vi.mock("@prosopo/dotenv", async (importOriginal) => {
|
|
80
|
-
const actual = await importOriginal<typeof import("@prosopo/dotenv")>();
|
|
81
|
-
return {
|
|
82
|
-
...actual,
|
|
83
|
-
// The real loadEnv walks the repo for .env files; irrelevant here and
|
|
84
|
-
// it would overwrite the env each test sets up.
|
|
85
|
-
loadEnv: () => undefined,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const servers: Server[] = [];
|
|
90
|
-
|
|
91
|
-
const track = (server: Server): Server => {
|
|
92
|
-
servers.push(server);
|
|
93
|
-
return server;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const env = { ...process.env };
|
|
97
|
-
|
|
98
|
-
beforeEach(() => {
|
|
99
|
-
mocks.memorySetup.mockClear();
|
|
100
|
-
mocks.memoryUris.length = 0;
|
|
101
|
-
mocks.connections.length = 0;
|
|
102
|
-
mocks.routesArgs.length = 0;
|
|
103
|
-
mocks.config = serverConfig();
|
|
104
|
-
process.env.NODE_ENV = "test";
|
|
105
|
-
process.env.MONGO_URI = "mongodb://configured:27017/";
|
|
106
|
-
process.env.PROSOPO_SITE_PRIVATE_KEY = "//Alice";
|
|
107
|
-
Reflect.deleteProperty(process.env, "PROSOPO_VERIFY_ENDPOINT");
|
|
108
|
-
Reflect.deleteProperty(process.env, "PROSOPO_VERIFICATION_TYPE");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
afterEach(async () => {
|
|
112
|
-
await Promise.all(
|
|
113
|
-
servers.splice(0).map(
|
|
114
|
-
(server) =>
|
|
115
|
-
new Promise<void>((resolve) => {
|
|
116
|
-
server.close(() => resolve());
|
|
117
|
-
}),
|
|
118
|
-
),
|
|
119
|
-
);
|
|
120
|
-
for (const key of Object.keys(process.env)) {
|
|
121
|
-
if (!(key in env)) Reflect.deleteProperty(process.env, key);
|
|
122
|
-
}
|
|
123
|
-
Object.assign(process.env, env);
|
|
124
|
-
vi.restoreAllMocks();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("resolveVerifyEndpoint", () => {
|
|
128
|
-
test("prefers an explicitly configured endpoint", () => {
|
|
129
|
-
process.env.PROSOPO_VERIFY_ENDPOINT = "https://self-hosted/siteverify";
|
|
130
|
-
expect(resolveVerifyEndpoint()).toBe("https://self-hosted/siteverify");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("prefixes the hosted API with the environment name", () => {
|
|
134
|
-
process.env.NODE_ENV = "staging";
|
|
135
|
-
expect(resolveVerifyEndpoint()).toBe(
|
|
136
|
-
"https://staging-api.prosopo.io/siteverify",
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("uses the bare hosted API in production", () => {
|
|
141
|
-
process.env.NODE_ENV = "production";
|
|
142
|
-
expect(resolveVerifyEndpoint()).toBe("https://api.prosopo.io/siteverify");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("an unset NODE_ENV falls back to the production host", () => {
|
|
146
|
-
// Prefixing an absent environment produced undefined-api.prosopo.io,
|
|
147
|
-
// which resolves to nothing.
|
|
148
|
-
Reflect.deleteProperty(process.env, "NODE_ENV");
|
|
149
|
-
expect(resolveVerifyEndpoint()).toBe("https://api.prosopo.io/siteverify");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("an empty NODE_ENV is treated the same way", () => {
|
|
153
|
-
process.env.NODE_ENV = "";
|
|
154
|
-
expect(resolveVerifyEndpoint()).toBe("https://api.prosopo.io/siteverify");
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("resolveVerifyType", () => {
|
|
159
|
-
test("accepts the verification types it knows", () => {
|
|
160
|
-
process.env.PROSOPO_VERIFICATION_TYPE = "local";
|
|
161
|
-
expect(resolveVerifyType()).toBe(ProsopoVerificationType.local);
|
|
162
|
-
process.env.PROSOPO_VERIFICATION_TYPE = "api";
|
|
163
|
-
expect(resolveVerifyType()).toBe(ProsopoVerificationType.api);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("falls back to the hosted API for anything else", () => {
|
|
167
|
-
process.env.PROSOPO_VERIFICATION_TYPE = "carrier-pigeon";
|
|
168
|
-
expect(resolveVerifyType()).toBe(ProsopoVerificationType.api);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("falls back to the hosted API when unset", () => {
|
|
172
|
-
expect(resolveVerifyType()).toBe(ProsopoVerificationType.api);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("isDevelopmentOrTest", () => {
|
|
177
|
-
test("is true only for development and test", () => {
|
|
178
|
-
for (const [value, expected] of [
|
|
179
|
-
["development", true],
|
|
180
|
-
["test", true],
|
|
181
|
-
["production", false],
|
|
182
|
-
["staging", false],
|
|
183
|
-
] as [string, boolean][]) {
|
|
184
|
-
process.env.NODE_ENV = value;
|
|
185
|
-
expect(isDevelopmentOrTest()).toBe(expected);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("resolvePort", () => {
|
|
191
|
-
test("takes the port from the configured server URL", () => {
|
|
192
|
-
expect(resolvePort(serverConfig())).toBe(9228);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("defaults when no server URL is configured", () => {
|
|
196
|
-
const config: ProsopoServerConfigOutput = {
|
|
197
|
-
...serverConfig(),
|
|
198
|
-
serverUrl: "",
|
|
199
|
-
};
|
|
200
|
-
expect(resolvePort(config)).toBe(9228);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("defaults on a server URL with no port segment", () => {
|
|
204
|
-
// This used to produce NaN, which node listens on as an arbitrary free
|
|
205
|
-
// port — the server came up somewhere nothing was pointed at.
|
|
206
|
-
const config: ProsopoServerConfigOutput = {
|
|
207
|
-
...serverConfig(),
|
|
208
|
-
serverUrl: "https://localhost",
|
|
209
|
-
};
|
|
210
|
-
expect(resolvePort(config)).toBe(DEFAULT_PORT);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("defaults when the port segment isn't a number", () => {
|
|
214
|
-
const config: ProsopoServerConfigOutput = {
|
|
215
|
-
...serverConfig(),
|
|
216
|
-
serverUrl: "https://localhost:https",
|
|
217
|
-
};
|
|
218
|
-
expect(resolvePort(config)).toBe(DEFAULT_PORT);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe("createApp", () => {
|
|
223
|
-
const listen = (app: express.Express): Promise<string> =>
|
|
224
|
-
new Promise((resolve) => {
|
|
225
|
-
const server = track(
|
|
226
|
-
app.listen(0, () => {
|
|
227
|
-
const { port } = server.address() as AddressInfo;
|
|
228
|
-
resolve(`http://127.0.0.1:${port}`);
|
|
229
|
-
}),
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("sets permissive CORS headers on every response", async () => {
|
|
234
|
-
const app = createApp();
|
|
235
|
-
app.get("/thing", (_req, res) => {
|
|
236
|
-
res.status(200).json({ ok: true });
|
|
237
|
-
});
|
|
238
|
-
const base = await listen(app);
|
|
239
|
-
const response = await fetch(`${base}/thing`);
|
|
240
|
-
expect(response.headers.get("access-control-allow-origin")).toBe("*");
|
|
241
|
-
expect(response.headers.get("access-control-allow-methods")).toContain(
|
|
242
|
-
"POST",
|
|
243
|
-
);
|
|
244
|
-
expect(response.headers.get("access-control-allow-headers")).toContain(
|
|
245
|
-
"Authorization",
|
|
246
|
-
);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("answers preflight requests", async () => {
|
|
250
|
-
// cors() answers preflight itself with a 204; the explicit OPTIONS
|
|
251
|
-
// handler this app used to declare could never run.
|
|
252
|
-
const base = await listen(createApp());
|
|
253
|
-
const response = await fetch(`${base}/anything`, { method: "OPTIONS" });
|
|
254
|
-
expect(response.status).toBe(204);
|
|
255
|
-
expect(response.headers.get("access-control-allow-methods")).toContain(
|
|
256
|
-
"POST",
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("parses JSON bodies", async () => {
|
|
261
|
-
const app = createApp();
|
|
262
|
-
app.post("/echo", (req, res) => {
|
|
263
|
-
res.status(200).json(req.body);
|
|
264
|
-
});
|
|
265
|
-
const base = await listen(app);
|
|
266
|
-
const response = await fetch(`${base}/echo`, {
|
|
267
|
-
method: "POST",
|
|
268
|
-
headers: { "content-type": "application/json" },
|
|
269
|
-
body: JSON.stringify({ email: "user@example.com" }),
|
|
270
|
-
});
|
|
271
|
-
expect(await response.json()).toEqual({ email: "user@example.com" });
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("parses form-encoded bodies", async () => {
|
|
275
|
-
const app = createApp();
|
|
276
|
-
app.post("/echo", (req, res) => {
|
|
277
|
-
res.status(200).json(req.body);
|
|
278
|
-
});
|
|
279
|
-
const base = await listen(app);
|
|
280
|
-
const response = await fetch(`${base}/echo`, {
|
|
281
|
-
method: "POST",
|
|
282
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
283
|
-
body: "email=user%40example.com",
|
|
284
|
-
});
|
|
285
|
-
expect(await response.json()).toEqual({ email: "user@example.com" });
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("trusts one proxy hop, so client IPs survive the reverse proxy", () => {
|
|
289
|
-
expect(createApp().get("trust proxy")).toBe(1);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe("startServer", () => {
|
|
294
|
-
const certDir = (): string => {
|
|
295
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ces-certs-"));
|
|
296
|
-
fs.mkdirSync(path.join(root, "certs"));
|
|
297
|
-
return root;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
test("serves HTTPS in development when the certificates exist", async () => {
|
|
301
|
-
process.env.NODE_ENV = "development";
|
|
302
|
-
const root = certDir();
|
|
303
|
-
// startServer resolves ../../../certs relative to the module, so the
|
|
304
|
-
// fake module directory sits three levels below the cert directory.
|
|
305
|
-
const moduleDir = path.join(root, "certs", "a", "b");
|
|
306
|
-
fs.mkdirSync(moduleDir, { recursive: true });
|
|
307
|
-
const certs = path.join(root, "certs");
|
|
308
|
-
// A throwaway self-signed pair would need openssl; the files only have
|
|
309
|
-
// to exist for the branch, and https.createServer is stubbed.
|
|
310
|
-
fs.writeFileSync(path.join(certs, "server.key"), "key");
|
|
311
|
-
fs.writeFileSync(path.join(certs, "server.crt"), "cert");
|
|
312
|
-
const https = await import("node:https");
|
|
313
|
-
const fake = { listen: vi.fn((_port: number, cb: () => void) => cb()) };
|
|
314
|
-
const createServer = vi
|
|
315
|
-
.spyOn(https.default, "createServer")
|
|
316
|
-
.mockReturnValue(
|
|
317
|
-
fake as unknown as ReturnType<typeof https.createServer>,
|
|
318
|
-
);
|
|
319
|
-
startServer(express(), 1234, path.join(certs, "a", "b"));
|
|
320
|
-
expect(createServer).toHaveBeenCalledTimes(1);
|
|
321
|
-
const options = createServer.mock.calls[0]?.[0] as {
|
|
322
|
-
key: Buffer;
|
|
323
|
-
cert: Buffer;
|
|
324
|
-
};
|
|
325
|
-
expect(options.key.toString()).toBe("key");
|
|
326
|
-
expect(options.cert.toString()).toBe("cert");
|
|
327
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("falls back to HTTP in development when the certificates are missing", async () => {
|
|
331
|
-
process.env.NODE_ENV = "development";
|
|
332
|
-
const server = track(
|
|
333
|
-
startServer(express(), 0, path.join(os.tmpdir(), "no-such-dir")),
|
|
334
|
-
);
|
|
335
|
-
await new Promise<void>((resolve) => {
|
|
336
|
-
if (server.listening) return resolve();
|
|
337
|
-
server.once("listening", () => resolve());
|
|
338
|
-
});
|
|
339
|
-
expect((server.address() as AddressInfo).port).toBeGreaterThan(0);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("serves plain HTTP in production, where a proxy terminates TLS", async () => {
|
|
343
|
-
process.env.NODE_ENV = "production";
|
|
344
|
-
const https = await import("node:https");
|
|
345
|
-
const createServer = vi.spyOn(https.default, "createServer");
|
|
346
|
-
const server = track(startServer(express(), 0));
|
|
347
|
-
expect(createServer).not.toHaveBeenCalled();
|
|
348
|
-
expect((server.address() as AddressInfo).port).toBeGreaterThan(0);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("the server it returns actually serves the app", async () => {
|
|
352
|
-
process.env.NODE_ENV = "production";
|
|
353
|
-
const app = express();
|
|
354
|
-
app.get("/health", (_req, res) => {
|
|
355
|
-
res.status(200).json({ status: "ok" });
|
|
356
|
-
});
|
|
357
|
-
const server = track(startServer(app, 0));
|
|
358
|
-
await new Promise<void>((resolve) => {
|
|
359
|
-
if (server.listening) return resolve();
|
|
360
|
-
server.once("listening", () => resolve());
|
|
361
|
-
});
|
|
362
|
-
const { port } = server.address() as AddressInfo;
|
|
363
|
-
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
364
|
-
expect(await response.json()).toEqual({ status: "ok" });
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe("main", () => {
|
|
369
|
-
test("wires the routes with the resolved endpoint and verification type", async () => {
|
|
370
|
-
process.env.PROSOPO_VERIFY_ENDPOINT = "https://self-hosted/siteverify";
|
|
371
|
-
process.env.PROSOPO_VERIFICATION_TYPE = "local";
|
|
372
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
373
|
-
track(await main());
|
|
374
|
-
expect(mocks.routesArgs[0]?.slice(1)).toEqual([
|
|
375
|
-
mocks.config,
|
|
376
|
-
"https://self-hosted/siteverify",
|
|
377
|
-
ProsopoVerificationType.local,
|
|
378
|
-
]);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("connects to the configured mongo instance", async () => {
|
|
382
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
383
|
-
track(await main());
|
|
384
|
-
expect(mocks.connections).toEqual(["mongodb://configured:27017/"]);
|
|
385
|
-
expect(mocks.memorySetup).not.toHaveBeenCalled();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test("starts an in-memory mongo when none is configured", async () => {
|
|
389
|
-
Reflect.deleteProperty(process.env, "MONGO_URI");
|
|
390
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
391
|
-
track(await main());
|
|
392
|
-
expect(mocks.memorySetup).toHaveBeenCalledTimes(1);
|
|
393
|
-
expect(mocks.connections).toEqual(["mongodb://memory:27017/"]);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
test("refuses to run an in-memory database outside development", async () => {
|
|
397
|
-
Reflect.deleteProperty(process.env, "MONGO_URI");
|
|
398
|
-
process.env.NODE_ENV = "production";
|
|
399
|
-
await expect(main()).rejects.toThrow(
|
|
400
|
-
"Cannot run mongo memory when NODE_ENV is neither development nor test",
|
|
401
|
-
);
|
|
402
|
-
expect(mocks.connections).toHaveLength(0);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
test("boots without a site private key, but says so", async () => {
|
|
406
|
-
// The demo server is still useful without one — every signup will just
|
|
407
|
-
// fail verification — so this logs rather than throws.
|
|
408
|
-
Reflect.deleteProperty(process.env, "PROSOPO_SITE_PRIVATE_KEY");
|
|
409
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
410
|
-
const server = track(await main());
|
|
411
|
-
expect(server.listening).toBe(true);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test("listens on the port the config names", async () => {
|
|
415
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
416
|
-
const server = track(await main());
|
|
417
|
-
expect((server.address() as AddressInfo).port).toBeGreaterThan(0);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
test("propagates a database connection failure instead of listening", async () => {
|
|
421
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
422
|
-
const connection = await import("../utils/connection.js");
|
|
423
|
-
vi.spyOn(connection, "default").mockImplementation(() => {
|
|
424
|
-
throw new Error("mongo unreachable");
|
|
425
|
-
});
|
|
426
|
-
await expect(main()).rejects.toThrow("mongo unreachable");
|
|
427
|
-
});
|
|
428
|
-
});
|
|
@@ -1,174 +0,0 @@
|
|
|
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 { Server } from "node:http";
|
|
16
|
-
import type { ProsopoServerConfigOutput } from "@prosopo/types";
|
|
17
|
-
import express from "express";
|
|
18
|
-
import type { Connection } from "mongoose";
|
|
19
|
-
import {
|
|
20
|
-
afterEach,
|
|
21
|
-
beforeAll,
|
|
22
|
-
beforeEach,
|
|
23
|
-
describe,
|
|
24
|
-
expect,
|
|
25
|
-
test,
|
|
26
|
-
vi,
|
|
27
|
-
} from "vitest";
|
|
28
|
-
import { serverConfig } from "./authHarness.js";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Importing app.js as the process entrypoint: isMain is forced true so the
|
|
32
|
-
* module's boot block runs, with every external dependency stubbed.
|
|
33
|
-
*/
|
|
34
|
-
const mocks = vi.hoisted(() => ({
|
|
35
|
-
isMain: true,
|
|
36
|
-
servers: [] as Server[],
|
|
37
|
-
connectionThrows: undefined as Error | undefined,
|
|
38
|
-
config: {} as ProsopoServerConfigOutput,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
vi.mock("@prosopo/util", async (importOriginal) => {
|
|
42
|
-
const actual = await importOriginal<typeof import("@prosopo/util")>();
|
|
43
|
-
return {
|
|
44
|
-
...actual,
|
|
45
|
-
isMain: () => mocks.isMain,
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
vi.mock("../utils/connection.js", () => ({
|
|
50
|
-
default: (): Connection => {
|
|
51
|
-
if (mocks.connectionThrows) throw mocks.connectionThrows;
|
|
52
|
-
return {} as unknown as Connection;
|
|
53
|
-
},
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
vi.mock("../utils/database.js", () => ({
|
|
57
|
-
default: () => Promise.resolve("mongodb://memory:27017/"),
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
vi.mock("../routes/routes.js", () => ({
|
|
61
|
-
default: () => express.Router(),
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
vi.mock("@prosopo/server", async (importOriginal) => {
|
|
65
|
-
const actual = await importOriginal<typeof import("@prosopo/server")>();
|
|
66
|
-
return { ...actual, getServerConfig: () => mocks.config };
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
vi.mock("@prosopo/dotenv", async (importOriginal) => {
|
|
70
|
-
const actual = await importOriginal<typeof import("@prosopo/dotenv")>();
|
|
71
|
-
return { ...actual, loadEnv: () => undefined };
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Loads app.js afresh and hands back the server it opened, if any. The module
|
|
76
|
-
* boots on import, so each case needs its own module registry.
|
|
77
|
-
*/
|
|
78
|
-
const importApp = async (
|
|
79
|
-
options: { expectServer?: boolean } = {},
|
|
80
|
-
): Promise<Server | undefined> => {
|
|
81
|
-
vi.resetModules();
|
|
82
|
-
const listeners: Server[] = [];
|
|
83
|
-
const httpModule = await import("node:http");
|
|
84
|
-
const httpsModule = await import("node:https");
|
|
85
|
-
const createServer = httpModule.default.createServer;
|
|
86
|
-
vi.spyOn(httpModule.default, "createServer").mockImplementation(
|
|
87
|
-
(...args: Parameters<typeof createServer>) => {
|
|
88
|
-
const server = createServer(...args);
|
|
89
|
-
listeners.push(server);
|
|
90
|
-
mocks.servers.push(server);
|
|
91
|
-
return server;
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
// startServer prefers HTTPS whenever dev certificates happen to be on
|
|
95
|
-
// disk, so both factories have to be watched — otherwise the assertion
|
|
96
|
-
// depends on whether ./setup_certs.sh has been run.
|
|
97
|
-
const createSecureServer = httpsModule.default.createServer;
|
|
98
|
-
vi.spyOn(httpsModule.default, "createServer").mockImplementation(
|
|
99
|
-
(...args: Parameters<typeof createSecureServer>) => {
|
|
100
|
-
const server = createSecureServer(...args);
|
|
101
|
-
listeners.push(server);
|
|
102
|
-
mocks.servers.push(server);
|
|
103
|
-
return server;
|
|
104
|
-
},
|
|
105
|
-
);
|
|
106
|
-
await import("../app.js");
|
|
107
|
-
// The boot block is fire-and-forget, so wait for its promise chain. How
|
|
108
|
-
// many turns that takes depends on the machine, so poll rather than
|
|
109
|
-
// assume a fixed number of ticks.
|
|
110
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
111
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
112
|
-
if (!options.expectServer) break;
|
|
113
|
-
if (listeners[0]?.listening) break;
|
|
114
|
-
}
|
|
115
|
-
return listeners[0];
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The first import of app.js transforms its whole dependency graph, which on
|
|
120
|
-
* a loaded machine takes far longer than the per-test timeout, and lands on
|
|
121
|
-
* whichever case happens to run first. Pay it once here: vitest keeps the
|
|
122
|
-
* transformed modules across resetModules, so each case only re-evaluates.
|
|
123
|
-
*/
|
|
124
|
-
const WARM_UP_TIMEOUT_MS = 180_000;
|
|
125
|
-
beforeAll(async () => {
|
|
126
|
-
mocks.isMain = false;
|
|
127
|
-
await import("../app.js");
|
|
128
|
-
}, WARM_UP_TIMEOUT_MS);
|
|
129
|
-
|
|
130
|
-
beforeEach(() => {
|
|
131
|
-
mocks.isMain = true;
|
|
132
|
-
mocks.connectionThrows = undefined;
|
|
133
|
-
mocks.config = { ...serverConfig(), serverUrl: "https://localhost:0" };
|
|
134
|
-
process.env.NODE_ENV = "test";
|
|
135
|
-
process.env.MONGO_URI = "mongodb://configured:27017/";
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
afterEach(async () => {
|
|
139
|
-
vi.restoreAllMocks();
|
|
140
|
-
await Promise.all(
|
|
141
|
-
mocks.servers.splice(0).map(
|
|
142
|
-
(server) =>
|
|
143
|
-
new Promise<void>((resolve) => {
|
|
144
|
-
server.close(() => resolve());
|
|
145
|
-
}),
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("running app.js as the entrypoint", () => {
|
|
151
|
-
test("boots the server", async () => {
|
|
152
|
-
const server = await importApp({ expectServer: true });
|
|
153
|
-
expect(server?.listening).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("stays quiet when the module is merely imported", async () => {
|
|
157
|
-
// A test, or another entrypoint reusing createApp, must not end up
|
|
158
|
-
// opening a port or spinning up mongo.
|
|
159
|
-
mocks.isMain = false;
|
|
160
|
-
const server = await importApp();
|
|
161
|
-
expect(server).toBeUndefined();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("exits when the server cannot start", async () => {
|
|
165
|
-
mocks.connectionThrows = new Error("mongo unreachable");
|
|
166
|
-
const exit = vi
|
|
167
|
-
.spyOn(process, "exit")
|
|
168
|
-
.mockImplementation((): never => undefined as never);
|
|
169
|
-
await importApp();
|
|
170
|
-
expect(exit).toHaveBeenCalledTimes(1);
|
|
171
|
-
// A zero status would tell a supervisor the process shut down cleanly.
|
|
172
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
173
|
-
});
|
|
174
|
-
});
|