@terreno/api 0.0.1
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/LICENSE +202 -0
- package/README.md +170 -0
- package/biome.jsonc +22 -0
- package/bunfig.toml +4 -0
- package/dist/api.d.ts +227 -0
- package/dist/api.js +1024 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +2143 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +512 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +778 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.js +216 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +118 -0
- package/dist/expressServer.d.ts +35 -0
- package/dist/expressServer.js +436 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +30 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +249 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.js +52 -0
- package/dist/notifiers/googleChatNotifier.d.ts +5 -0
- package/dist/notifiers/googleChatNotifier.js +130 -0
- package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
- package/dist/notifiers/googleChatNotifier.test.js +260 -0
- package/dist/notifiers/index.d.ts +3 -0
- package/dist/notifiers/index.js +19 -0
- package/dist/notifiers/slackNotifier.d.ts +5 -0
- package/dist/notifiers/slackNotifier.js +130 -0
- package/dist/notifiers/slackNotifier.test.d.ts +1 -0
- package/dist/notifiers/slackNotifier.test.js +259 -0
- package/dist/notifiers/zoomNotifier.d.ts +34 -0
- package/dist/notifiers/zoomNotifier.js +181 -0
- package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
- package/dist/notifiers/zoomNotifier.test.js +370 -0
- package/dist/openApi.d.ts +60 -0
- package/dist/openApi.js +441 -0
- package/dist/openApi.test.d.ts +1 -0
- package/dist/openApi.test.js +445 -0
- package/dist/openApiBuilder.d.ts +419 -0
- package/dist/openApiBuilder.js +424 -0
- package/dist/openApiBuilder.test.d.ts +1 -0
- package/dist/openApiBuilder.test.js +509 -0
- package/dist/openApiEtag.d.ts +7 -0
- package/dist/openApiEtag.js +38 -0
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +331 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/permissions.test.js +413 -0
- package/dist/plugins.d.ts +67 -0
- package/dist/plugins.js +315 -0
- package/dist/plugins.test.d.ts +1 -0
- package/dist/plugins.test.js +639 -0
- package/dist/populate.d.ts +14 -0
- package/dist/populate.js +315 -0
- package/dist/populate.test.d.ts +1 -0
- package/dist/populate.test.js +133 -0
- package/dist/response.d.ts +0 -0
- package/dist/response.js +1 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +297 -0
- package/dist/tests/index.d.ts +1 -0
- package/dist/tests/index.js +17 -0
- package/dist/tests.d.ts +99 -0
- package/dist/tests.js +273 -0
- package/dist/transformers.d.ts +25 -0
- package/dist/transformers.js +217 -0
- package/dist/transformers.test.d.ts +1 -0
- package/dist/transformers.test.js +370 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +143 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +14 -0
- package/index.ts +1 -0
- package/package.json +88 -0
- package/src/__snapshots__/openApi.test.ts.snap +4814 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
- package/src/api.test.ts +1661 -0
- package/src/api.ts +1036 -0
- package/src/auth.test.ts +550 -0
- package/src/auth.ts +408 -0
- package/src/errors.ts +225 -0
- package/src/example.ts +99 -0
- package/src/express.d.ts +5 -0
- package/src/expressServer.ts +387 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +190 -0
- package/src/middleware.ts +18 -0
- package/src/notifiers/googleChatNotifier.test.ts +114 -0
- package/src/notifiers/googleChatNotifier.ts +47 -0
- package/src/notifiers/index.ts +3 -0
- package/src/notifiers/slackNotifier.test.ts +113 -0
- package/src/notifiers/slackNotifier.ts +55 -0
- package/src/notifiers/zoomNotifier.test.ts +207 -0
- package/src/notifiers/zoomNotifier.ts +111 -0
- package/src/openApi.test.ts +331 -0
- package/src/openApi.ts +494 -0
- package/src/openApiBuilder.test.ts +442 -0
- package/src/openApiBuilder.ts +636 -0
- package/src/openApiEtag.ts +40 -0
- package/src/permissions.test.ts +219 -0
- package/src/permissions.ts +228 -0
- package/src/plugins.test.ts +390 -0
- package/src/plugins.ts +289 -0
- package/src/populate.test.ts +65 -0
- package/src/populate.ts +258 -0
- package/src/response.ts +0 -0
- package/src/tests/bunSetup.ts +234 -0
- package/src/tests/index.ts +1 -0
- package/src/tests.ts +218 -0
- package/src/transformers.test.ts +202 -0
- package/src/transformers.ts +170 -0
- package/src/utils.test.ts +14 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +60 -0
- package/types.d.ts +17 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import type {Router} from "express";
|
|
4
|
+
import supertest from "supertest";
|
|
5
|
+
import type TestAgent from "supertest/lib/agent";
|
|
6
|
+
|
|
7
|
+
import {modelRouter, type modelRouterOptions} from "./api";
|
|
8
|
+
import {addAuthRoutes, setupAuth} from "./auth";
|
|
9
|
+
import {setupServer} from "./expressServer";
|
|
10
|
+
import {createOpenApiBuilder, OpenApiMiddlewareBuilder} from "./openApiBuilder";
|
|
11
|
+
import {Permissions} from "./permissions";
|
|
12
|
+
import {FoodModel, UserModel} from "./tests";
|
|
13
|
+
|
|
14
|
+
function addRoutesWithBuilder(router: Router, options?: Partial<modelRouterOptions<any>>): void {
|
|
15
|
+
// Add a custom endpoint using the OpenApiMiddlewareBuilder
|
|
16
|
+
const statsMiddleware = createOpenApiBuilder(options ?? {})
|
|
17
|
+
.withTags(["Stats"])
|
|
18
|
+
.withSummary("Get food statistics")
|
|
19
|
+
.withDescription("Returns aggregated statistics about food items")
|
|
20
|
+
.withQueryParameter(
|
|
21
|
+
"category",
|
|
22
|
+
{type: "string"},
|
|
23
|
+
{
|
|
24
|
+
description: "Filter by food category",
|
|
25
|
+
required: false,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
.withResponse<{count: number; avgCalories: number}>(200, {
|
|
29
|
+
avgCalories: {description: "Average calories", type: "number"},
|
|
30
|
+
count: {description: "Total number of food items", type: "number"},
|
|
31
|
+
})
|
|
32
|
+
.build();
|
|
33
|
+
|
|
34
|
+
router.get("/food/stats", statsMiddleware, async (_req, res) => {
|
|
35
|
+
res.json({avgCalories: 250, count: 10});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Add endpoint with request body
|
|
39
|
+
const createReportMiddleware = createOpenApiBuilder(options ?? {})
|
|
40
|
+
.withTags(["Reports"])
|
|
41
|
+
.withSummary("Create a food report")
|
|
42
|
+
.withDescription("Generates a report based on provided criteria")
|
|
43
|
+
.withRequestBody<{
|
|
44
|
+
startDate: string;
|
|
45
|
+
endDate: string;
|
|
46
|
+
includeDeleted: boolean;
|
|
47
|
+
}>({
|
|
48
|
+
endDate: {
|
|
49
|
+
description: "Report end date",
|
|
50
|
+
format: "date",
|
|
51
|
+
required: true,
|
|
52
|
+
type: "string",
|
|
53
|
+
},
|
|
54
|
+
includeDeleted: {
|
|
55
|
+
description: "Whether to include deleted items",
|
|
56
|
+
type: "boolean",
|
|
57
|
+
},
|
|
58
|
+
startDate: {
|
|
59
|
+
description: "Report start date",
|
|
60
|
+
format: "date",
|
|
61
|
+
required: true,
|
|
62
|
+
type: "string",
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
.withResponse<{reportId: string}>(
|
|
66
|
+
201,
|
|
67
|
+
{
|
|
68
|
+
reportId: {description: "Generated report ID", type: "string"},
|
|
69
|
+
},
|
|
70
|
+
{description: "Report created successfully"}
|
|
71
|
+
)
|
|
72
|
+
.build();
|
|
73
|
+
|
|
74
|
+
router.post("/food/reports", createReportMiddleware, async (_req, res) => {
|
|
75
|
+
res.status(201).json({reportId: "report-123"});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Add endpoint with array response
|
|
79
|
+
const listCategoriesMiddleware = createOpenApiBuilder(options ?? {})
|
|
80
|
+
.withTags(["Categories"])
|
|
81
|
+
.withSummary("List food categories")
|
|
82
|
+
.withArrayResponse<{id: string; name: string; count: number}>(
|
|
83
|
+
200,
|
|
84
|
+
{
|
|
85
|
+
count: {description: "Number of items in category", type: "number"},
|
|
86
|
+
id: {description: "Category ID", type: "string"},
|
|
87
|
+
name: {description: "Category name", type: "string"},
|
|
88
|
+
},
|
|
89
|
+
{description: "List of categories"}
|
|
90
|
+
)
|
|
91
|
+
.build();
|
|
92
|
+
|
|
93
|
+
router.get("/food/categories", listCategoriesMiddleware, async (_req, res) => {
|
|
94
|
+
res.json([
|
|
95
|
+
{count: 5, id: "1", name: "Fruits"},
|
|
96
|
+
{count: 3, id: "2", name: "Vegetables"},
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Add endpoint with path parameter
|
|
101
|
+
const getCategoryMiddleware = createOpenApiBuilder(options ?? {})
|
|
102
|
+
.withTags(["Categories"])
|
|
103
|
+
.withSummary("Get category by ID")
|
|
104
|
+
.withPathParameter(
|
|
105
|
+
"categoryId",
|
|
106
|
+
{type: "string"},
|
|
107
|
+
{
|
|
108
|
+
description: "The category identifier",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
.withResponse<{id: string; name: string}>(200, {
|
|
112
|
+
id: {type: "string"},
|
|
113
|
+
name: {type: "string"},
|
|
114
|
+
})
|
|
115
|
+
.withResponse(404, "Category not found")
|
|
116
|
+
.build();
|
|
117
|
+
|
|
118
|
+
router.get("/food/categories/:categoryId", getCategoryMiddleware, async (req, res) => {
|
|
119
|
+
res.json({id: req.params.categoryId, name: "Fruits"});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Standard modelRouter for food
|
|
123
|
+
router.use(
|
|
124
|
+
"/food",
|
|
125
|
+
modelRouter(FoodModel as any, {
|
|
126
|
+
...options,
|
|
127
|
+
allowAnonymous: true,
|
|
128
|
+
permissions: {
|
|
129
|
+
create: [Permissions.IsAny],
|
|
130
|
+
delete: [Permissions.IsAny],
|
|
131
|
+
list: [Permissions.IsAny],
|
|
132
|
+
read: [Permissions.IsAny],
|
|
133
|
+
update: [Permissions.IsAny],
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
describe("OpenApiMiddlewareBuilder", () => {
|
|
140
|
+
let server: TestAgent;
|
|
141
|
+
let app: express.Application;
|
|
142
|
+
|
|
143
|
+
beforeEach(async () => {
|
|
144
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
145
|
+
process.env.ENABLE_SWAGGER = "true";
|
|
146
|
+
|
|
147
|
+
app = setupServer({
|
|
148
|
+
addRoutes: addRoutesWithBuilder,
|
|
149
|
+
skipListen: true,
|
|
150
|
+
userModel: UserModel as any,
|
|
151
|
+
});
|
|
152
|
+
setupAuth(app, UserModel as any);
|
|
153
|
+
addAuthRoutes(app, UserModel as any);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("builder pattern", () => {
|
|
157
|
+
it("returns a builder instance from createOpenApiBuilder", () => {
|
|
158
|
+
const builder = createOpenApiBuilder({});
|
|
159
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("supports method chaining", () => {
|
|
163
|
+
const builder = createOpenApiBuilder({});
|
|
164
|
+
const result = builder
|
|
165
|
+
.withTags(["test"])
|
|
166
|
+
.withSummary("Test summary")
|
|
167
|
+
.withDescription("Test description");
|
|
168
|
+
|
|
169
|
+
expect(result).toBe(builder);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns noop middleware when openApi is not configured", () => {
|
|
173
|
+
const middleware = createOpenApiBuilder({}).build();
|
|
174
|
+
expect(typeof middleware).toBe("function");
|
|
175
|
+
expect(middleware.length).toBe(3); // Express middleware signature
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("OpenAPI spec generation", () => {
|
|
180
|
+
it("includes custom endpoint with query parameter in OpenAPI spec", async () => {
|
|
181
|
+
server = supertest(app);
|
|
182
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
183
|
+
|
|
184
|
+
const statsPath = res.body.paths["/food/stats"];
|
|
185
|
+
expect(statsPath).toBeDefined();
|
|
186
|
+
expect(statsPath.get).toBeDefined();
|
|
187
|
+
expect(statsPath.get.tags).toContain("Stats");
|
|
188
|
+
expect(statsPath.get.summary).toBe("Get food statistics");
|
|
189
|
+
expect(statsPath.get.description).toBe("Returns aggregated statistics about food items");
|
|
190
|
+
|
|
191
|
+
// Check query parameter
|
|
192
|
+
const categoryParam = statsPath.get.parameters.find((p: any) => p.name === "category");
|
|
193
|
+
expect(categoryParam).toBeDefined();
|
|
194
|
+
expect(categoryParam.in).toBe("query");
|
|
195
|
+
expect(categoryParam.schema.type).toBe("string");
|
|
196
|
+
expect(categoryParam.description).toBe("Filter by food category");
|
|
197
|
+
expect(categoryParam.required).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("includes request body schema in OpenAPI spec", async () => {
|
|
201
|
+
server = supertest(app);
|
|
202
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
203
|
+
|
|
204
|
+
const reportsPath = res.body.paths["/food/reports"];
|
|
205
|
+
expect(reportsPath).toBeDefined();
|
|
206
|
+
expect(reportsPath.post).toBeDefined();
|
|
207
|
+
expect(reportsPath.post.tags).toContain("Reports");
|
|
208
|
+
|
|
209
|
+
const requestBody = reportsPath.post.requestBody;
|
|
210
|
+
expect(requestBody).toBeDefined();
|
|
211
|
+
expect(requestBody.required).toBe(true);
|
|
212
|
+
|
|
213
|
+
const schema = requestBody.content["application/json"].schema;
|
|
214
|
+
expect(schema.type).toBe("object");
|
|
215
|
+
expect(schema.properties.startDate.type).toBe("string");
|
|
216
|
+
expect(schema.properties.startDate.format).toBe("date");
|
|
217
|
+
expect(schema.properties.endDate.type).toBe("string");
|
|
218
|
+
expect(schema.properties.includeDeleted.type).toBe("boolean");
|
|
219
|
+
expect(schema.required).toContain("startDate");
|
|
220
|
+
expect(schema.required).toContain("endDate");
|
|
221
|
+
expect(schema.required).not.toContain("includeDeleted");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("includes response schema in OpenAPI spec", async () => {
|
|
225
|
+
server = supertest(app);
|
|
226
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
227
|
+
|
|
228
|
+
const statsPath = res.body.paths["/food/stats"];
|
|
229
|
+
const response200 = statsPath.get.responses["200"];
|
|
230
|
+
expect(response200).toBeDefined();
|
|
231
|
+
expect(response200.description).toBe("Success");
|
|
232
|
+
|
|
233
|
+
const schema = response200.content["application/json"].schema;
|
|
234
|
+
expect(schema.type).toBe("object");
|
|
235
|
+
expect(schema.properties.count.type).toBe("number");
|
|
236
|
+
expect(schema.properties.avgCalories.type).toBe("number");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("includes array response schema in OpenAPI spec", async () => {
|
|
240
|
+
server = supertest(app);
|
|
241
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
242
|
+
|
|
243
|
+
const categoriesPath = res.body.paths["/food/categories"];
|
|
244
|
+
expect(categoriesPath).toBeDefined();
|
|
245
|
+
|
|
246
|
+
const response200 = categoriesPath.get.responses["200"];
|
|
247
|
+
expect(response200.description).toBe("List of categories");
|
|
248
|
+
|
|
249
|
+
const schema = response200.content["application/json"].schema;
|
|
250
|
+
expect(schema.type).toBe("array");
|
|
251
|
+
expect(schema.items.type).toBe("object");
|
|
252
|
+
expect(schema.items.properties.id.type).toBe("string");
|
|
253
|
+
expect(schema.items.properties.name.type).toBe("string");
|
|
254
|
+
expect(schema.items.properties.count.type).toBe("number");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("includes path parameter in OpenAPI spec", async () => {
|
|
258
|
+
server = supertest(app);
|
|
259
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
260
|
+
|
|
261
|
+
const categoryPath = res.body.paths["/food/categories/{categoryId}"];
|
|
262
|
+
expect(categoryPath).toBeDefined();
|
|
263
|
+
|
|
264
|
+
const pathParam = categoryPath.get.parameters.find((p: any) => p.name === "categoryId");
|
|
265
|
+
expect(pathParam).toBeDefined();
|
|
266
|
+
expect(pathParam.in).toBe("path");
|
|
267
|
+
expect(pathParam.required).toBe(true);
|
|
268
|
+
expect(pathParam.schema.type).toBe("string");
|
|
269
|
+
expect(pathParam.description).toBe("The category identifier");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("includes custom response without body in OpenAPI spec", async () => {
|
|
273
|
+
server = supertest(app);
|
|
274
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
275
|
+
|
|
276
|
+
// Test with the 201 response from reports endpoint which has a custom description
|
|
277
|
+
const reportsPath = res.body.paths["/food/reports"];
|
|
278
|
+
const response201 = reportsPath.post.responses["201"];
|
|
279
|
+
expect(response201).toBeDefined();
|
|
280
|
+
expect(response201.description).toBe("Report created successfully");
|
|
281
|
+
expect(response201.content).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("includes default error responses", async () => {
|
|
285
|
+
server = supertest(app);
|
|
286
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
287
|
+
|
|
288
|
+
const statsPath = res.body.paths["/food/stats"];
|
|
289
|
+
const responses = statsPath.get.responses;
|
|
290
|
+
|
|
291
|
+
// Default error responses should be merged
|
|
292
|
+
expect(responses["400"]).toBeDefined();
|
|
293
|
+
expect(responses["401"]).toBeDefined();
|
|
294
|
+
expect(responses["403"]).toBeDefined();
|
|
295
|
+
expect(responses["404"]).toBeDefined();
|
|
296
|
+
expect(responses["405"]).toBeDefined();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("endpoint functionality", () => {
|
|
301
|
+
it("stats endpoint returns correct data", async () => {
|
|
302
|
+
server = supertest(app);
|
|
303
|
+
const res = await server.get("/food/stats").expect(200);
|
|
304
|
+
expect(res.body).toEqual({avgCalories: 250, count: 10});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("reports endpoint returns correct data", async () => {
|
|
308
|
+
server = supertest(app);
|
|
309
|
+
const res = await server
|
|
310
|
+
.post("/food/reports")
|
|
311
|
+
.send({endDate: "2024-12-31", startDate: "2024-01-01"})
|
|
312
|
+
.expect(201);
|
|
313
|
+
expect(res.body).toEqual({reportId: "report-123"});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("categories endpoint returns array data", async () => {
|
|
317
|
+
server = supertest(app);
|
|
318
|
+
const res = await server.get("/food/categories").expect(200);
|
|
319
|
+
expect(res.body).toHaveLength(2);
|
|
320
|
+
expect(res.body[0]).toHaveProperty("id");
|
|
321
|
+
expect(res.body[0]).toHaveProperty("name");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("category by id endpoint returns correct data", async () => {
|
|
325
|
+
server = supertest(app);
|
|
326
|
+
const res = await server.get("/food/categories/cat-123").expect(200);
|
|
327
|
+
expect(res.body).toEqual({id: "cat-123", name: "Fruits"});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("snapshot tests", () => {
|
|
332
|
+
it("matches OpenAPI spec snapshot", async () => {
|
|
333
|
+
server = supertest(app);
|
|
334
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
335
|
+
expect(res.body).toMatchSnapshot();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe("OpenApiMiddlewareBuilder without OpenAPI", () => {
|
|
341
|
+
it("build returns noop middleware when openApi.path is not configured", () => {
|
|
342
|
+
const builder = new OpenApiMiddlewareBuilder({});
|
|
343
|
+
const middleware = builder
|
|
344
|
+
.withTags(["test"])
|
|
345
|
+
.withSummary("Test")
|
|
346
|
+
.withResponse(200, {id: {type: "string"}})
|
|
347
|
+
.build();
|
|
348
|
+
|
|
349
|
+
// Middleware should be a function
|
|
350
|
+
expect(typeof middleware).toBe("function");
|
|
351
|
+
|
|
352
|
+
// Should call next() without error
|
|
353
|
+
let nextCalled = false;
|
|
354
|
+
middleware({}, {}, () => {
|
|
355
|
+
nextCalled = true;
|
|
356
|
+
});
|
|
357
|
+
expect(nextCalled).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("build returns noop middleware when options is empty", () => {
|
|
361
|
+
const middleware = createOpenApiBuilder({}).build();
|
|
362
|
+
|
|
363
|
+
let nextCalled = false;
|
|
364
|
+
middleware({}, {}, () => {
|
|
365
|
+
nextCalled = true;
|
|
366
|
+
});
|
|
367
|
+
expect(nextCalled).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe("OpenApiMiddlewareBuilder configuration", () => {
|
|
372
|
+
it("correctly extracts required fields from request body schema", () => {
|
|
373
|
+
// We can't easily test this without a mock openApi.path, but we can at least
|
|
374
|
+
// verify the builder accepts the configuration
|
|
375
|
+
const builder = createOpenApiBuilder({}).withRequestBody<{
|
|
376
|
+
required1: string;
|
|
377
|
+
required2: string;
|
|
378
|
+
optional: string;
|
|
379
|
+
}>({
|
|
380
|
+
optional: {type: "string"},
|
|
381
|
+
required1: {required: true, type: "string"},
|
|
382
|
+
required2: {required: true, type: "string"},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("supports custom media types for request body", () => {
|
|
389
|
+
const builder = createOpenApiBuilder({}).withRequestBody(
|
|
390
|
+
{data: {type: "string"}},
|
|
391
|
+
{mediaType: "application/xml"}
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("supports custom media types for response", () => {
|
|
398
|
+
const builder = createOpenApiBuilder({}).withResponse(
|
|
399
|
+
200,
|
|
400
|
+
{data: {type: "string"}},
|
|
401
|
+
{mediaType: "text/plain"}
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("supports optional request body", () => {
|
|
408
|
+
const builder = createOpenApiBuilder({}).withRequestBody(
|
|
409
|
+
{data: {type: "string"}},
|
|
410
|
+
{required: false}
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("supports multiple query parameters", () => {
|
|
417
|
+
const builder = createOpenApiBuilder({})
|
|
418
|
+
.withQueryParameter("limit", {type: "number"}, {required: false})
|
|
419
|
+
.withQueryParameter("offset", {type: "number"}, {required: false})
|
|
420
|
+
.withQueryParameter("search", {type: "string"});
|
|
421
|
+
|
|
422
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("supports multiple path parameters", () => {
|
|
426
|
+
const builder = createOpenApiBuilder({})
|
|
427
|
+
.withPathParameter("userId", {type: "string"})
|
|
428
|
+
.withPathParameter("postId", {type: "string"});
|
|
429
|
+
|
|
430
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("supports multiple responses", () => {
|
|
434
|
+
const builder = createOpenApiBuilder({})
|
|
435
|
+
.withResponse(200, {data: {type: "string"}})
|
|
436
|
+
.withResponse(201, {id: {type: "string"}})
|
|
437
|
+
.withResponse(204, "No content")
|
|
438
|
+
.withResponse(404, "Not found");
|
|
439
|
+
|
|
440
|
+
expect(builder).toBeInstanceOf(OpenApiMiddlewareBuilder);
|
|
441
|
+
});
|
|
442
|
+
});
|