-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathswagger.test.ts
More file actions
263 lines (221 loc) · 10.4 KB
/
Copy pathswagger.test.ts
File metadata and controls
263 lines (221 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Swagger } from "../../actions/swagger";
import { api, type ActionResponse } from "../../api";
import { config } from "../../config";
import "./../setup";
const url = config.server.web.applicationUrl;
beforeAll(async () => {
await api.start();
});
afterAll(async () => {
await api.stop();
});
describe("swagger", () => {
test("the swagger endpoint returns valid OpenAPI 3.0 metadata", async () => {
const res = await fetch(url + "/api/swagger");
expect(res.status).toBe(200);
const response = (await res.json()) as ActionResponse<Swagger>;
// OpenAPI 3.0 structure
expect(response.openapi).toBe("3.0.0");
expect(response.info.title).toBe("actionhero");
expect(response.info.version).toBeDefined();
expect(response.info.license).toBeDefined();
expect(response.info.description).toBeDefined();
expect(response.servers).toBeDefined();
expect(response.servers!.length).toBeGreaterThan(0);
const serverUrl = response.servers![0].url;
expect(serverUrl.endsWith("/api") || serverUrl.endsWith("/api/")).toBe(
true,
);
});
// Helper function to convert route format for swagger tests
const convertRouteForSwagger = (route: string): string => {
return route.replace(/:\w+/g, (match) => `{${match.slice(1)}}`);
};
test("swagger documents all web actions", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Count web actions (actions with both route and method)
const webActions = api.actions.actions.filter(
(action) => action.web?.route && action.web?.method,
);
// Count unique routes (since multiple methods on same route are grouped)
const uniqueRoutes = new Set(
webActions.map((action) =>
convertRouteForSwagger(action.web!.route.toString()),
),
);
expect(Object.keys(response.paths).length).toBe(uniqueRoutes.size);
// Verify each web action is documented
for (const action of webActions) {
const path = convertRouteForSwagger(action.web!.route.toString());
const method = action.web!.method.toLowerCase();
expect(response.paths[path]).toBeDefined();
expect(response.paths[path]![method]).toBeDefined();
expect(response.paths[path]![method]!.summary).toBe(
action.description || action.name,
);
}
});
test("swagger documents request bodies for actions with inputs", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Find actions with Zod inputs
const actionsWithInputs = api.actions.actions.filter(
(action) => action.web?.route && action.web?.method && action.inputs,
);
for (const action of actionsWithInputs) {
const path = convertRouteForSwagger(action.web!.route.toString());
const method = action.web!.method.toLowerCase();
const pathObj = response.paths[path]![method]!;
// GET and HEAD methods should not have requestBody
if (method === "get" || method === "head") {
expect(pathObj.requestBody).toBeUndefined();
} else {
expect(pathObj.requestBody).toBeDefined();
expect(pathObj.requestBody!.required).toBe(true);
expect(pathObj.requestBody!.content["application/json"]).toBeDefined();
const schema = pathObj.requestBody!.content["application/json"].schema;
expect(schema).toBeDefined();
// Should be a $ref
expect(typeof schema.$ref).toBe("string");
// The referenced schema should exist in components.schemas
const refName = schema.$ref.replace("#/components/schemas/", "");
expect(response.components.schemas[refName]).toBeDefined();
}
}
});
test("swagger documents standard response codes", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Check a specific endpoint (swagger itself)
const swaggerPath = response.paths["/swagger"]!.get!;
expect(swaggerPath.responses["200"]).toBeDefined();
expect(swaggerPath.responses["400"]).toBeDefined();
expect(swaggerPath.responses["404"]).toBeDefined();
expect(swaggerPath.responses["422"]).toBeDefined();
expect(swaggerPath.responses["500"]).toBeDefined();
// Verify response schemas
expect(
swaggerPath.responses["200"]!.content["application/json"].schema,
).toBeDefined();
expect(
swaggerPath.responses["400"]!.content["application/json"].schema,
).toBeDefined();
});
test("swagger groups actions by tags", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Check that actions are tagged by their namespace
for (const action of api.actions.actions) {
if (!action.web?.route || !action.web?.method) continue;
const path = convertRouteForSwagger(action.web.route.toString());
const method = action.web.method.toLowerCase();
const pathObj = response.paths[path]![method]!;
expect(pathObj.tags).toBeDefined();
expect(pathObj.tags!.length).toBeGreaterThan(0);
const expectedTag = action.name.split(":")[0];
expect(pathObj.tags).toContain(expectedTag);
}
});
test("swagger endpoint itself is documented", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
expect(response.paths["/swagger"]).toBeDefined();
expect(response.paths["/swagger"]!.get).toBeDefined();
expect(response.paths["/swagger"]!.get!.summary).toBe(
"Return API documentation in the OpenAPI specification",
);
expect(response.paths["/swagger"]!.get!.responses["200"]).toBeDefined();
});
test("swagger properly enumerates UserCreate action parameters", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Check the UserCreate action (PUT /user)
const userCreatePath = response.paths["/user"]!.put!;
expect(userCreatePath.requestBody).toBeDefined();
const requestBody = userCreatePath.requestBody as any;
expect(requestBody.required).toBe(true);
expect(requestBody.content["application/json"]).toBeDefined();
const schema = requestBody.content["application/json"].schema;
expect(schema).toBeDefined();
// Should be a $ref
expect(typeof schema.$ref).toBe("string");
const refName = schema.$ref.replace("#/components/schemas/", "");
const resolved = response.components.schemas[refName];
expect(resolved).toBeDefined();
// Check that the schema is an object with properties
expect(resolved.type).toBe("object");
expect(resolved.properties).toBeDefined();
// Check that all expected UserCreate parameters are present
expect(resolved.properties.name).toBeDefined();
expect(resolved.properties.email).toBeDefined();
expect(resolved.properties.password).toBeDefined();
// Check parameter types
expect(resolved.properties.name.type).toBe("string");
expect(resolved.properties.email.type).toBe("string");
expect(resolved.properties.password.type).toBe("string");
// Check required fields
expect(resolved.required).toContain("name");
expect(resolved.required).toContain("email");
expect(resolved.required).toContain("password");
});
test("swagger documents response types from TypeScript return types", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Check the status action response schema
const statusPath = response.paths["/status"]!.get!;
expect(statusPath.responses["200"]).toBeDefined();
const responseContent =
statusPath.responses["200"]!.content["application/json"];
expect(responseContent.schema).toBeDefined();
// Should be a $ref to a response schema
const schema = responseContent.schema;
expect(schema.$ref).toBeDefined();
const refName = schema.$ref.replace("#/components/schemas/", "");
expect(refName).toBe("status_Response");
// The response schema should exist in components
const resolved = response.components.schemas[refName];
expect(resolved).toBeDefined();
expect(resolved.type).toBe("object");
expect(resolved.properties).toBeDefined();
// Check that the status response properties are present
expect(resolved.properties.name).toBeDefined();
expect(resolved.properties.pid).toBeDefined();
expect(resolved.properties.version).toBeDefined();
expect(resolved.properties.uptime).toBeDefined();
expect(resolved.properties.consumedMemoryMB).toBeDefined();
// Verify correct types are inferred (not generic objects)
expect(resolved.properties.name.type).toBe("string");
expect(resolved.properties.pid.type).toBe("number");
expect(resolved.properties.version.type).toBe("string");
expect(resolved.properties.uptime.type).toBe("number");
expect(resolved.properties.consumedMemoryMB.type).toBe("number");
// Ensure these are NOT incorrectly typed as objects with additionalProperties
expect(resolved.properties.name.additionalProperties).toBeUndefined();
expect(resolved.properties.pid.additionalProperties).toBeUndefined();
// Check required fields
expect(resolved.required).toContain("name");
expect(resolved.required).toContain("pid");
expect(resolved.required).toContain("version");
expect(resolved.required).toContain("uptime");
expect(resolved.required).toContain("consumedMemoryMB");
});
test("swagger documents response types for UserCreate action", async () => {
const res = await fetch(url + "/api/swagger");
const response = (await res.json()) as ActionResponse<Swagger>;
// Check the user:create action response schema
const userCreatePath = response.paths["/user"]!.put!;
const responseContent =
userCreatePath.responses["200"]!.content["application/json"];
const schema = responseContent.schema;
expect(schema.$ref).toBeDefined();
const refName = schema.$ref.replace("#/components/schemas/", "");
expect(refName).toBe("user_create_Response");
// The response schema should have a user property
const resolved = response.components.schemas[refName];
expect(resolved).toBeDefined();
expect(resolved.type).toBe("object");
expect(resolved.properties.user).toBeDefined();
});
});