@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
package/src/tests/authHarness.ts
DELETED
|
@@ -1,131 +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 { KeyringPair } from "@prosopo/types";
|
|
16
|
-
import type { ProsopoServerConfigOutput } from "@prosopo/types";
|
|
17
|
-
import type { Request, Response } from "express";
|
|
18
|
-
import type { Connection } from "mongoose";
|
|
19
|
-
import { vi } from "vitest";
|
|
20
|
-
import type { UserInterface } from "../models/user.js";
|
|
21
|
-
|
|
22
|
-
export const SITE_KEY = "site-key-address";
|
|
23
|
-
export const SECRET = "//Alice";
|
|
24
|
-
export const VERIFY_ENDPOINT = "https://api.prosopo.io/siteverify";
|
|
25
|
-
|
|
26
|
-
/** The subset of the config the controllers actually read. */
|
|
27
|
-
export const serverConfig = (
|
|
28
|
-
options: { secret?: string } = { secret: SECRET },
|
|
29
|
-
): ProsopoServerConfigOutput =>
|
|
30
|
-
({
|
|
31
|
-
account: { secret: options.secret },
|
|
32
|
-
defaultEnvironment: "development",
|
|
33
|
-
serverUrl: "https://localhost:9228",
|
|
34
|
-
}) as unknown as ProsopoServerConfigOutput;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* getPair returns a real keyring pair in production; the controllers only ever
|
|
38
|
-
* compare `address` and hand the pair to ProsopoServer, so a stub with the
|
|
39
|
-
* address is enough to drive every branch of the site-key matching.
|
|
40
|
-
*/
|
|
41
|
-
export const pairWithAddress = (address: string): KeyringPair =>
|
|
42
|
-
({ address }) as unknown as KeyringPair;
|
|
43
|
-
|
|
44
|
-
export type FoundUser = Pick<UserInterface, "password" | "salt">;
|
|
45
|
-
|
|
46
|
-
export interface UserModelStub {
|
|
47
|
-
findOne: ReturnType<typeof vi.fn>;
|
|
48
|
-
create: ReturnType<typeof vi.fn>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* A mongoose connection whose `model()` hands back a stub with the two methods
|
|
53
|
-
* the controllers call. The real model would need a live mongod, and its
|
|
54
|
-
* query builder is chainable in ways nothing here relies on.
|
|
55
|
-
*/
|
|
56
|
-
export const connectionWith = (
|
|
57
|
-
model: UserModelStub,
|
|
58
|
-
): { connection: Connection; model: UserModelStub } => ({
|
|
59
|
-
connection: {
|
|
60
|
-
model: () => model,
|
|
61
|
-
} as unknown as Connection,
|
|
62
|
-
model,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export const userModel = (options: {
|
|
66
|
-
found?: FoundUser | null;
|
|
67
|
-
findOneRejects?: Error;
|
|
68
|
-
createRejects?: Error;
|
|
69
|
-
}): UserModelStub => ({
|
|
70
|
-
findOne: vi.fn(() =>
|
|
71
|
-
options.findOneRejects
|
|
72
|
-
? Promise.reject(options.findOneRejects)
|
|
73
|
-
: Promise.resolve(options.found ?? null),
|
|
74
|
-
),
|
|
75
|
-
create: vi.fn(() =>
|
|
76
|
-
options.createRejects
|
|
77
|
-
? Promise.reject(options.createRejects)
|
|
78
|
-
: Promise.resolve({}),
|
|
79
|
-
),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
export interface ResponseStub {
|
|
83
|
-
response: Response;
|
|
84
|
-
statuses: number[];
|
|
85
|
-
bodies: unknown[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Records what the handler wrote, in order, so double-sends are visible. */
|
|
89
|
-
export const responseStub = (): ResponseStub => {
|
|
90
|
-
const statuses: number[] = [];
|
|
91
|
-
const bodies: unknown[] = [];
|
|
92
|
-
const response = {
|
|
93
|
-
status: (code: number) => {
|
|
94
|
-
statuses.push(code);
|
|
95
|
-
return response;
|
|
96
|
-
},
|
|
97
|
-
json: (body: unknown) => {
|
|
98
|
-
bodies.push(body);
|
|
99
|
-
return response;
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
return {
|
|
103
|
-
response: response as unknown as Response,
|
|
104
|
-
statuses,
|
|
105
|
-
bodies,
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export const request = (options: {
|
|
110
|
-
body?: Record<string, unknown>;
|
|
111
|
-
headers?: Record<string, string>;
|
|
112
|
-
authorization?: string;
|
|
113
|
-
}): Request =>
|
|
114
|
-
({
|
|
115
|
-
body: options.body ?? {},
|
|
116
|
-
headers: options.headers ?? {},
|
|
117
|
-
get: (name: string) =>
|
|
118
|
-
name === "Authorization" ? options.authorization : undefined,
|
|
119
|
-
}) as unknown as Request;
|
|
120
|
-
|
|
121
|
-
/** A body that satisfies SubscribeBodySpec. */
|
|
122
|
-
export const signupBody = (
|
|
123
|
-
overrides: Record<string, unknown> = {},
|
|
124
|
-
): Record<string, unknown> => ({
|
|
125
|
-
email: "user@example.com",
|
|
126
|
-
password: "hunter2",
|
|
127
|
-
name: "user",
|
|
128
|
-
siteKey: SITE_KEY,
|
|
129
|
-
"procaptcha-response": "0xtoken",
|
|
130
|
-
...overrides,
|
|
131
|
-
});
|
|
@@ -1,146 +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 type express from "express";
|
|
18
|
-
import type { Connection } from "mongoose";
|
|
19
|
-
import { assertType, describe, expectTypeOf, test } from "vitest";
|
|
20
|
-
import {
|
|
21
|
-
type ProsopoVerificationType,
|
|
22
|
-
createApp,
|
|
23
|
-
isDevelopmentOrTest,
|
|
24
|
-
main,
|
|
25
|
-
resolvePort,
|
|
26
|
-
resolveVerifyEndpoint,
|
|
27
|
-
resolveVerifyType,
|
|
28
|
-
startServer,
|
|
29
|
-
} from "../app.js";
|
|
30
|
-
import { isAuth, login, signup } from "../controllers/auth.js";
|
|
31
|
-
import type UserSchema from "../models/user.js";
|
|
32
|
-
import type { UserInterface } from "../models/user.js";
|
|
33
|
-
import getRoutes from "../routes/routes.js";
|
|
34
|
-
import connectionFactory from "../utils/connection.js";
|
|
35
|
-
import memoryServerSetup from "../utils/database.js";
|
|
36
|
-
|
|
37
|
-
describe("the app module's types", () => {
|
|
38
|
-
test("the environment readers take no arguments", () => {
|
|
39
|
-
expectTypeOf(resolveVerifyEndpoint).parameters.toEqualTypeOf<[]>();
|
|
40
|
-
expectTypeOf(resolveVerifyEndpoint).returns.toEqualTypeOf<string>();
|
|
41
|
-
expectTypeOf(
|
|
42
|
-
resolveVerifyType,
|
|
43
|
-
).returns.toEqualTypeOf<ProsopoVerificationType>();
|
|
44
|
-
expectTypeOf(isDevelopmentOrTest).returns.toEqualTypeOf<boolean>();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("the verification type is a closed set", () => {
|
|
48
|
-
expectTypeOf<`${ProsopoVerificationType}`>().toEqualTypeOf<
|
|
49
|
-
"api" | "local"
|
|
50
|
-
>();
|
|
51
|
-
// @ts-expect-error - anything else has to fall back to the API at runtime.
|
|
52
|
-
assertType<ProsopoVerificationType>("carrier-pigeon");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("the port comes from the shared server config, not a loose object", () => {
|
|
56
|
-
expectTypeOf(resolvePort)
|
|
57
|
-
.parameter(0)
|
|
58
|
-
.toEqualTypeOf<ProsopoServerConfigOutput>();
|
|
59
|
-
expectTypeOf(resolvePort).returns.toEqualTypeOf<number>();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("createApp yields an express app the caller can mount routes on", () => {
|
|
63
|
-
expectTypeOf(createApp).returns.toEqualTypeOf<express.Express>();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("startServer's module directory is optional and it returns a server", () => {
|
|
67
|
-
expectTypeOf(startServer).parameter(1).toEqualTypeOf<number>();
|
|
68
|
-
expectTypeOf(startServer).parameter(2).toEqualTypeOf<string | undefined>();
|
|
69
|
-
expectTypeOf(startServer).returns.toEqualTypeOf<Server>();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("main resolves to the server it started, so a caller can close it", () => {
|
|
73
|
-
expectTypeOf(main).parameters.toEqualTypeOf<[]>();
|
|
74
|
-
expectTypeOf(main).returns.toEqualTypeOf<Promise<Server>>();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("the router's types", () => {
|
|
79
|
-
test("routes are built from a connection, config and verification settings", () => {
|
|
80
|
-
expectTypeOf(getRoutes).parameters.toEqualTypeOf<
|
|
81
|
-
[Connection, ProsopoServerConfigOutput, string, string]
|
|
82
|
-
>();
|
|
83
|
-
expectTypeOf(getRoutes).returns.toEqualTypeOf<express.Router>();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("every argument is required", () => {
|
|
87
|
-
// @ts-expect-error - a router with no verification settings would
|
|
88
|
-
// silently accept unverified signups.
|
|
89
|
-
getRoutes({} as Connection, {} as ProsopoServerConfigOutput, "endpoint");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("the controllers' types", () => {
|
|
94
|
-
test("signup and login are express handlers with their context bound first", () => {
|
|
95
|
-
expectTypeOf(signup).parameter(0).toEqualTypeOf<Connection>();
|
|
96
|
-
expectTypeOf(signup)
|
|
97
|
-
.parameter(1)
|
|
98
|
-
.toEqualTypeOf<ProsopoServerConfigOutput>();
|
|
99
|
-
expectTypeOf(signup).parameter(2).toEqualTypeOf<string>();
|
|
100
|
-
expectTypeOf(signup).parameter(3).toEqualTypeOf<string>();
|
|
101
|
-
expectTypeOf(signup).returns.toExtend<Promise<unknown>>();
|
|
102
|
-
expectTypeOf(login).parameter(0).toEqualTypeOf<Connection>();
|
|
103
|
-
expectTypeOf(login).returns.toEqualTypeOf<Promise<void>>();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("isAuth needs nothing bound, so it mounts directly", () => {
|
|
107
|
-
expectTypeOf(isAuth).parameters.toEqualTypeOf<
|
|
108
|
-
[express.Request, express.Response]
|
|
109
|
-
>();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe("the persistence types", () => {
|
|
114
|
-
test("connectionFactory takes a URI and returns a mongoose connection", () => {
|
|
115
|
-
expectTypeOf(connectionFactory).parameters.toEqualTypeOf<[string]>();
|
|
116
|
-
expectTypeOf(connectionFactory).returns.toEqualTypeOf<Connection>();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("the memory server hands back a URI string", () => {
|
|
120
|
-
expectTypeOf(memoryServerSetup).parameters.toEqualTypeOf<[]>();
|
|
121
|
-
expectTypeOf(memoryServerSetup).returns.toEqualTypeOf<Promise<string>>();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("the user schema is typed by the interface it stores", () => {
|
|
125
|
-
expectTypeOf<keyof typeof UserSchema.obj>().toEqualTypeOf<
|
|
126
|
-
keyof UserInterface
|
|
127
|
-
>();
|
|
128
|
-
expectTypeOf<UserInterface>().toEqualTypeOf<{
|
|
129
|
-
id: number;
|
|
130
|
-
email: string;
|
|
131
|
-
name: string;
|
|
132
|
-
password: string;
|
|
133
|
-
salt: string;
|
|
134
|
-
}>();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("a user document cannot be built without credentials", () => {
|
|
138
|
-
// @ts-expect-error - salt is what makes the stored hash meaningful.
|
|
139
|
-
assertType<UserInterface>({
|
|
140
|
-
id: 1,
|
|
141
|
-
email: "user@example.com",
|
|
142
|
-
name: "user",
|
|
143
|
-
password: "0xhash",
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,166 +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 { AddressInfo } from "node:net";
|
|
17
|
-
import express from "express";
|
|
18
|
-
import type { Connection } from "mongoose";
|
|
19
|
-
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
20
|
-
import getRoutes from "../routes/routes.js";
|
|
21
|
-
import { VERIFY_ENDPOINT, serverConfig } from "./authHarness.js";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The controllers have their own suite; here we only care that each path is
|
|
25
|
-
* bound to the right handler and that the bound arguments reach it.
|
|
26
|
-
*/
|
|
27
|
-
const mocks = vi.hoisted(() => ({
|
|
28
|
-
calls: [] as { handler: string; args: unknown[] }[],
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
vi.mock("../controllers/auth.js", () => {
|
|
32
|
-
type Handler = (...args: unknown[]) => void;
|
|
33
|
-
const record =
|
|
34
|
-
(handler: string): Handler =>
|
|
35
|
-
(...args: unknown[]) => {
|
|
36
|
-
mocks.calls.push({ handler, args: args.slice(0, 4) });
|
|
37
|
-
const res = args[args.length - 2] as {
|
|
38
|
-
status: (code: number) => { json: (body: unknown) => void };
|
|
39
|
-
};
|
|
40
|
-
res.status(200).json({ handler });
|
|
41
|
-
};
|
|
42
|
-
return {
|
|
43
|
-
signup: record("signup"),
|
|
44
|
-
login: record("login"),
|
|
45
|
-
// isAuth is mounted directly, so it only ever gets (req, res).
|
|
46
|
-
isAuth: (_req: unknown, res: unknown) =>
|
|
47
|
-
(res as { status: (code: number) => { json: (body: unknown) => void } })
|
|
48
|
-
.status(200)
|
|
49
|
-
.json({ handler: "isAuth" }),
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const connection: Connection = {} as unknown as Connection;
|
|
54
|
-
|
|
55
|
-
let server: Server | undefined;
|
|
56
|
-
|
|
57
|
-
const listen = (): Promise<string> => {
|
|
58
|
-
const app = express();
|
|
59
|
-
app.use(express.json());
|
|
60
|
-
app.use(getRoutes(connection, serverConfig(), VERIFY_ENDPOINT, "api"));
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
server = app.listen(0, () => {
|
|
63
|
-
const { port } = server?.address() as AddressInfo;
|
|
64
|
-
resolve(`http://127.0.0.1:${port}`);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
afterEach(async () => {
|
|
70
|
-
mocks.calls.length = 0;
|
|
71
|
-
await new Promise<void>((resolve) => {
|
|
72
|
-
if (!server) return resolve();
|
|
73
|
-
server.close(() => resolve());
|
|
74
|
-
server = undefined;
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("the router", () => {
|
|
79
|
-
test("serves the public resource without any authentication", async () => {
|
|
80
|
-
const base = await listen();
|
|
81
|
-
const response = await fetch(`${base}/public`);
|
|
82
|
-
expect(response.status).toBe(200);
|
|
83
|
-
expect(await response.json()).toEqual({
|
|
84
|
-
message: "here is your public resource",
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("answers health checks", async () => {
|
|
89
|
-
const base = await listen();
|
|
90
|
-
const response = await fetch(`${base}/health`);
|
|
91
|
-
expect(response.status).toBe(200);
|
|
92
|
-
expect(await response.json()).toEqual({ status: "ok" });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("routes a signup to the signup handler, with the bound context", async () => {
|
|
96
|
-
const base = await listen();
|
|
97
|
-
const response = await fetch(`${base}/signup`, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: { "content-type": "application/json" },
|
|
100
|
-
body: JSON.stringify({ email: "user@example.com" }),
|
|
101
|
-
});
|
|
102
|
-
expect(response.status).toBe(200);
|
|
103
|
-
expect(mocks.calls[0]?.handler).toBe("signup");
|
|
104
|
-
expect(mocks.calls[0]?.args).toEqual([
|
|
105
|
-
connection,
|
|
106
|
-
serverConfig(),
|
|
107
|
-
VERIFY_ENDPOINT,
|
|
108
|
-
"api",
|
|
109
|
-
]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("routes a login to the login handler", async () => {
|
|
113
|
-
const base = await listen();
|
|
114
|
-
await fetch(`${base}/login`, {
|
|
115
|
-
method: "POST",
|
|
116
|
-
headers: { "content-type": "application/json" },
|
|
117
|
-
body: JSON.stringify({}),
|
|
118
|
-
});
|
|
119
|
-
expect(mocks.calls[0]?.handler).toBe("login");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("guards the private resource", async () => {
|
|
123
|
-
const base = await listen();
|
|
124
|
-
const response = await fetch(`${base}/private`);
|
|
125
|
-
expect(await response.json()).toEqual({ handler: "isAuth" });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("answers anything else with a 404 body, not express's HTML", async () => {
|
|
129
|
-
const base = await listen();
|
|
130
|
-
const response = await fetch(`${base}/nope`);
|
|
131
|
-
expect(response.status).toBe(404);
|
|
132
|
-
expect(await response.json()).toEqual({ error: "page not found" });
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("a GET to a POST-only path falls through to the catch-all", async () => {
|
|
136
|
-
const base = await listen();
|
|
137
|
-
const response = await fetch(`${base}/login`);
|
|
138
|
-
expect(response.status).toBe(404);
|
|
139
|
-
expect(mocks.calls).toHaveLength(0);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("each call builds a fresh router", async () => {
|
|
143
|
-
// The module used to share one router across calls, so a second server
|
|
144
|
-
// stacked a second set of handlers on top of the first call's bound
|
|
145
|
-
// config and answered every request with the stale one.
|
|
146
|
-
const first = getRoutes(connection, serverConfig(), "https://one", "api");
|
|
147
|
-
const second = getRoutes(connection, serverConfig(), "https://two", "api");
|
|
148
|
-
expect(first).not.toBe(second);
|
|
149
|
-
const app = express();
|
|
150
|
-
app.use(express.json());
|
|
151
|
-
app.use(second);
|
|
152
|
-
const base = await new Promise<string>((resolve) => {
|
|
153
|
-
server = app.listen(0, () => {
|
|
154
|
-
const { port } = server?.address() as AddressInfo;
|
|
155
|
-
resolve(`http://127.0.0.1:${port}`);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
await fetch(`${base}/signup`, {
|
|
159
|
-
method: "POST",
|
|
160
|
-
headers: { "content-type": "application/json" },
|
|
161
|
-
body: JSON.stringify({}),
|
|
162
|
-
});
|
|
163
|
-
expect(mocks.calls).toHaveLength(1);
|
|
164
|
-
expect(mocks.calls[0]?.args[2]).toBe("https://two");
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,169 +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 { Connection, Schema } from "mongoose";
|
|
16
|
-
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
17
|
-
import UserSchema, { type UserInterface } from "../models/user.js";
|
|
18
|
-
import connectionFactory from "../utils/connection.js";
|
|
19
|
-
import memoryServerSetup from "../utils/database.js";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* connectionFactory opens a real mongo connection and registers a model on
|
|
23
|
-
* it. Both mongoose and the auto-increment plugin are replaced so the test
|
|
24
|
-
* observes the wiring without a database.
|
|
25
|
-
*/
|
|
26
|
-
const mocks = vi.hoisted(() => ({
|
|
27
|
-
createConnection: vi.fn(),
|
|
28
|
-
models: {} as Record<string, unknown>,
|
|
29
|
-
model: vi.fn(),
|
|
30
|
-
plugin: vi.fn(),
|
|
31
|
-
connectionOptions: vi.fn(),
|
|
32
|
-
memoryUri: "mongodb://memory:27017/",
|
|
33
|
-
memoryCreate: vi.fn(),
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
vi.mock("mongoose", async (importOriginal) => {
|
|
37
|
-
const actual = await importOriginal<typeof import("mongoose")>();
|
|
38
|
-
return {
|
|
39
|
-
...actual,
|
|
40
|
-
default: {
|
|
41
|
-
...actual.default,
|
|
42
|
-
createConnection: (...args: unknown[]) => {
|
|
43
|
-
mocks.createConnection(...args);
|
|
44
|
-
return {
|
|
45
|
-
models: mocks.models,
|
|
46
|
-
model: mocks.model,
|
|
47
|
-
} as unknown as Connection;
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
vi.mock("@typegoose/auto-increment", () => ({
|
|
54
|
-
AutoIncrementID: "auto-increment-plugin",
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
vi.mock("@prosopo/database", async (importOriginal) => {
|
|
58
|
-
const actual = await importOriginal<typeof import("@prosopo/database")>();
|
|
59
|
-
return {
|
|
60
|
-
...actual,
|
|
61
|
-
getMongoConnectionOptions: (args: { url: string; appName: string }) => {
|
|
62
|
-
mocks.connectionOptions(args);
|
|
63
|
-
return { appName: args.appName };
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
vi.mock("mongodb-memory-server", () => ({
|
|
69
|
-
MongoMemoryServer: {
|
|
70
|
-
create: () => {
|
|
71
|
-
mocks.memoryCreate();
|
|
72
|
-
return Promise.resolve({ getUri: () => mocks.memoryUri });
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
}));
|
|
76
|
-
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
mocks.createConnection.mockClear();
|
|
79
|
-
mocks.model.mockClear();
|
|
80
|
-
mocks.plugin.mockClear();
|
|
81
|
-
mocks.connectionOptions.mockClear();
|
|
82
|
-
mocks.memoryCreate.mockClear();
|
|
83
|
-
mocks.models = {};
|
|
84
|
-
vi.spyOn(UserSchema, "plugin").mockImplementation(
|
|
85
|
-
(...args: unknown[]): Schema<UserInterface> => {
|
|
86
|
-
mocks.plugin(...args);
|
|
87
|
-
return UserSchema;
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("connectionFactory", () => {
|
|
93
|
-
const URI = "mongodb://localhost:27017/client-example";
|
|
94
|
-
|
|
95
|
-
test("connects to the URI it was given", () => {
|
|
96
|
-
connectionFactory(URI);
|
|
97
|
-
expect(mocks.createConnection).toHaveBeenCalledTimes(1);
|
|
98
|
-
expect(mocks.createConnection.mock.calls[0]?.[0]).toBe(URI);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("names the connection after the module, for mongo's server logs", () => {
|
|
102
|
-
connectionFactory(URI);
|
|
103
|
-
const options = mocks.createConnection.mock.calls[0]?.[1] as {
|
|
104
|
-
appName: string;
|
|
105
|
-
};
|
|
106
|
-
expect(options.appName).toContain("connection");
|
|
107
|
-
expect(mocks.connectionOptions).toHaveBeenCalledWith({
|
|
108
|
-
url: URI,
|
|
109
|
-
appName: options.appName,
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("registers the User model with auto-incrementing ids", () => {
|
|
114
|
-
connectionFactory(URI);
|
|
115
|
-
expect(mocks.plugin).toHaveBeenCalledWith("auto-increment-plugin", {
|
|
116
|
-
field: "id",
|
|
117
|
-
});
|
|
118
|
-
expect(mocks.model).toHaveBeenCalledWith("User", UserSchema);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("leaves an already-registered model alone", () => {
|
|
122
|
-
// Re-registering throws in mongoose (OverwriteModelError), so the
|
|
123
|
-
// factory has to be safe to call on a connection that has been set up.
|
|
124
|
-
mocks.models = { user: {} };
|
|
125
|
-
connectionFactory(URI);
|
|
126
|
-
expect(mocks.model).not.toHaveBeenCalled();
|
|
127
|
-
expect(mocks.plugin).not.toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("returns the connection it created", () => {
|
|
131
|
-
const connection = connectionFactory(URI);
|
|
132
|
-
expect(connection.model).toBe(mocks.model);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("memoryServerSetup", () => {
|
|
137
|
-
test("boots an in-memory mongo and returns its URI", async () => {
|
|
138
|
-
await expect(memoryServerSetup()).resolves.toBe(mocks.memoryUri);
|
|
139
|
-
expect(mocks.memoryCreate).toHaveBeenCalledTimes(1);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("each call gets its own server", async () => {
|
|
143
|
-
await memoryServerSetup();
|
|
144
|
-
await memoryServerSetup();
|
|
145
|
-
expect(mocks.memoryCreate).toHaveBeenCalledTimes(2);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("the User schema", () => {
|
|
150
|
-
test("requires the fields a login needs to work", () => {
|
|
151
|
-
// Without email, password and salt, a stored user cannot be logged in.
|
|
152
|
-
for (const field of ["email", "password", "salt"]) {
|
|
153
|
-
expect(UserSchema.path(field).isRequired).toBe(true);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("leaves name and id optional", () => {
|
|
158
|
-
// `id` is filled in by the auto-increment plugin after validation.
|
|
159
|
-
expect(UserSchema.path("name").isRequired).toBeFalsy();
|
|
160
|
-
expect(UserSchema.path("id").isRequired).toBeFalsy();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("stores the id as a number and the rest as strings", () => {
|
|
164
|
-
expect(UserSchema.path("id").instance).toBe("Number");
|
|
165
|
-
for (const field of ["email", "name", "password", "salt"]) {
|
|
166
|
-
expect(UserSchema.path(field).instance).toBe("String");
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
});
|
package/src/utils/connection.ts
DELETED
|
@@ -1,41 +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
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { getMongoConnectionOptions } from "@prosopo/database";
|
|
16
|
-
import {
|
|
17
|
-
AutoIncrementID,
|
|
18
|
-
type AutoIncrementIDOptions,
|
|
19
|
-
} from "@typegoose/auto-increment";
|
|
20
|
-
import mongoose, { type Connection } from "mongoose";
|
|
21
|
-
import UserSchema from "../models/user.js";
|
|
22
|
-
|
|
23
|
-
function connectionFactory(uri: string): Connection {
|
|
24
|
-
const appName = fileURLToPath(import.meta.url);
|
|
25
|
-
|
|
26
|
-
const conn = mongoose.createConnection(
|
|
27
|
-
uri,
|
|
28
|
-
getMongoConnectionOptions({
|
|
29
|
-
url: uri,
|
|
30
|
-
appName,
|
|
31
|
-
}),
|
|
32
|
-
);
|
|
33
|
-
if (!conn.models.user) {
|
|
34
|
-
UserSchema.plugin(AutoIncrementID, {
|
|
35
|
-
field: "id",
|
|
36
|
-
} as AutoIncrementIDOptions);
|
|
37
|
-
conn.model("User", UserSchema);
|
|
38
|
-
}
|
|
39
|
-
return conn;
|
|
40
|
-
}
|
|
41
|
-
export default connectionFactory;
|
package/src/utils/database.ts
DELETED
|
@@ -1,22 +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
|
-
export async function memoryServerSetup(): Promise<string> {
|
|
16
|
-
const MongoMemoryServer = (await import("mongodb-memory-server"))
|
|
17
|
-
.MongoMemoryServer;
|
|
18
|
-
const mongod = MongoMemoryServer.create();
|
|
19
|
-
const memoryServer = await mongod;
|
|
20
|
-
return memoryServer.getUri();
|
|
21
|
-
}
|
|
22
|
-
export default memoryServerSetup;
|