@prosopo/api-express-router 3.1.51 → 3.1.53
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 -2
- package/.turbo/turbo-build$colon$tsc.log +21 -21
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +11 -0
- package/dist/tests/apiExpressRouter.test-d.d.ts +2 -0
- package/dist/tests/apiExpressRouter.test-d.d.ts.map +1 -0
- package/dist/tests/apiExpressRouter.test-d.js +76 -0
- package/dist/tests/apiExpressRouter.test-d.js.map +1 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.d.ts +2 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.js +197 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.js.map +1 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.d.ts +2 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.js +108 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.js.map +1 -0
- package/dist/tests/unit/index.unit.test.d.ts +2 -0
- package/dist/tests/unit/index.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/index.unit.test.js +44 -0
- package/dist/tests/unit/index.unit.test.js.map +1 -0
- package/dist/tests/unit/middlewares/authMiddleware.unit.test.js +210 -136
- package/dist/tests/unit/middlewares/authMiddleware.unit.test.js.map +1 -1
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.d.ts +2 -0
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.js +242 -0
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.js.map +1 -0
- package/dist/tests/unit/testDoubles.d.ts +8 -0
- package/dist/tests/unit/testDoubles.d.ts.map +1 -0
- package/dist/tests/unit/testDoubles.js +11 -0
- package/dist/tests/unit/testDoubles.js.map +1 -0
- package/package.json +2 -2
- package/src/tests/apiExpressRouter.test-d.ts +190 -0
- package/src/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.ts +395 -0
- package/src/tests/unit/apiExpressRouterFactory.unit.test.ts +215 -0
- package/src/tests/unit/index.unit.test.ts +95 -0
- package/src/tests/unit/middlewares/authMiddleware.unit.test.ts +318 -159
- package/src/tests/unit/middlewares/requestLoggerMiddleware.unit.test.ts +368 -0
- package/src/tests/unit/testDoubles.ts +39 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
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 {
|
|
16
|
+
type ApiEndpoint,
|
|
17
|
+
ApiEndpointResponseStatus,
|
|
18
|
+
type ApiRoutes,
|
|
19
|
+
type ApiRoutesProvider,
|
|
20
|
+
} from "@prosopo/api-route";
|
|
21
|
+
import type { NextFunction, Request, Response, Router } from "express";
|
|
22
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
23
|
+
import type { ZodType } from "zod";
|
|
24
|
+
import { ApiExpressRouterFactory } from "../../apiExpressRouterFactory.js";
|
|
25
|
+
import type { ApiExpressEndpointAdapter } from "../../endpointAdapter/apiExpressEndpointAdapter.js";
|
|
26
|
+
import { handleErrors } from "../../errorHandler.js";
|
|
27
|
+
import { captureNext } from "./testDoubles.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The factory is the wiring between a routes provider and express. What matters
|
|
31
|
+
* is that every declared route reaches the adapter untouched, that the error
|
|
32
|
+
* handler is mounted last, and that a provider handing back nothing still
|
|
33
|
+
* produces a usable router rather than throwing.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
type Handler = (
|
|
37
|
+
request: Request,
|
|
38
|
+
response: Response,
|
|
39
|
+
next: NextFunction,
|
|
40
|
+
) => Promise<void>;
|
|
41
|
+
|
|
42
|
+
const endpoint = (): ApiEndpoint<ZodType | undefined> => ({
|
|
43
|
+
getRequestArgsSchema: vi.fn<() => ZodType | undefined>(() => undefined),
|
|
44
|
+
processRequest: vi.fn<ApiEndpoint<ZodType | undefined>["processRequest"]>(
|
|
45
|
+
async () => ({ status: ApiEndpointResponseStatus.SUCCESS }),
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const provider = (routes: ApiRoutes): ApiRoutesProvider => ({
|
|
50
|
+
getRoutes: (): ApiRoutes => routes,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const adapter = (): ApiExpressEndpointAdapter => ({
|
|
54
|
+
handleRequest: vi.fn<ApiExpressEndpointAdapter["handleRequest"]>(
|
|
55
|
+
async () => undefined,
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** The (path, handler) pairs the factory registered as POST routes. */
|
|
60
|
+
const postRoutes = (router: Router): [string, Handler][] => {
|
|
61
|
+
const registered: [string, Handler][] = [];
|
|
62
|
+
for (const layer of router.stack) {
|
|
63
|
+
const route = layer.route;
|
|
64
|
+
if (!route) continue;
|
|
65
|
+
const handler = route.stack[route.stack.length - 1]?.handle as
|
|
66
|
+
| Handler
|
|
67
|
+
| undefined;
|
|
68
|
+
if (handler) registered.push([route.path, handler]);
|
|
69
|
+
}
|
|
70
|
+
return registered;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let factory: ApiExpressRouterFactory;
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
factory = new ApiExpressRouterFactory();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("createRouter", () => {
|
|
80
|
+
test("registers one POST route per entry the provider declares", () => {
|
|
81
|
+
const router = factory.createRouter(
|
|
82
|
+
provider({ "/a": endpoint(), "/b": endpoint() }),
|
|
83
|
+
adapter(),
|
|
84
|
+
);
|
|
85
|
+
expect(postRoutes(router).map(([path]) => path)).toEqual(["/a", "/b"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("registers the routes as POST only", () => {
|
|
89
|
+
// express records the verbs it accepted on the route itself; anything
|
|
90
|
+
// beyond post here would widen the API surface silently.
|
|
91
|
+
const router = factory.createRouter(
|
|
92
|
+
provider({ "/a": endpoint() }),
|
|
93
|
+
adapter(),
|
|
94
|
+
);
|
|
95
|
+
const route = router.stack.find((layer) => layer.route)?.route as
|
|
96
|
+
| { methods: Record<string, boolean> }
|
|
97
|
+
| undefined;
|
|
98
|
+
expect(route?.methods).toEqual({ post: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a provider with no routes still yields a router", () => {
|
|
102
|
+
// An empty route table is a legitimate state during start-up, so it must
|
|
103
|
+
// not throw: the router simply has nothing but the error handler on it.
|
|
104
|
+
const router = factory.createRouter(provider({}), adapter());
|
|
105
|
+
expect(postRoutes(router)).toEqual([]);
|
|
106
|
+
expect(router.stack).toHaveLength(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("mounts the error handler last, after every route", () => {
|
|
110
|
+
// Express only reaches an error handler declared after the handlers that
|
|
111
|
+
// might fail, so its position is load-bearing rather than cosmetic.
|
|
112
|
+
const router = factory.createRouter(
|
|
113
|
+
provider({ "/a": endpoint(), "/b": endpoint() }),
|
|
114
|
+
adapter(),
|
|
115
|
+
);
|
|
116
|
+
const last = router.stack[router.stack.length - 1];
|
|
117
|
+
expect(last?.handle).toBe(handleErrors);
|
|
118
|
+
expect(last?.route).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("asks the provider for its routes exactly once", () => {
|
|
122
|
+
const getRoutes = vi.fn<() => ApiRoutes>(() => ({ "/a": endpoint() }));
|
|
123
|
+
factory.createRouter({ getRoutes }, adapter());
|
|
124
|
+
expect(getRoutes).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("each call builds a fresh router", () => {
|
|
128
|
+
const routes = provider({ "/a": endpoint() });
|
|
129
|
+
expect(factory.createRouter(routes, adapter())).not.toBe(
|
|
130
|
+
factory.createRouter(routes, adapter()),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("routes named oddly are passed to express verbatim", () => {
|
|
135
|
+
// The factory does no normalising of its own, so a provider is free to
|
|
136
|
+
// use path parameters or a bare root route.
|
|
137
|
+
const router = factory.createRouter(
|
|
138
|
+
provider({ "/": endpoint(), "/nested/:id": endpoint() }),
|
|
139
|
+
adapter(),
|
|
140
|
+
);
|
|
141
|
+
expect(postRoutes(router).map(([path]) => path)).toEqual([
|
|
142
|
+
"/",
|
|
143
|
+
"/nested/:id",
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("the handler each route is given", () => {
|
|
149
|
+
test("hands the endpoint and the express trio to the adapter", async () => {
|
|
150
|
+
const target = endpoint();
|
|
151
|
+
const endpointAdapter = adapter();
|
|
152
|
+
const router = factory.createRouter(
|
|
153
|
+
provider({ "/a": target }),
|
|
154
|
+
endpointAdapter,
|
|
155
|
+
);
|
|
156
|
+
const request = {} as Request;
|
|
157
|
+
const response = {} as Response;
|
|
158
|
+
const next = captureNext();
|
|
159
|
+
|
|
160
|
+
const handler = postRoutes(router)[0]?.[1];
|
|
161
|
+
await handler?.(request, response, next.fn);
|
|
162
|
+
|
|
163
|
+
expect(endpointAdapter.handleRequest).toHaveBeenCalledWith(
|
|
164
|
+
target,
|
|
165
|
+
request,
|
|
166
|
+
response,
|
|
167
|
+
next.fn,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("routes each path to its own endpoint, not the last one registered", () => {
|
|
172
|
+
// A loop that closes over the wrong variable would send every request to
|
|
173
|
+
// whichever endpoint happened to be last.
|
|
174
|
+
const first = endpoint();
|
|
175
|
+
const second = endpoint();
|
|
176
|
+
const endpointAdapter = adapter();
|
|
177
|
+
const router = factory.createRouter(
|
|
178
|
+
provider({ "/first": first, "/second": second }),
|
|
179
|
+
endpointAdapter,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
for (const [, handler] of postRoutes(router)) {
|
|
183
|
+
void handler({} as Request, {} as Response, captureNext().fn);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const handled = vi.mocked(endpointAdapter.handleRequest).mock.calls;
|
|
187
|
+
expect(handled.map(([called]) => called)).toEqual([first, second]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("propagates a rejection from the adapter to the caller", async () => {
|
|
191
|
+
// The handler returns the adapter's promise, so express receives the
|
|
192
|
+
// rejection rather than it becoming an unhandled one.
|
|
193
|
+
const endpointAdapter = adapter();
|
|
194
|
+
vi.mocked(endpointAdapter.handleRequest).mockRejectedValue(
|
|
195
|
+
new Error("adapter blew up"),
|
|
196
|
+
);
|
|
197
|
+
const router = factory.createRouter(
|
|
198
|
+
provider({ "/a": endpoint() }),
|
|
199
|
+
endpointAdapter,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const handler = postRoutes(router)[0]?.[1];
|
|
203
|
+
await expect(
|
|
204
|
+
handler?.({} as Request, {} as Response, captureNext().fn),
|
|
205
|
+
).rejects.toThrow("adapter blew up");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("does not call the endpoint itself — that is the adapter's job", async () => {
|
|
209
|
+
const target = endpoint();
|
|
210
|
+
const router = factory.createRouter(provider({ "/a": target }), adapter());
|
|
211
|
+
const handler = postRoutes(router)[0]?.[1];
|
|
212
|
+
await handler?.({} as Request, {} as Response, captureNext().fn);
|
|
213
|
+
expect(target.processRequest).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
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 { ApiRoutes, ApiRoutesProvider } from "@prosopo/api-route";
|
|
16
|
+
import { LogLevel } from "@prosopo/logger";
|
|
17
|
+
import type { NextFunction, Request, Response } from "express";
|
|
18
|
+
import { describe, expect, test, vi } from "vitest";
|
|
19
|
+
import {
|
|
20
|
+
apiExpressRouterFactory,
|
|
21
|
+
authMiddleware,
|
|
22
|
+
createApiExpressDefaultEndpointAdapter,
|
|
23
|
+
handleErrors,
|
|
24
|
+
requestLoggerMiddleware,
|
|
25
|
+
} from "../../index.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The barrel is what every consuming package imports, so the shape of it is
|
|
29
|
+
* part of the contract — a missing re-export or a changed default breaks
|
|
30
|
+
* callers without touching any of the implementations.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
describe("createApiExpressDefaultEndpointAdapter", () => {
|
|
34
|
+
test("builds an adapter exposing handleRequest", () => {
|
|
35
|
+
const adapter = createApiExpressDefaultEndpointAdapter(LogLevel.enum.info);
|
|
36
|
+
expect(typeof adapter.handleRequest).toBe("function");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("builds a fresh adapter per call", () => {
|
|
40
|
+
expect(createApiExpressDefaultEndpointAdapter(LogLevel.enum.info)).not.toBe(
|
|
41
|
+
createApiExpressDefaultEndpointAdapter(LogLevel.enum.info),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("defaults the error status code so callers may omit it", () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
createApiExpressDefaultEndpointAdapter(LogLevel.enum.debug),
|
|
48
|
+
).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("accepts an explicit error status code", () => {
|
|
52
|
+
expect(() =>
|
|
53
|
+
createApiExpressDefaultEndpointAdapter(LogLevel.enum.debug, 503),
|
|
54
|
+
).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("apiExpressRouterFactory", () => {
|
|
59
|
+
test("is exported as a single shared instance", () => {
|
|
60
|
+
// It holds no per-router state, so one instance is enough — but callers
|
|
61
|
+
// import the value rather than the class, so it must stay a singleton.
|
|
62
|
+
expect(typeof apiExpressRouterFactory.createRouter).toBe("function");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("builds a router from a provider", () => {
|
|
66
|
+
const provider: ApiRoutesProvider = {
|
|
67
|
+
getRoutes: (): ApiRoutes => ({}),
|
|
68
|
+
};
|
|
69
|
+
const router = apiExpressRouterFactory.createRouter(provider, {
|
|
70
|
+
handleRequest: vi.fn<
|
|
71
|
+
(
|
|
72
|
+
endpoint: never,
|
|
73
|
+
request: Request,
|
|
74
|
+
response: Response,
|
|
75
|
+
next: NextFunction,
|
|
76
|
+
) => Promise<void>
|
|
77
|
+
>(async () => undefined),
|
|
78
|
+
});
|
|
79
|
+
expect(typeof router).toBe("function");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("the re-exported middleware", () => {
|
|
84
|
+
test("exposes the error handler", () => {
|
|
85
|
+
expect(typeof handleErrors).toBe("function");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("exposes the auth middleware", () => {
|
|
89
|
+
expect(typeof authMiddleware).toBe("function");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("exposes the request logger middleware", () => {
|
|
93
|
+
expect(typeof requestLoggerMiddleware).toBe("function");
|
|
94
|
+
});
|
|
95
|
+
});
|