@navios/openapi 1.0.0-alpha.2 → 1.0.0-alpha.4
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 +23 -0
- package/README.md +1 -2
- package/dist/src/__tests__/endpoint-scanner.service.spec.d.mts +2 -0
- package/dist/src/__tests__/endpoint-scanner.service.spec.d.mts.map +1 -0
- package/dist/src/__tests__/openapi-generator.service.spec.d.mts +2 -0
- package/dist/src/__tests__/openapi-generator.service.spec.d.mts.map +1 -0
- package/dist/tsconfig.spec.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/{index-COmVnnSM.d.mts → index-Bzkj5ltS.d.cts} +9 -9
- package/lib/{index-Dz-4huer.d.cts.map → index-Bzkj5ltS.d.cts.map} +1 -1
- package/lib/{index-Dz-4huer.d.cts → index-CwN9u2YO.d.mts} +9 -9
- package/lib/{index-COmVnnSM.d.mts.map → index-CwN9u2YO.d.mts.map} +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.mts +1 -1
- package/lib/legacy-compat/index.d.cts +7 -7
- package/lib/legacy-compat/index.d.mts +7 -7
- package/package.json +4 -5
- package/src/__tests__/endpoint-scanner.service.spec.mts +370 -0
- package/src/__tests__/metadata.spec.mts +1 -1
- package/src/__tests__/openapi-generator.service.spec.mts +394 -0
- package/src/__tests__/services.spec.mts +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import type { ModuleMetadata } from '@navios/core'
|
|
2
|
+
|
|
3
|
+
import { Logger } from '@navios/core'
|
|
4
|
+
import { TestContainer } from '@navios/di/testing'
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import type { DiscoveredEndpoint } from '../services/endpoint-scanner.service.mjs'
|
|
9
|
+
import type { OpenApiGeneratorOptions } from '../services/openapi-generator.service.mjs'
|
|
10
|
+
|
|
11
|
+
import { EndpointScannerService } from '../services/endpoint-scanner.service.mjs'
|
|
12
|
+
import { OpenApiGeneratorService } from '../services/openapi-generator.service.mjs'
|
|
13
|
+
import { PathBuilderService } from '../services/path-builder.service.mjs'
|
|
14
|
+
|
|
15
|
+
// Mock logger
|
|
16
|
+
const mockLogger = {
|
|
17
|
+
debug: vi.fn(),
|
|
18
|
+
warn: vi.fn(),
|
|
19
|
+
error: vi.fn(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mock scanner
|
|
23
|
+
const mockScanner = {
|
|
24
|
+
scan: vi.fn().mockReturnValue([]),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Mock path builder
|
|
28
|
+
const mockPathBuilder = {
|
|
29
|
+
build: vi.fn().mockReturnValue({
|
|
30
|
+
path: '/test',
|
|
31
|
+
pathItem: { get: { responses: { 200: { description: 'OK' } } } },
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('OpenApiGeneratorService', () => {
|
|
36
|
+
let container: TestContainer
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
container = new TestContainer()
|
|
40
|
+
// Bind required dependencies
|
|
41
|
+
container.bind(Logger).toValue(mockLogger as any)
|
|
42
|
+
container.bind(EndpointScannerService).toValue(mockScanner as any)
|
|
43
|
+
container.bind(PathBuilderService).toValue(mockPathBuilder as any)
|
|
44
|
+
vi.clearAllMocks()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await container.dispose()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('generate', () => {
|
|
52
|
+
it('should generate basic OpenAPI document', async () => {
|
|
53
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
54
|
+
|
|
55
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
56
|
+
const options: OpenApiGeneratorOptions = {
|
|
57
|
+
info: {
|
|
58
|
+
title: 'Test API',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const document = generator.generate(modules, options)
|
|
64
|
+
|
|
65
|
+
expect(document.openapi).toBe('3.1.0')
|
|
66
|
+
expect(document.info.title).toBe('Test API')
|
|
67
|
+
expect(document.info.version).toBe('1.0.0')
|
|
68
|
+
expect(document.paths).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should include optional info fields', async () => {
|
|
72
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
73
|
+
|
|
74
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
75
|
+
const options: OpenApiGeneratorOptions = {
|
|
76
|
+
info: {
|
|
77
|
+
title: 'Test API',
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
description: 'A test API',
|
|
80
|
+
termsOfService: 'https://example.com/terms',
|
|
81
|
+
contact: {
|
|
82
|
+
name: 'Support',
|
|
83
|
+
email: 'support@example.com',
|
|
84
|
+
url: 'https://example.com',
|
|
85
|
+
},
|
|
86
|
+
license: {
|
|
87
|
+
name: 'MIT',
|
|
88
|
+
url: 'https://opensource.org/licenses/MIT',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const document = generator.generate(modules, options)
|
|
94
|
+
|
|
95
|
+
expect(document.info.description).toBe('A test API')
|
|
96
|
+
expect(document.info.termsOfService).toBe('https://example.com/terms')
|
|
97
|
+
expect(document.info.contact?.name).toBe('Support')
|
|
98
|
+
expect(document.info.license?.name).toBe('MIT')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should include servers when provided', async () => {
|
|
102
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
103
|
+
|
|
104
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
105
|
+
const options: OpenApiGeneratorOptions = {
|
|
106
|
+
info: {
|
|
107
|
+
title: 'Test API',
|
|
108
|
+
version: '1.0.0',
|
|
109
|
+
},
|
|
110
|
+
servers: [
|
|
111
|
+
{ url: 'https://api.example.com', description: 'Production' },
|
|
112
|
+
{ url: 'https://staging.example.com', description: 'Staging' },
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const document = generator.generate(modules, options)
|
|
117
|
+
|
|
118
|
+
expect(document.servers).toHaveLength(2)
|
|
119
|
+
expect(document.servers?.[0].url).toBe('https://api.example.com')
|
|
120
|
+
expect(document.servers?.[1].description).toBe('Staging')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should not include servers when empty array', async () => {
|
|
124
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
125
|
+
|
|
126
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
127
|
+
const options: OpenApiGeneratorOptions = {
|
|
128
|
+
info: {
|
|
129
|
+
title: 'Test API',
|
|
130
|
+
version: '1.0.0',
|
|
131
|
+
},
|
|
132
|
+
servers: [],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const document = generator.generate(modules, options)
|
|
136
|
+
|
|
137
|
+
expect(document.servers).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should include external docs when provided', async () => {
|
|
141
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
142
|
+
|
|
143
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
144
|
+
const options: OpenApiGeneratorOptions = {
|
|
145
|
+
info: {
|
|
146
|
+
title: 'Test API',
|
|
147
|
+
version: '1.0.0',
|
|
148
|
+
},
|
|
149
|
+
externalDocs: {
|
|
150
|
+
url: 'https://docs.example.com',
|
|
151
|
+
description: 'Full documentation',
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const document = generator.generate(modules, options)
|
|
156
|
+
|
|
157
|
+
expect(document.externalDocs?.url).toBe('https://docs.example.com')
|
|
158
|
+
expect(document.externalDocs?.description).toBe('Full documentation')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should include security schemes when provided', async () => {
|
|
162
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
163
|
+
|
|
164
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
165
|
+
const options: OpenApiGeneratorOptions = {
|
|
166
|
+
info: {
|
|
167
|
+
title: 'Test API',
|
|
168
|
+
version: '1.0.0',
|
|
169
|
+
},
|
|
170
|
+
securitySchemes: {
|
|
171
|
+
bearerAuth: {
|
|
172
|
+
type: 'http',
|
|
173
|
+
scheme: 'bearer',
|
|
174
|
+
bearerFormat: 'JWT',
|
|
175
|
+
},
|
|
176
|
+
apiKey: {
|
|
177
|
+
type: 'apiKey',
|
|
178
|
+
in: 'header',
|
|
179
|
+
name: 'X-API-Key',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const document = generator.generate(modules, options)
|
|
185
|
+
|
|
186
|
+
expect(document.components?.securitySchemes).toBeDefined()
|
|
187
|
+
const bearerAuth = document.components?.securitySchemes?.bearerAuth
|
|
188
|
+
const apiKey = document.components?.securitySchemes?.apiKey
|
|
189
|
+
expect(
|
|
190
|
+
bearerAuth && 'type' in bearerAuth ? bearerAuth.type : undefined,
|
|
191
|
+
).toBe('http')
|
|
192
|
+
expect(apiKey && 'type' in apiKey ? apiKey.type : undefined).toBe(
|
|
193
|
+
'apiKey',
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should include global security requirements', async () => {
|
|
198
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
199
|
+
|
|
200
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
201
|
+
const options: OpenApiGeneratorOptions = {
|
|
202
|
+
info: {
|
|
203
|
+
title: 'Test API',
|
|
204
|
+
version: '1.0.0',
|
|
205
|
+
},
|
|
206
|
+
security: [{ bearerAuth: [] }],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const document = generator.generate(modules, options)
|
|
210
|
+
|
|
211
|
+
expect(document.security).toHaveLength(1)
|
|
212
|
+
expect(document.security?.[0]).toEqual({ bearerAuth: [] })
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should build paths from discovered endpoints', async () => {
|
|
216
|
+
const mockEndpoints: DiscoveredEndpoint[] = [
|
|
217
|
+
{
|
|
218
|
+
module: {} as any,
|
|
219
|
+
controllerClass: class {},
|
|
220
|
+
controller: {} as any,
|
|
221
|
+
handler: { config: { method: 'GET', url: '/users' } } as any,
|
|
222
|
+
config: { method: 'GET', url: '/users' },
|
|
223
|
+
openApiMetadata: {
|
|
224
|
+
tags: ['users'],
|
|
225
|
+
summary: '',
|
|
226
|
+
description: '',
|
|
227
|
+
operationId: '',
|
|
228
|
+
deprecated: false,
|
|
229
|
+
excluded: false,
|
|
230
|
+
security: [],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
mockScanner.scan.mockReturnValue(mockEndpoints)
|
|
236
|
+
mockPathBuilder.build.mockReturnValue({
|
|
237
|
+
path: '/users',
|
|
238
|
+
pathItem: {
|
|
239
|
+
get: { tags: ['users'], responses: { 200: { description: 'OK' } } },
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
244
|
+
|
|
245
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
246
|
+
const options: OpenApiGeneratorOptions = {
|
|
247
|
+
info: {
|
|
248
|
+
title: 'Test API',
|
|
249
|
+
version: '1.0.0',
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const document = generator.generate(modules, options)
|
|
254
|
+
|
|
255
|
+
expect(mockScanner.scan).toHaveBeenCalledWith(modules)
|
|
256
|
+
expect(mockPathBuilder.build).toHaveBeenCalledWith(mockEndpoints[0])
|
|
257
|
+
expect(document.paths?.['/users']).toBeDefined()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should merge paths with different methods', async () => {
|
|
261
|
+
const mockEndpoints: DiscoveredEndpoint[] = [
|
|
262
|
+
{
|
|
263
|
+
module: {} as any,
|
|
264
|
+
controllerClass: class {},
|
|
265
|
+
controller: {} as any,
|
|
266
|
+
handler: { config: { method: 'GET', url: '/users' } } as any,
|
|
267
|
+
config: { method: 'GET', url: '/users' },
|
|
268
|
+
openApiMetadata: { tags: ['users'], excluded: false } as any,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
module: {} as any,
|
|
272
|
+
controllerClass: class {},
|
|
273
|
+
controller: {} as any,
|
|
274
|
+
handler: { config: { method: 'POST', url: '/users' } } as any,
|
|
275
|
+
config: { method: 'POST', url: '/users' },
|
|
276
|
+
openApiMetadata: { tags: ['users'], excluded: false } as any,
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
mockScanner.scan.mockReturnValue(mockEndpoints)
|
|
281
|
+
mockPathBuilder.build
|
|
282
|
+
.mockReturnValueOnce({
|
|
283
|
+
path: '/users',
|
|
284
|
+
pathItem: { get: { responses: { 200: { description: 'OK' } } } },
|
|
285
|
+
})
|
|
286
|
+
.mockReturnValueOnce({
|
|
287
|
+
path: '/users',
|
|
288
|
+
pathItem: {
|
|
289
|
+
post: { responses: { 201: { description: 'Created' } } },
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
294
|
+
|
|
295
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
296
|
+
const options: OpenApiGeneratorOptions = {
|
|
297
|
+
info: {
|
|
298
|
+
title: 'Test API',
|
|
299
|
+
version: '1.0.0',
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const document = generator.generate(modules, options)
|
|
304
|
+
|
|
305
|
+
expect(document.paths?.['/users']?.get).toBeDefined()
|
|
306
|
+
expect(document.paths?.['/users']?.post).toBeDefined()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should collect and merge tags', async () => {
|
|
310
|
+
const mockEndpoints: DiscoveredEndpoint[] = [
|
|
311
|
+
{
|
|
312
|
+
module: {} as any,
|
|
313
|
+
controllerClass: class {},
|
|
314
|
+
controller: {} as any,
|
|
315
|
+
handler: {} as any,
|
|
316
|
+
config: {} as any,
|
|
317
|
+
openApiMetadata: { tags: ['users', 'api'], excluded: false } as any,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
module: {} as any,
|
|
321
|
+
controllerClass: class {},
|
|
322
|
+
controller: {} as any,
|
|
323
|
+
handler: {} as any,
|
|
324
|
+
config: {} as any,
|
|
325
|
+
openApiMetadata: { tags: ['orders'], excluded: false } as any,
|
|
326
|
+
},
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
mockScanner.scan.mockReturnValue(mockEndpoints)
|
|
330
|
+
|
|
331
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
332
|
+
|
|
333
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
334
|
+
const options: OpenApiGeneratorOptions = {
|
|
335
|
+
info: {
|
|
336
|
+
title: 'Test API',
|
|
337
|
+
version: '1.0.0',
|
|
338
|
+
},
|
|
339
|
+
tags: [{ name: 'users', description: 'User operations' }],
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const document = generator.generate(modules, options)
|
|
343
|
+
|
|
344
|
+
expect(document.tags).toBeDefined()
|
|
345
|
+
expect(document.tags).toContainEqual({
|
|
346
|
+
name: 'users',
|
|
347
|
+
description: 'User operations',
|
|
348
|
+
})
|
|
349
|
+
expect(document.tags).toContainEqual({ name: 'api' })
|
|
350
|
+
expect(document.tags).toContainEqual({ name: 'orders' })
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('should not include tags when none discovered and none configured', async () => {
|
|
354
|
+
mockScanner.scan.mockReturnValue([])
|
|
355
|
+
|
|
356
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
357
|
+
|
|
358
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
359
|
+
const options: OpenApiGeneratorOptions = {
|
|
360
|
+
info: {
|
|
361
|
+
title: 'Test API',
|
|
362
|
+
version: '1.0.0',
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const document = generator.generate(modules, options)
|
|
367
|
+
|
|
368
|
+
expect(document.tags).toBeUndefined()
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('should log generation info', async () => {
|
|
372
|
+
mockScanner.scan.mockReturnValue([])
|
|
373
|
+
|
|
374
|
+
const generator = await container.get(OpenApiGeneratorService)
|
|
375
|
+
|
|
376
|
+
const modules = new Map<string, ModuleMetadata>()
|
|
377
|
+
const options: OpenApiGeneratorOptions = {
|
|
378
|
+
info: {
|
|
379
|
+
title: 'Test API',
|
|
380
|
+
version: '1.0.0',
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
generator.generate(modules, options)
|
|
385
|
+
|
|
386
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
387
|
+
'Generating OpenAPI document',
|
|
388
|
+
)
|
|
389
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
390
|
+
expect.stringContaining('Generated OpenAPI document'),
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
})
|