@prosopo/api-route 2.6.51 → 2.6.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 +12 -10
- package/.turbo/turbo-build$colon$tsc.log +7 -7
- package/.turbo/turbo-build.log +14 -11
- package/CHANGELOG.md +24 -0
- package/dist/.export.d.ts +1 -1
- package/dist/.export.d.ts.map +1 -1
- package/dist/.export.js +1 -1
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/cjs/.export.cjs +0 -1
- package/dist/cjs/endpoint/.export.cjs +2 -4
- package/dist/cjs/endpoint/apiEndpointResponseStatus.cjs +8 -8
- package/dist/cjs/index.cjs +2 -5
- package/dist/endpoint/.export.js +1 -3
- package/dist/endpoint/apiEndpointResponseStatus.js +9 -9
- package/dist/index.js +2 -5
- package/dist/tests/apiRoute.test-d.d.ts +2 -0
- package/dist/tests/apiRoute.test-d.d.ts.map +1 -0
- package/dist/tests/apiRoute.test-d.js +126 -0
- package/dist/tests/apiRoute.test-d.js.map +1 -0
- package/dist/tests/apiRoute.unit.test.d.ts +2 -0
- package/dist/tests/apiRoute.unit.test.d.ts.map +1 -0
- package/dist/tests/apiRoute.unit.test.js +102 -0
- package/dist/tests/apiRoute.unit.test.js.map +1 -0
- package/package.json +10 -5
- package/src/.export.ts +4 -0
- package/src/tests/apiRoute.test-d.ts +236 -0
- package/src/tests/apiRoute.unit.test.ts +178 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +18 -0
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
// This package ships one runtime value and otherwise consists of the contracts
|
|
16
|
+
// every API endpoint in the workspace implements. Its business logic *is* its
|
|
17
|
+
// types — in particular the conditional signature of processRequest — so these
|
|
18
|
+
// are the tests that actually cover it.
|
|
19
|
+
|
|
20
|
+
import type { Logger } from "@prosopo/logger";
|
|
21
|
+
import { assertType, describe, expectTypeOf, it } from "vitest";
|
|
22
|
+
import type { ZodType, z } from "zod";
|
|
23
|
+
import type {
|
|
24
|
+
ApiEndpoint,
|
|
25
|
+
ApiEndpointResponse,
|
|
26
|
+
ApiRouteLimit,
|
|
27
|
+
ApiRouteLimits,
|
|
28
|
+
ApiRoutes,
|
|
29
|
+
ApiRoutesProvider,
|
|
30
|
+
} from "../index.js";
|
|
31
|
+
import { ApiEndpointResponseStatus } from "../index.js";
|
|
32
|
+
|
|
33
|
+
type SiteKeySchema = ZodType<{ siteKey: string }>;
|
|
34
|
+
|
|
35
|
+
describe("ApiEndpoint.processRequest", () => {
|
|
36
|
+
it("takes the parsed arguments when the endpoint declares a schema", () => {
|
|
37
|
+
// The whole point of the conditional: an endpoint with a schema receives
|
|
38
|
+
// the inferred arguments, not a raw unknown it has to parse itself.
|
|
39
|
+
expectTypeOf<
|
|
40
|
+
ApiEndpoint<SiteKeySchema>["processRequest"]
|
|
41
|
+
>().parameters.toEqualTypeOf<
|
|
42
|
+
[z.infer<SiteKeySchema>, (Logger | undefined)?]
|
|
43
|
+
>();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("takes no arguments at all when the schema is undefined", () => {
|
|
47
|
+
// The other branch. If this collapsed into the schema branch, every
|
|
48
|
+
// argument-free endpoint would be forced to accept a phantom first
|
|
49
|
+
// parameter.
|
|
50
|
+
expectTypeOf<
|
|
51
|
+
ApiEndpoint<undefined>["processRequest"]
|
|
52
|
+
>().parameters.toEqualTypeOf<[(Logger | undefined)?]>();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("keeps the logger optional on both branches", () => {
|
|
56
|
+
// Callers that do not have a logger to hand must still be able to invoke
|
|
57
|
+
// an endpoint directly.
|
|
58
|
+
const withArgs: ApiEndpoint<SiteKeySchema>["processRequest"] =
|
|
59
|
+
async () => ({
|
|
60
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
61
|
+
});
|
|
62
|
+
expectTypeOf(withArgs).toBeCallableWith({ siteKey: "abc" });
|
|
63
|
+
|
|
64
|
+
const withoutArgs: ApiEndpoint<undefined>["processRequest"] = async () => ({
|
|
65
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
66
|
+
});
|
|
67
|
+
expectTypeOf(withoutArgs).toBeCallableWith();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("always resolves to a response, never to void", () => {
|
|
71
|
+
// A void-returning endpoint would give the mounting layer nothing to
|
|
72
|
+
// serialise back to the client.
|
|
73
|
+
expectTypeOf<
|
|
74
|
+
ApiEndpoint<SiteKeySchema>["processRequest"]
|
|
75
|
+
>().returns.toEqualTypeOf<Promise<ApiEndpointResponse>>();
|
|
76
|
+
expectTypeOf<
|
|
77
|
+
ApiEndpoint<undefined>["processRequest"]
|
|
78
|
+
>().returns.toEqualTypeOf<Promise<ApiEndpointResponse>>();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects arguments that do not match the declared schema", () => {
|
|
82
|
+
const endpoint: ApiEndpoint<SiteKeySchema> = {
|
|
83
|
+
getRequestArgsSchema: (): SiteKeySchema => {
|
|
84
|
+
throw new Error("not called");
|
|
85
|
+
},
|
|
86
|
+
processRequest: async (): Promise<ApiEndpointResponse> => ({
|
|
87
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
// @ts-expect-error siteKey is required by the schema
|
|
91
|
+
endpoint.processRequest({});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects an argument-free endpoint being called with arguments", () => {
|
|
95
|
+
const endpoint: ApiEndpoint<undefined> = {
|
|
96
|
+
getRequestArgsSchema: (): undefined => undefined,
|
|
97
|
+
processRequest: async (): Promise<ApiEndpointResponse> => ({
|
|
98
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
// @ts-expect-error this endpoint declared no schema, so takes no args
|
|
102
|
+
endpoint.processRequest({ siteKey: "abc" });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("ApiEndpoint.getRequestArgsSchema", () => {
|
|
107
|
+
it("returns exactly the schema the endpoint was parameterised with", () => {
|
|
108
|
+
// Returning a widened ZodType would leave callers unable to infer the
|
|
109
|
+
// argument type from the schema they just fetched.
|
|
110
|
+
expectTypeOf<
|
|
111
|
+
ApiEndpoint<SiteKeySchema>["getRequestArgsSchema"]
|
|
112
|
+
>().returns.toEqualTypeOf<SiteKeySchema>();
|
|
113
|
+
expectTypeOf<
|
|
114
|
+
ApiEndpoint<undefined>["getRequestArgsSchema"]
|
|
115
|
+
>().returns.toEqualTypeOf<undefined>();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("takes no arguments", () => {
|
|
119
|
+
expectTypeOf<
|
|
120
|
+
ApiEndpoint<undefined>["getRequestArgsSchema"]
|
|
121
|
+
>().parameters.toEqualTypeOf<[]>();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("ApiEndpointResponse", () => {
|
|
126
|
+
it("requires a status and nothing else", () => {
|
|
127
|
+
// A bare acknowledgement is a valid response.
|
|
128
|
+
assertType<ApiEndpointResponse>({
|
|
129
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
130
|
+
});
|
|
131
|
+
// @ts-expect-error status is mandatory
|
|
132
|
+
assertType<ApiEndpointResponse>({ data: {} });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("keeps data and error independently optional", () => {
|
|
136
|
+
// Not modelled as a discriminated union, so an endpoint may legitimately
|
|
137
|
+
// return partial data alongside an error.
|
|
138
|
+
expectTypeOf<ApiEndpointResponse["data"]>().toEqualTypeOf<
|
|
139
|
+
object | undefined
|
|
140
|
+
>();
|
|
141
|
+
expectTypeOf<ApiEndpointResponse["error"]>().toEqualTypeOf<
|
|
142
|
+
string | undefined
|
|
143
|
+
>();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("types the status as the enum, not a bare string", () => {
|
|
147
|
+
expectTypeOf<
|
|
148
|
+
ApiEndpointResponse["status"]
|
|
149
|
+
>().toEqualTypeOf<ApiEndpointResponseStatus>();
|
|
150
|
+
// @ts-expect-error an arbitrary string is not a status
|
|
151
|
+
assertType<ApiEndpointResponse>({ status: "OK" });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("rejects an error that is not a string", () => {
|
|
155
|
+
assertType<ApiEndpointResponse>({
|
|
156
|
+
status: ApiEndpointResponseStatus.FAIL,
|
|
157
|
+
// @ts-expect-error error is a message, not an Error instance
|
|
158
|
+
error: new Error("boom"),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("ApiRoutes", () => {
|
|
164
|
+
it("is keyed by route string and valued by endpoints of any schema", () => {
|
|
165
|
+
expectTypeOf<ApiRoutes>().toEqualTypeOf<
|
|
166
|
+
Record<string, ApiEndpoint<ZodType | undefined>>
|
|
167
|
+
>();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("accepts an empty record, for a provider with no endpoints yet", () => {
|
|
171
|
+
assertType<ApiRoutes>({});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("rejects a plain function in place of an endpoint", () => {
|
|
175
|
+
// @ts-expect-error routes map to endpoint objects, not handlers
|
|
176
|
+
assertType<ApiRoutes>({ "/x": async (): Promise<void> => {} });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("ApiRoutesProvider", () => {
|
|
181
|
+
it("exposes the routes through a method, not a property", () => {
|
|
182
|
+
// A method lets a provider build its routes lazily, once its own
|
|
183
|
+
// dependencies are ready.
|
|
184
|
+
expectTypeOf<ApiRoutesProvider["getRoutes"]>().toEqualTypeOf<
|
|
185
|
+
() => ApiRoutes
|
|
186
|
+
>();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("ApiRouteLimits", () => {
|
|
191
|
+
it("requires a limit for every member of the route enum", () => {
|
|
192
|
+
// Partial coverage would leave a route silently unlimited.
|
|
193
|
+
enum Route {
|
|
194
|
+
Verify = "verify",
|
|
195
|
+
Submit = "submit",
|
|
196
|
+
}
|
|
197
|
+
assertType<ApiRouteLimits<Route>>({
|
|
198
|
+
[Route.Verify]: { windowMs: 1000, limit: 5 },
|
|
199
|
+
[Route.Submit]: { windowMs: 1000, limit: 5 },
|
|
200
|
+
});
|
|
201
|
+
// @ts-expect-error every route needs a limit
|
|
202
|
+
assertType<ApiRouteLimits<Route>>({
|
|
203
|
+
[Route.Verify]: { windowMs: 1000, limit: 5 },
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("requires both the window and the count on each limit", () => {
|
|
208
|
+
// A window without a count, or a count without a window, is not a rate
|
|
209
|
+
// limit — it is a config mistake that would otherwise reach production.
|
|
210
|
+
assertType<ApiRouteLimits<"verify">>({
|
|
211
|
+
// @ts-expect-error limit is required alongside windowMs
|
|
212
|
+
verify: { windowMs: 1000 },
|
|
213
|
+
});
|
|
214
|
+
assertType<ApiRouteLimits<"verify">>({
|
|
215
|
+
// @ts-expect-error windowMs is required alongside limit
|
|
216
|
+
verify: { limit: 5 },
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("exposes the entry type, so a consumer can name one limit", () => {
|
|
221
|
+
// Building a limits record entry by entry is the common case; without
|
|
222
|
+
// this export the shape has to be redeclared at every call site.
|
|
223
|
+
expectTypeOf<
|
|
224
|
+
ApiRouteLimits<"verify">["verify"]
|
|
225
|
+
>().toEqualTypeOf<ApiRouteLimit>();
|
|
226
|
+
assertType<ApiRouteLimit>({ windowMs: 1000, limit: 5 });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("works with a numeric route enum as well as a string one", () => {
|
|
230
|
+
// The parameter is `string | number`, so both enum flavours are usable.
|
|
231
|
+
assertType<ApiRouteLimits<0 | 1>>({
|
|
232
|
+
0: { windowMs: 1000, limit: 5 },
|
|
233
|
+
1: { windowMs: 1000, limit: 5 },
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
// This package is almost entirely types; the only runtime value it ships is the
|
|
16
|
+
// response status enum, and the only runtime behaviour is what its index
|
|
17
|
+
// re-exports. Both matter: the enum's string values travel over the wire to
|
|
18
|
+
// clients that compare them literally, and a dropped re-export is a breaking
|
|
19
|
+
// change that compiles fine inside this package. The contracts themselves are
|
|
20
|
+
// covered in apiRoute.test-d.ts.
|
|
21
|
+
|
|
22
|
+
import { describe, expect, it } from "vitest";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import * as api from "../index.js";
|
|
25
|
+
import type {
|
|
26
|
+
ApiEndpoint,
|
|
27
|
+
ApiEndpointResponse,
|
|
28
|
+
ApiRoutes,
|
|
29
|
+
ApiRoutesProvider,
|
|
30
|
+
} from "../index.js";
|
|
31
|
+
import { ApiEndpointResponseStatus } from "../index.js";
|
|
32
|
+
|
|
33
|
+
describe("ApiEndpointResponseStatus", () => {
|
|
34
|
+
it("keeps the exact strings clients compare against", () => {
|
|
35
|
+
// These cross the wire. Renaming a member is invisible to this package's
|
|
36
|
+
// own compiler but breaks every consumer reading the JSON.
|
|
37
|
+
expect(ApiEndpointResponseStatus.SUCCESS).toBe("SUCCESS");
|
|
38
|
+
expect(ApiEndpointResponseStatus.FAIL).toBe("FAIL");
|
|
39
|
+
expect(ApiEndpointResponseStatus.PROCESSING).toBe("PROCESSING");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("declares exactly three statuses", () => {
|
|
43
|
+
// A fourth would need handling in every consumer's switch; catching the
|
|
44
|
+
// addition here forces that to be a deliberate change.
|
|
45
|
+
expect(Object.keys(ApiEndpointResponseStatus)).toEqual([
|
|
46
|
+
"SUCCESS",
|
|
47
|
+
"FAIL",
|
|
48
|
+
"PROCESSING",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("is a string enum, so it has no reverse numeric mapping", () => {
|
|
53
|
+
// Numeric enums gain reverse keys, which would make Object.keys above and
|
|
54
|
+
// any `key in Status` check behave differently.
|
|
55
|
+
expect(Object.values(ApiEndpointResponseStatus)).toEqual([
|
|
56
|
+
"SUCCESS",
|
|
57
|
+
"FAIL",
|
|
58
|
+
"PROCESSING",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("distinguishes failure from a still-running request", () => {
|
|
63
|
+
// PROCESSING is not a terminal state; conflating it with FAIL would have
|
|
64
|
+
// callers give up on work that is still in flight.
|
|
65
|
+
expect(ApiEndpointResponseStatus.PROCESSING).not.toBe(
|
|
66
|
+
ApiEndpointResponseStatus.FAIL,
|
|
67
|
+
);
|
|
68
|
+
expect(ApiEndpointResponseStatus.PROCESSING).not.toBe(
|
|
69
|
+
ApiEndpointResponseStatus.SUCCESS,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("public export surface", () => {
|
|
75
|
+
it("re-exports the status enum from the package root", () => {
|
|
76
|
+
// Consumers import from "@prosopo/api-route", not the inner paths.
|
|
77
|
+
expect(api.ApiEndpointResponseStatus).toBe(ApiEndpointResponseStatus);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("ships the enum as its only runtime export", () => {
|
|
81
|
+
// Everything else is type-only and must erase completely; a stray runtime
|
|
82
|
+
// value here would mean the package cannot be fully tree-shaken away.
|
|
83
|
+
expect(Object.keys(api)).toEqual(["ApiEndpointResponseStatus"]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("endpoint contract", () => {
|
|
88
|
+
/** An endpoint whose request carries arguments. */
|
|
89
|
+
const schema = z.object({ siteKey: z.string() });
|
|
90
|
+
|
|
91
|
+
const endpointWithArgs: ApiEndpoint<typeof schema> = {
|
|
92
|
+
getRequestArgsSchema: () => schema,
|
|
93
|
+
processRequest: async (
|
|
94
|
+
args: z.infer<typeof schema>,
|
|
95
|
+
): Promise<ApiEndpointResponse> => ({
|
|
96
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
97
|
+
data: { siteKey: args.siteKey },
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const endpointWithoutArgs: ApiEndpoint<undefined> = {
|
|
102
|
+
getRequestArgsSchema: () => undefined,
|
|
103
|
+
processRequest: async (): Promise<ApiEndpointResponse> => ({
|
|
104
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
it("lets an implementation validate its own arguments with its schema", () => {
|
|
109
|
+
// The point of getRequestArgsSchema: a caller can parse untrusted input
|
|
110
|
+
// against it before ever invoking processRequest.
|
|
111
|
+
const parsed = endpointWithArgs
|
|
112
|
+
.getRequestArgsSchema()
|
|
113
|
+
.safeParse({ siteKey: "abc" });
|
|
114
|
+
expect(parsed.success).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects arguments the endpoint's own schema does not accept", () => {
|
|
118
|
+
const parsed = endpointWithArgs.getRequestArgsSchema().safeParse({});
|
|
119
|
+
expect(parsed.success).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("allows an endpoint that takes no arguments at all", async () => {
|
|
123
|
+
// The `undefined` branch exists precisely so such an endpoint does not
|
|
124
|
+
// have to invent an empty schema.
|
|
125
|
+
expect(endpointWithoutArgs.getRequestArgsSchema()).toBeUndefined();
|
|
126
|
+
await expect(endpointWithoutArgs.processRequest()).resolves.toEqual({
|
|
127
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("carries the request arguments through to the response", async () => {
|
|
132
|
+
await expect(
|
|
133
|
+
endpointWithArgs.processRequest({ siteKey: "abc" }),
|
|
134
|
+
).resolves.toEqual({
|
|
135
|
+
status: ApiEndpointResponseStatus.SUCCESS,
|
|
136
|
+
data: { siteKey: "abc" },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("permits a response with neither data nor error", async () => {
|
|
141
|
+
// status is the only required field; a bare acknowledgement is valid.
|
|
142
|
+
const response: ApiEndpointResponse =
|
|
143
|
+
await endpointWithoutArgs.processRequest();
|
|
144
|
+
expect(response.data).toBeUndefined();
|
|
145
|
+
expect(response.error).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("holds endpoints with differing schemas in one routes record", () => {
|
|
149
|
+
// ApiRoutes is keyed by string and valued by endpoints of any schema, so
|
|
150
|
+
// a provider can mix argument-taking and argument-free endpoints.
|
|
151
|
+
const routes: ApiRoutes = {
|
|
152
|
+
"/with": endpointWithArgs,
|
|
153
|
+
"/without": endpointWithoutArgs,
|
|
154
|
+
};
|
|
155
|
+
const provider: ApiRoutesProvider = { getRoutes: () => routes };
|
|
156
|
+
|
|
157
|
+
expect(Object.keys(provider.getRoutes())).toEqual(["/with", "/without"]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("tolerates a provider that exposes no routes", () => {
|
|
161
|
+
// Length 0: a provider registered before its endpoints exist must not be
|
|
162
|
+
// a special case for whatever iterates the record.
|
|
163
|
+
const provider: ApiRoutesProvider = { getRoutes: () => ({}) };
|
|
164
|
+
expect(Object.keys(provider.getRoutes())).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("surfaces a rejection rather than swallowing it into a FAIL response", async () => {
|
|
168
|
+
// Nothing in this contract catches; a throwing endpoint rejects, and the
|
|
169
|
+
// layer that mounts it is responsible for turning that into a response.
|
|
170
|
+
const throwing: ApiEndpoint<undefined> = {
|
|
171
|
+
getRequestArgsSchema: () => undefined,
|
|
172
|
+
processRequest: async (): Promise<ApiEndpointResponse> => {
|
|
173
|
+
throw new Error("boom");
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
await expect(throwing.processRequest()).rejects.toThrow("boom");
|
|
177
|
+
});
|
|
178
|
+
});
|