Skip to content

Commit 997b4f8

Browse files
authored
better swagger (#40)
* better swagger * more swagger tests
1 parent 0612de8 commit 997b4f8

4 files changed

Lines changed: 219 additions & 175 deletions

File tree

backend/__tests__/actions/swagger.test.ts

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,126 @@ afterAll(async () => {
1414
});
1515

1616
describe("swagger", () => {
17-
test("the swagger endpoint returns metadata about the server", async () => {
17+
test("the swagger endpoint returns valid OpenAPI 3.0 metadata", async () => {
1818
const res = await fetch(url + "/api/swagger");
1919
expect(res.status).toBe(200);
2020
const response = (await res.json()) as ActionResponse<Swagger>;
2121

22-
expect(response.basePath).toInclude("/api/");
23-
expect(response.host).toInclude(
24-
config.server.web.applicationUrl
25-
.replace(/^https?:\/\//, "")
26-
.replace(/^http?:\/\//, ""),
22+
// OpenAPI 3.0 structure
23+
expect(response.openapi).toBe("3.0.0");
24+
expect(response.info.title).toBe("actionhero");
25+
expect(response.info.version).toBeDefined();
26+
expect(response.info.license).toBeDefined();
27+
expect(response.info.description).toBeDefined();
28+
29+
expect(response.servers).toBeDefined();
30+
expect(response.servers!.length).toBeGreaterThan(0);
31+
const serverUrl = response.servers![0].url;
32+
expect(serverUrl.endsWith("/api") || serverUrl.endsWith("/api/")).toBe(
33+
true,
34+
);
35+
});
36+
37+
test("swagger documents all web actions", async () => {
38+
const res = await fetch(url + "/api/swagger");
39+
const response = (await res.json()) as ActionResponse<Swagger>;
40+
41+
// Count web actions (actions with both route and method)
42+
const webActions = api.actions.actions.filter(
43+
(action) => action.web?.route && action.web?.method,
2744
);
2845

29-
expect(Object.keys(response.paths).length).toBeGreaterThan(2);
30-
31-
expect(response.paths["/swagger"]).toEqual({
32-
get: {
33-
consumes: ["application/json"],
34-
parameters: [],
35-
produces: ["application/json"],
36-
responses: {
37-
"200": {
38-
description: "successful operation",
39-
},
40-
"400": {
41-
description: "Invalid input",
42-
},
43-
"404": {
44-
description: "Not Found",
45-
},
46-
"422": {
47-
description: "Missing or invalid params",
48-
},
49-
"500": {
50-
description: "Server error",
51-
},
52-
},
53-
security: [],
54-
summary: "Return API documentation in the OpenAPI specification",
55-
},
56-
});
46+
// Count unique routes (since multiple methods on same route are grouped)
47+
const uniqueRoutes = new Set(webActions.map((action) => action.web!.route));
48+
expect(Object.keys(response.paths).length).toBe(uniqueRoutes.size);
49+
50+
// Verify each web action is documented
51+
for (const action of webActions) {
52+
const path = action.web!.route;
53+
const method = action.web!.method.toLowerCase();
54+
55+
expect(response.paths[path]).toBeDefined();
56+
expect(response.paths[path]![method]).toBeDefined();
57+
expect(response.paths[path]![method]!.summary).toBe(
58+
action.description || action.name,
59+
);
60+
}
61+
});
62+
63+
test("swagger documents request bodies for actions with inputs", async () => {
64+
const res = await fetch(url + "/api/swagger");
65+
const response = (await res.json()) as ActionResponse<Swagger>;
66+
67+
// Find actions with Zod inputs
68+
const actionsWithInputs = api.actions.actions.filter(
69+
(action) => action.web?.route && action.web?.method && action.inputs,
70+
);
71+
72+
for (const action of actionsWithInputs) {
73+
const path = action.web!.route;
74+
const method = action.web!.method.toLowerCase();
75+
const pathObj = response.paths[path]![method]!;
76+
77+
expect(pathObj.requestBody).toBeDefined();
78+
expect(pathObj.requestBody!.required).toBe(true);
79+
expect(pathObj.requestBody!.content["application/json"]).toBeDefined();
80+
expect(
81+
pathObj.requestBody!.content["application/json"].schema,
82+
).toBeDefined();
83+
}
84+
});
85+
86+
test("swagger documents standard response codes", async () => {
87+
const res = await fetch(url + "/api/swagger");
88+
const response = (await res.json()) as ActionResponse<Swagger>;
89+
90+
// Check a specific endpoint (swagger itself)
91+
const swaggerPath = response.paths["/swagger"]!.get!;
92+
93+
expect(swaggerPath.responses["200"]).toBeDefined();
94+
expect(swaggerPath.responses["400"]).toBeDefined();
95+
expect(swaggerPath.responses["404"]).toBeDefined();
96+
expect(swaggerPath.responses["422"]).toBeDefined();
97+
expect(swaggerPath.responses["500"]).toBeDefined();
98+
99+
// Verify response schemas
100+
expect(
101+
swaggerPath.responses["200"]!.content["application/json"].schema,
102+
).toBeDefined();
103+
expect(
104+
swaggerPath.responses["400"]!.content["application/json"].schema,
105+
).toBeDefined();
106+
});
107+
108+
test("swagger groups actions by tags", async () => {
109+
const res = await fetch(url + "/api/swagger");
110+
const response = (await res.json()) as ActionResponse<Swagger>;
111+
112+
// Check that actions are tagged by their namespace
113+
for (const action of api.actions.actions) {
114+
if (!action.web?.route || !action.web?.method) continue;
115+
116+
const path = action.web.route;
117+
const method = action.web.method.toLowerCase();
118+
const pathObj = response.paths[path]![method]!;
119+
120+
expect(pathObj.tags).toBeDefined();
121+
expect(pathObj.tags!.length).toBeGreaterThan(0);
122+
123+
const expectedTag = action.name.split(":")[0];
124+
expect(pathObj.tags).toContain(expectedTag);
125+
}
126+
});
127+
128+
test("swagger endpoint itself is documented", async () => {
129+
const res = await fetch(url + "/api/swagger");
130+
const response = (await res.json()) as ActionResponse<Swagger>;
131+
132+
expect(response.paths["/swagger"]).toBeDefined();
133+
expect(response.paths["/swagger"]!.get).toBeDefined();
134+
expect(response.paths["/swagger"]!.get!.summary).toBe(
135+
"Return API documentation in the OpenAPI specification",
136+
);
137+
expect(response.paths["/swagger"]!.get!.responses["200"]).toBeDefined();
57138
});
58139
});

0 commit comments

Comments
 (0)