@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.
@@ -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
+ })
@@ -1,4 +1,4 @@
1
- import { TestContainer } from '@navios/di/testing'
1
+ import { TestContainer } from '@navios/core/testing'
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
4
4
  import { z } from 'zod/v4'