@furystack/rest-service 12.2.0 → 12.3.0
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/CHANGELOG.md +44 -0
- package/esm/api-manager.d.ts +1 -1
- package/esm/api-manager.d.ts.map +1 -1
- package/esm/api-manager.js +3 -2
- package/esm/api-manager.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +29 -0
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -0
- package/esm/endpoint-generators/create-get-openapi-document-action.js +61 -0
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -0
- package/esm/endpoint-generators/index.d.ts +2 -0
- package/esm/endpoint-generators/index.d.ts.map +1 -1
- package/esm/endpoint-generators/index.js +2 -0
- package/esm/endpoint-generators/index.js.map +1 -1
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts +23 -0
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts.map +1 -0
- package/esm/endpoint-generators/with-schema-and-openapi-action.js +2 -0
- package/esm/endpoint-generators/with-schema-and-openapi-action.js.map +1 -0
- package/esm/openapi/auth-provider-to-security-scheme.d.ts +14 -0
- package/esm/openapi/auth-provider-to-security-scheme.d.ts.map +1 -0
- package/esm/openapi/auth-provider-to-security-scheme.js +35 -0
- package/esm/openapi/auth-provider-to-security-scheme.js.map +1 -0
- package/esm/openapi/auth-provider-to-security-scheme.spec.d.ts +2 -0
- package/esm/openapi/auth-provider-to-security-scheme.spec.d.ts.map +1 -0
- package/esm/openapi/auth-provider-to-security-scheme.spec.js +42 -0
- package/esm/openapi/auth-provider-to-security-scheme.spec.js.map +1 -0
- package/esm/openapi/generate-openapi-document.d.ts +21 -0
- package/esm/openapi/generate-openapi-document.d.ts.map +1 -0
- package/esm/openapi/generate-openapi-document.js +144 -0
- package/esm/openapi/generate-openapi-document.js.map +1 -0
- package/esm/openapi/generate-openapi-document.spec.d.ts +2 -0
- package/esm/openapi/generate-openapi-document.spec.d.ts.map +1 -0
- package/esm/openapi/generate-openapi-document.spec.js +643 -0
- package/esm/openapi/generate-openapi-document.spec.js.map +1 -0
- package/esm/openapi/openapi-round-trip.advanced-api.json +363 -0
- package/esm/openapi/openapi-round-trip.crud-api.json +115 -0
- package/esm/openapi/openapi-round-trip.example-api.json +71 -0
- package/esm/openapi/openapi-round-trip.spec.d.ts +2 -0
- package/esm/openapi/openapi-round-trip.spec.d.ts.map +1 -0
- package/esm/openapi/openapi-round-trip.spec.js +525 -0
- package/esm/openapi/openapi-round-trip.spec.js.map +1 -0
- package/esm/swagger/generate-swagger-json.spec.js +1 -1
- package/esm/swagger/generate-swagger-json.spec.js.map +1 -1
- package/esm/validate.integration.spec.js +153 -32
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +9 -9
- package/src/api-manager.ts +7 -3
- package/src/endpoint-generators/create-get-openapi-document-action.ts +96 -0
- package/src/endpoint-generators/index.ts +2 -0
- package/src/endpoint-generators/with-schema-and-openapi-action.ts +14 -0
- package/src/openapi/auth-provider-to-security-scheme.spec.ts +50 -0
- package/src/openapi/auth-provider-to-security-scheme.ts +41 -0
- package/src/openapi/generate-openapi-document.spec.ts +733 -0
- package/src/openapi/generate-openapi-document.ts +198 -0
- package/src/openapi/openapi-round-trip.advanced-api.json +363 -0
- package/src/openapi/openapi-round-trip.crud-api.json +115 -0
- package/src/openapi/openapi-round-trip.example-api.json +71 -0
- package/src/openapi/openapi-round-trip.spec.ts +621 -0
- package/src/swagger/generate-swagger-json.spec.ts +1 -1
- package/src/validate.integration.spec.ts +184 -33
- package/esm/endpoint-generators/create-get-swagger-json-action.d.ts +0 -14
- package/esm/endpoint-generators/create-get-swagger-json-action.d.ts.map +0 -1
- package/esm/endpoint-generators/create-get-swagger-json-action.js +0 -18
- package/esm/endpoint-generators/create-get-swagger-json-action.js.map +0 -1
- package/esm/endpoint-generators/with-schema-and-swagger-action.d.ts +0 -21
- package/esm/endpoint-generators/with-schema-and-swagger-action.d.ts.map +0 -1
- package/esm/endpoint-generators/with-schema-and-swagger-action.js +0 -2
- package/esm/endpoint-generators/with-schema-and-swagger-action.js.map +0 -1
- package/esm/swagger/generate-swagger-json.d.ts +0 -14
- package/esm/swagger/generate-swagger-json.d.ts.map +0 -1
- package/esm/swagger/generate-swagger-json.js +0 -108
- package/esm/swagger/generate-swagger-json.js.map +0 -1
- package/src/endpoint-generators/create-get-swagger-json-action.ts +0 -26
- package/src/endpoint-generators/with-schema-and-swagger-action.ts +0 -11
- package/src/swagger/generate-swagger-json.ts +0 -132
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateOpenApiDocument } from './generate-openapi-document.js';
|
|
3
|
+
describe('generateOpenApiDocument', () => {
|
|
4
|
+
describe('Document structure', () => {
|
|
5
|
+
it('Should generate a valid OpenAPI 3.1 document', () => {
|
|
6
|
+
const api = {
|
|
7
|
+
GET: {
|
|
8
|
+
'/test': { path: '/test', isAuthenticated: false, schemaName: 'Test', schema: { type: 'object' } },
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const result = generateOpenApiDocument({ api });
|
|
12
|
+
expect(result.openapi).toBe('3.1.0');
|
|
13
|
+
expect(result.info.title).toBe('FuryStack API');
|
|
14
|
+
expect(result.info.version).toBe('1.0.0');
|
|
15
|
+
expect(result.info.description).toBe('API documentation generated from FuryStack API schema');
|
|
16
|
+
expect(result.jsonSchemaDialect).toBe('https://spec.openapis.org/oas/3.1/dialect/base');
|
|
17
|
+
expect(result.servers).toEqual([{ url: '/' }]);
|
|
18
|
+
expect(result.tags).toEqual([]);
|
|
19
|
+
expect(result.paths).toBeDefined();
|
|
20
|
+
expect(result.components).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
it('Should use custom title, description, and version', () => {
|
|
23
|
+
const result = generateOpenApiDocument({
|
|
24
|
+
api: {},
|
|
25
|
+
title: 'Custom API',
|
|
26
|
+
description: 'Custom description',
|
|
27
|
+
version: '3.0.0',
|
|
28
|
+
});
|
|
29
|
+
expect(result.info.title).toBe('Custom API');
|
|
30
|
+
expect(result.info.description).toBe('Custom description');
|
|
31
|
+
expect(result.info.version).toBe('3.0.0');
|
|
32
|
+
});
|
|
33
|
+
it('Should include cookieAuth security scheme', () => {
|
|
34
|
+
const result = generateOpenApiDocument({ api: {} });
|
|
35
|
+
expect(result.components?.securitySchemes?.cookieAuth).toEqual({
|
|
36
|
+
type: 'apiKey',
|
|
37
|
+
in: 'cookie',
|
|
38
|
+
name: 'session',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('HTTP methods', () => {
|
|
43
|
+
it('Should generate GET operations', () => {
|
|
44
|
+
const api = {
|
|
45
|
+
GET: { '/items': { path: '/items', isAuthenticated: false, schemaName: 'Items', schema: { type: 'array' } } },
|
|
46
|
+
};
|
|
47
|
+
const result = generateOpenApiDocument({ api });
|
|
48
|
+
expect(result.paths?.['/items']?.get).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
it('Should generate POST operations', () => {
|
|
51
|
+
const api = {
|
|
52
|
+
POST: { '/items': { path: '/items', isAuthenticated: false, schemaName: 'Item', schema: { type: 'object' } } },
|
|
53
|
+
};
|
|
54
|
+
const result = generateOpenApiDocument({ api });
|
|
55
|
+
expect(result.paths?.['/items']?.post).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
it('Should generate PUT operations', () => {
|
|
58
|
+
const api = {
|
|
59
|
+
PUT: {
|
|
60
|
+
'/items/:id': {
|
|
61
|
+
path: '/items/:id',
|
|
62
|
+
isAuthenticated: false,
|
|
63
|
+
schemaName: 'Item',
|
|
64
|
+
schema: { type: 'object' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const result = generateOpenApiDocument({ api });
|
|
69
|
+
expect(result.paths?.['/items/{id}']?.put).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
it('Should generate DELETE operations', () => {
|
|
72
|
+
const api = {
|
|
73
|
+
DELETE: {
|
|
74
|
+
'/items/:id': {
|
|
75
|
+
path: '/items/:id',
|
|
76
|
+
isAuthenticated: false,
|
|
77
|
+
schemaName: 'Item',
|
|
78
|
+
schema: { type: 'object' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const result = generateOpenApiDocument({ api });
|
|
83
|
+
expect(result.paths?.['/items/{id}']?.delete).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
it('Should generate PATCH operations', () => {
|
|
86
|
+
const api = {
|
|
87
|
+
PATCH: {
|
|
88
|
+
'/items/:id': {
|
|
89
|
+
path: '/items/:id',
|
|
90
|
+
isAuthenticated: false,
|
|
91
|
+
schemaName: 'Item',
|
|
92
|
+
schema: { type: 'object' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const result = generateOpenApiDocument({ api });
|
|
97
|
+
expect(result.paths?.['/items/{id}']?.patch).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
it('Should generate HEAD operations', () => {
|
|
100
|
+
const api = {
|
|
101
|
+
HEAD: { '/items': { path: '/items', isAuthenticated: false, schemaName: 'Items', schema: { type: 'object' } } },
|
|
102
|
+
};
|
|
103
|
+
const result = generateOpenApiDocument({ api });
|
|
104
|
+
expect(result.paths?.['/items']?.head).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
it('Should generate OPTIONS operations', () => {
|
|
107
|
+
const api = {
|
|
108
|
+
OPTIONS: {
|
|
109
|
+
'/items': { path: '/items', isAuthenticated: false, schemaName: 'Items', schema: { type: 'object' } },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const result = generateOpenApiDocument({ api });
|
|
113
|
+
expect(result.paths?.['/items']?.options).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
it('Should generate TRACE operations', () => {
|
|
116
|
+
const api = {
|
|
117
|
+
TRACE: {
|
|
118
|
+
'/items': { path: '/items', isAuthenticated: false, schemaName: 'Items', schema: { type: 'object' } },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const result = generateOpenApiDocument({ api });
|
|
122
|
+
expect(result.paths?.['/items']?.trace).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
it('Should handle multiple methods on the same path', () => {
|
|
125
|
+
const api = {
|
|
126
|
+
GET: {
|
|
127
|
+
'/resource': {
|
|
128
|
+
path: '/resource',
|
|
129
|
+
isAuthenticated: false,
|
|
130
|
+
schemaName: 'ResourceGet',
|
|
131
|
+
schema: { type: 'object' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
POST: {
|
|
135
|
+
'/resource': {
|
|
136
|
+
path: '/resource',
|
|
137
|
+
isAuthenticated: true,
|
|
138
|
+
schemaName: 'ResourcePost',
|
|
139
|
+
schema: { type: 'object' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const result = generateOpenApiDocument({ api });
|
|
144
|
+
expect(result.paths?.['/resource']?.get).toBeDefined();
|
|
145
|
+
expect(result.paths?.['/resource']?.post).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('Path parameters', () => {
|
|
149
|
+
it('Should convert :param to {param} in paths', () => {
|
|
150
|
+
const api = {
|
|
151
|
+
GET: {
|
|
152
|
+
'/users/:id': {
|
|
153
|
+
path: '/users/:id',
|
|
154
|
+
isAuthenticated: false,
|
|
155
|
+
schemaName: 'User',
|
|
156
|
+
schema: { type: 'object' },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const result = generateOpenApiDocument({ api });
|
|
161
|
+
expect(result.paths?.['/users/{id}']).toBeDefined();
|
|
162
|
+
expect(result.paths?.['/users/:id']).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
it('Should generate path parameter objects', () => {
|
|
165
|
+
const api = {
|
|
166
|
+
GET: {
|
|
167
|
+
'/users/:id': {
|
|
168
|
+
path: '/users/:id',
|
|
169
|
+
isAuthenticated: false,
|
|
170
|
+
schemaName: 'User',
|
|
171
|
+
schema: { type: 'object' },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
const result = generateOpenApiDocument({ api });
|
|
176
|
+
const params = result.paths?.['/users/{id}']?.get?.parameters;
|
|
177
|
+
expect(params).toHaveLength(1);
|
|
178
|
+
expect(params[0]).toEqual({
|
|
179
|
+
name: 'id',
|
|
180
|
+
in: 'path',
|
|
181
|
+
required: true,
|
|
182
|
+
description: 'Path parameter: id',
|
|
183
|
+
schema: { type: 'string' },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
it('Should generate multiple path parameters', () => {
|
|
187
|
+
const api = {
|
|
188
|
+
GET: {
|
|
189
|
+
'/users/:userId/posts/:postId': {
|
|
190
|
+
path: '/users/:userId/posts/:postId',
|
|
191
|
+
isAuthenticated: false,
|
|
192
|
+
schemaName: 'Post',
|
|
193
|
+
schema: { type: 'object' },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const result = generateOpenApiDocument({ api });
|
|
198
|
+
const params = result.paths?.['/users/{userId}/posts/{postId}']?.get?.parameters;
|
|
199
|
+
expect(params).toHaveLength(2);
|
|
200
|
+
expect(params[0].name).toBe('userId');
|
|
201
|
+
expect(params[1].name).toBe('postId');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('Response schemas', () => {
|
|
205
|
+
it('Should reference schema via $ref when schemaName is set', () => {
|
|
206
|
+
const api = {
|
|
207
|
+
GET: {
|
|
208
|
+
'/test': {
|
|
209
|
+
path: '/test',
|
|
210
|
+
isAuthenticated: false,
|
|
211
|
+
schemaName: 'TestModel',
|
|
212
|
+
schema: { type: 'object', properties: { id: { type: 'string' } } },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
const result = generateOpenApiDocument({ api });
|
|
217
|
+
const response = result.paths?.['/test']?.get?.responses?.['200'];
|
|
218
|
+
expect((response.content?.['application/json']?.schema).$ref).toBe('#/components/schemas/TestModel');
|
|
219
|
+
});
|
|
220
|
+
it('Should include schemas in components', () => {
|
|
221
|
+
const testSchema = { type: 'object', properties: { id: { type: 'string' } } };
|
|
222
|
+
const api = {
|
|
223
|
+
GET: {
|
|
224
|
+
'/test': { path: '/test', isAuthenticated: false, schemaName: 'TestModel', schema: testSchema },
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const result = generateOpenApiDocument({ api });
|
|
228
|
+
expect(result.components?.schemas?.TestModel).toEqual(testSchema);
|
|
229
|
+
});
|
|
230
|
+
it('Should use generic object schema when no schemaName', () => {
|
|
231
|
+
const api = {
|
|
232
|
+
GET: {
|
|
233
|
+
'/test': { path: '/test', isAuthenticated: false, schemaName: '', schema: null },
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const result = generateOpenApiDocument({ api });
|
|
237
|
+
const response = result.paths?.['/test']?.get?.responses?.['200'];
|
|
238
|
+
expect(response.content?.['application/json']?.schema).toEqual({ type: 'object' });
|
|
239
|
+
});
|
|
240
|
+
it('Should include 401 and 500 error responses', () => {
|
|
241
|
+
const api = {
|
|
242
|
+
GET: {
|
|
243
|
+
'/test': { path: '/test', isAuthenticated: false, schemaName: 'Test', schema: { type: 'object' } },
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const result = generateOpenApiDocument({ api });
|
|
247
|
+
const responses = result.paths?.['/test']?.get?.responses;
|
|
248
|
+
expect(responses['401'].description).toBe('Unauthorized');
|
|
249
|
+
expect(responses['500'].description).toBe('Internal server error');
|
|
250
|
+
});
|
|
251
|
+
it('Should not add schema to components when schema is falsy', () => {
|
|
252
|
+
const api = {
|
|
253
|
+
GET: {
|
|
254
|
+
'/test': { path: '/test', isAuthenticated: false, schemaName: 'Empty', schema: null },
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
const result = generateOpenApiDocument({ api });
|
|
258
|
+
expect(result.components?.schemas?.Empty).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
describe('Request body extraction', () => {
|
|
262
|
+
it('Should include request body from schema definitions', () => {
|
|
263
|
+
const bodySchema = { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] };
|
|
264
|
+
const api = {
|
|
265
|
+
POST: {
|
|
266
|
+
'/users': {
|
|
267
|
+
path: '/users',
|
|
268
|
+
isAuthenticated: false,
|
|
269
|
+
schemaName: 'CreateUser',
|
|
270
|
+
schema: {
|
|
271
|
+
definitions: {
|
|
272
|
+
CreateUser: {
|
|
273
|
+
type: 'object',
|
|
274
|
+
properties: { body: bodySchema, result: { type: 'object' } },
|
|
275
|
+
required: ['body', 'result'],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
const result = generateOpenApiDocument({ api });
|
|
283
|
+
const requestBody = result.paths?.['/users']?.post?.requestBody;
|
|
284
|
+
expect(requestBody).toBeDefined();
|
|
285
|
+
expect(requestBody.required).toBe(true);
|
|
286
|
+
expect(requestBody.content['application/json'].schema).toEqual(bodySchema);
|
|
287
|
+
});
|
|
288
|
+
it('Should mark request body as not required when body is optional in schema', () => {
|
|
289
|
+
const api = {
|
|
290
|
+
POST: {
|
|
291
|
+
'/items': {
|
|
292
|
+
path: '/items',
|
|
293
|
+
isAuthenticated: false,
|
|
294
|
+
schemaName: 'CreateItem',
|
|
295
|
+
schema: {
|
|
296
|
+
definitions: {
|
|
297
|
+
CreateItem: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: { body: { type: 'object' }, result: { type: 'object' } },
|
|
300
|
+
required: ['result'],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
const result = generateOpenApiDocument({ api });
|
|
308
|
+
const requestBody = result.paths?.['/items']?.post?.requestBody;
|
|
309
|
+
expect(requestBody.required).toBe(false);
|
|
310
|
+
});
|
|
311
|
+
it('Should not add request body when schema has no body property', () => {
|
|
312
|
+
const api = {
|
|
313
|
+
GET: {
|
|
314
|
+
'/items': {
|
|
315
|
+
path: '/items',
|
|
316
|
+
isAuthenticated: false,
|
|
317
|
+
schemaName: 'ListItems',
|
|
318
|
+
schema: {
|
|
319
|
+
definitions: {
|
|
320
|
+
ListItems: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: { result: { type: 'array' } },
|
|
323
|
+
required: ['result'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
const result = generateOpenApiDocument({ api });
|
|
331
|
+
expect(result.paths?.['/items']?.get?.requestBody).toBeUndefined();
|
|
332
|
+
});
|
|
333
|
+
it('Should not add request body when schema has no definitions', () => {
|
|
334
|
+
const api = {
|
|
335
|
+
POST: {
|
|
336
|
+
'/items': { path: '/items', isAuthenticated: false, schemaName: 'Item', schema: { type: 'object' } },
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
const result = generateOpenApiDocument({ api });
|
|
340
|
+
expect(result.paths?.['/items']?.post?.requestBody).toBeUndefined();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
describe('Query parameter extraction', () => {
|
|
344
|
+
it('Should include query parameters from schema definitions', () => {
|
|
345
|
+
const api = {
|
|
346
|
+
GET: {
|
|
347
|
+
'/search': {
|
|
348
|
+
path: '/search',
|
|
349
|
+
isAuthenticated: false,
|
|
350
|
+
schemaName: 'Search',
|
|
351
|
+
schema: {
|
|
352
|
+
definitions: {
|
|
353
|
+
Search: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {
|
|
356
|
+
query: {
|
|
357
|
+
type: 'object',
|
|
358
|
+
properties: { q: { type: 'string' }, limit: { type: 'number' } },
|
|
359
|
+
required: ['q'],
|
|
360
|
+
},
|
|
361
|
+
result: { type: 'array' },
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
const result = generateOpenApiDocument({ api });
|
|
370
|
+
const params = result.paths?.['/search']?.get?.parameters;
|
|
371
|
+
const queryParams = params.filter((p) => p.in === 'query');
|
|
372
|
+
expect(queryParams).toHaveLength(2);
|
|
373
|
+
expect(queryParams.find((p) => p.name === 'q')?.required).toBe(true);
|
|
374
|
+
expect(queryParams.find((p) => p.name === 'q')?.schema).toEqual({ type: 'string' });
|
|
375
|
+
expect(queryParams.find((p) => p.name === 'limit')?.required).toBe(false);
|
|
376
|
+
expect(queryParams.find((p) => p.name === 'limit')?.schema).toEqual({ type: 'number' });
|
|
377
|
+
});
|
|
378
|
+
it('Should not add query parameters when query has no properties', () => {
|
|
379
|
+
const api = {
|
|
380
|
+
GET: {
|
|
381
|
+
'/items': {
|
|
382
|
+
path: '/items',
|
|
383
|
+
isAuthenticated: false,
|
|
384
|
+
schemaName: 'Items',
|
|
385
|
+
schema: {
|
|
386
|
+
definitions: {
|
|
387
|
+
Items: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: { query: { type: 'object' }, result: { type: 'array' } },
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
const result = generateOpenApiDocument({ api });
|
|
397
|
+
const params = result.paths?.['/items']?.get?.parameters;
|
|
398
|
+
expect(params.filter((p) => p.in === 'query')).toHaveLength(0);
|
|
399
|
+
});
|
|
400
|
+
it('Should mark query params as not required when required array is missing', () => {
|
|
401
|
+
const api = {
|
|
402
|
+
GET: {
|
|
403
|
+
'/search': {
|
|
404
|
+
path: '/search',
|
|
405
|
+
isAuthenticated: false,
|
|
406
|
+
schemaName: 'Search',
|
|
407
|
+
schema: {
|
|
408
|
+
definitions: {
|
|
409
|
+
Search: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {
|
|
412
|
+
query: {
|
|
413
|
+
type: 'object',
|
|
414
|
+
properties: { q: { type: 'string' } },
|
|
415
|
+
},
|
|
416
|
+
result: { type: 'array' },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
const result = generateOpenApiDocument({ api });
|
|
425
|
+
const params = result.paths?.['/search']?.get?.parameters;
|
|
426
|
+
const qParam = params.find((p) => p.in === 'query' && p.name === 'q');
|
|
427
|
+
expect(qParam?.required).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
describe('Header parameter extraction', () => {
|
|
431
|
+
it('Should include header parameters from schema definitions', () => {
|
|
432
|
+
const api = {
|
|
433
|
+
GET: {
|
|
434
|
+
'/data': {
|
|
435
|
+
path: '/data',
|
|
436
|
+
isAuthenticated: false,
|
|
437
|
+
schemaName: 'GetData',
|
|
438
|
+
schema: {
|
|
439
|
+
definitions: {
|
|
440
|
+
GetData: {
|
|
441
|
+
type: 'object',
|
|
442
|
+
properties: {
|
|
443
|
+
headers: {
|
|
444
|
+
type: 'object',
|
|
445
|
+
properties: { 'x-api-key': { type: 'string' } },
|
|
446
|
+
required: ['x-api-key'],
|
|
447
|
+
},
|
|
448
|
+
result: { type: 'object' },
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
const result = generateOpenApiDocument({ api });
|
|
457
|
+
const params = result.paths?.['/data']?.get?.parameters;
|
|
458
|
+
const headerParams = params.filter((p) => p.in === 'header');
|
|
459
|
+
expect(headerParams).toHaveLength(1);
|
|
460
|
+
expect(headerParams[0].name).toBe('x-api-key');
|
|
461
|
+
expect(headerParams[0].required).toBe(true);
|
|
462
|
+
expect(headerParams[0].schema).toEqual({ type: 'string' });
|
|
463
|
+
});
|
|
464
|
+
it('Should not add header parameters when headers has no properties', () => {
|
|
465
|
+
const api = {
|
|
466
|
+
GET: {
|
|
467
|
+
'/data': {
|
|
468
|
+
path: '/data',
|
|
469
|
+
isAuthenticated: false,
|
|
470
|
+
schemaName: 'GetData',
|
|
471
|
+
schema: {
|
|
472
|
+
definitions: {
|
|
473
|
+
GetData: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: { headers: { type: 'object' }, result: { type: 'object' } },
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
const result = generateOpenApiDocument({ api });
|
|
483
|
+
const params = result.paths?.['/data']?.get?.parameters;
|
|
484
|
+
expect(params.filter((p) => p.in === 'header')).toHaveLength(0);
|
|
485
|
+
});
|
|
486
|
+
it('Should mark header params as not required when required array is missing', () => {
|
|
487
|
+
const api = {
|
|
488
|
+
GET: {
|
|
489
|
+
'/data': {
|
|
490
|
+
path: '/data',
|
|
491
|
+
isAuthenticated: false,
|
|
492
|
+
schemaName: 'GetData',
|
|
493
|
+
schema: {
|
|
494
|
+
definitions: {
|
|
495
|
+
GetData: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
headers: {
|
|
499
|
+
type: 'object',
|
|
500
|
+
properties: { 'x-token': { type: 'string' } },
|
|
501
|
+
},
|
|
502
|
+
result: { type: 'object' },
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
const result = generateOpenApiDocument({ api });
|
|
511
|
+
const params = result.paths?.['/data']?.get?.parameters;
|
|
512
|
+
const headerParam = params.find((p) => p.in === 'header' && p.name === 'x-token');
|
|
513
|
+
expect(headerParam?.required).toBe(false);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
describe('Security', () => {
|
|
517
|
+
it('Should add cookieAuth for authenticated endpoints', () => {
|
|
518
|
+
const api = {
|
|
519
|
+
GET: {
|
|
520
|
+
'/private': { path: '/private', isAuthenticated: true, schemaName: 'P', schema: { type: 'object' } },
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
const result = generateOpenApiDocument({ api });
|
|
524
|
+
expect(result.paths?.['/private']?.get?.security).toEqual([{ cookieAuth: [] }]);
|
|
525
|
+
});
|
|
526
|
+
it('Should use empty security for non-authenticated endpoints', () => {
|
|
527
|
+
const api = {
|
|
528
|
+
GET: {
|
|
529
|
+
'/public': { path: '/public', isAuthenticated: false, schemaName: 'P', schema: { type: 'object' } },
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
const result = generateOpenApiDocument({ api });
|
|
533
|
+
expect(result.paths?.['/public']?.get?.security).toEqual([]);
|
|
534
|
+
});
|
|
535
|
+
it('Should use stored securitySchemes names when available', () => {
|
|
536
|
+
const api = {
|
|
537
|
+
GET: {
|
|
538
|
+
'/private': {
|
|
539
|
+
path: '/private',
|
|
540
|
+
isAuthenticated: true,
|
|
541
|
+
schemaName: 'P',
|
|
542
|
+
schema: { type: 'object' },
|
|
543
|
+
securitySchemes: ['bearerAuth'],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
const result = generateOpenApiDocument({ api });
|
|
548
|
+
expect(result.paths?.['/private']?.get?.security).toEqual([{ bearerAuth: [] }]);
|
|
549
|
+
});
|
|
550
|
+
it('Should use multiple stored securitySchemes names', () => {
|
|
551
|
+
const api = {
|
|
552
|
+
GET: {
|
|
553
|
+
'/private': {
|
|
554
|
+
path: '/private',
|
|
555
|
+
isAuthenticated: true,
|
|
556
|
+
schemaName: 'P',
|
|
557
|
+
schema: { type: 'object' },
|
|
558
|
+
securitySchemes: ['bearerAuth', 'apiKey'],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
const result = generateOpenApiDocument({ api });
|
|
563
|
+
expect(result.paths?.['/private']?.get?.security).toEqual([{ bearerAuth: [] }, { apiKey: [] }]);
|
|
564
|
+
});
|
|
565
|
+
it('Should use metadata securitySchemes keys as default when isAuthenticated is true', () => {
|
|
566
|
+
const api = {
|
|
567
|
+
GET: {
|
|
568
|
+
'/private': { path: '/private', isAuthenticated: true, schemaName: 'P', schema: { type: 'object' } },
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
const result = generateOpenApiDocument({
|
|
572
|
+
api,
|
|
573
|
+
metadata: {
|
|
574
|
+
securitySchemes: {
|
|
575
|
+
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
|
576
|
+
apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
expect(result.paths?.['/private']?.get?.security).toEqual([{ bearerAuth: [] }, { apiKey: [] }]);
|
|
581
|
+
});
|
|
582
|
+
it('Should prefer stored securitySchemes over metadata-derived defaults', () => {
|
|
583
|
+
const api = {
|
|
584
|
+
GET: {
|
|
585
|
+
'/private': {
|
|
586
|
+
path: '/private',
|
|
587
|
+
isAuthenticated: true,
|
|
588
|
+
schemaName: 'P',
|
|
589
|
+
schema: { type: 'object' },
|
|
590
|
+
securitySchemes: ['customScheme'],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
const result = generateOpenApiDocument({
|
|
595
|
+
api,
|
|
596
|
+
metadata: {
|
|
597
|
+
securitySchemes: {
|
|
598
|
+
bearerAuth: { type: 'http', scheme: 'bearer' },
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
expect(result.paths?.['/private']?.get?.security).toEqual([{ customScheme: [] }]);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
describe('Operation metadata', () => {
|
|
606
|
+
it('Should generate operationId from method and path', () => {
|
|
607
|
+
const api = {
|
|
608
|
+
GET: {
|
|
609
|
+
'/api/users/:id/profile': {
|
|
610
|
+
path: '/api/users/:id/profile',
|
|
611
|
+
isAuthenticated: false,
|
|
612
|
+
schemaName: 'P',
|
|
613
|
+
schema: { type: 'object' },
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
const result = generateOpenApiDocument({ api });
|
|
618
|
+
expect(result.paths?.['/api/users/{id}/profile']?.get?.operationId).toBe('get_api_users_id_profile');
|
|
619
|
+
});
|
|
620
|
+
it('Should gracefully ignore unsupported HTTP methods like CONNECT', () => {
|
|
621
|
+
const api = {
|
|
622
|
+
CONNECT: {
|
|
623
|
+
'/tunnel': { path: '/tunnel', isAuthenticated: false, schemaName: 'Tunnel', schema: { type: 'object' } },
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
const result = generateOpenApiDocument({ api });
|
|
627
|
+
expect(result.paths?.['/tunnel']).toBeDefined();
|
|
628
|
+
expect(result.paths?.['/tunnel']?.get).toBeUndefined();
|
|
629
|
+
expect(result.paths?.['/tunnel']?.post).toBeUndefined();
|
|
630
|
+
});
|
|
631
|
+
it('Should generate summary and description', () => {
|
|
632
|
+
const api = {
|
|
633
|
+
POST: {
|
|
634
|
+
'/users': { path: '/users', isAuthenticated: false, schemaName: 'U', schema: { type: 'object' } },
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
const result = generateOpenApiDocument({ api });
|
|
638
|
+
expect(result.paths?.['/users']?.post?.summary).toBe('POST /users');
|
|
639
|
+
expect(result.paths?.['/users']?.post?.description).toBe('Endpoint for /users');
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
//# sourceMappingURL=generate-openapi-document.spec.js.map
|