diff --git a/lib/services/tests/express.docs.unit.tests.js b/lib/services/tests/express.docs.unit.tests.js index e99651c66..7b35b3653 100644 --- a/lib/services/tests/express.docs.unit.tests.js +++ b/lib/services/tests/express.docs.unit.tests.js @@ -3,6 +3,155 @@ */ import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals'; +/** + * Unit tests for core/doc/index.yml OpenAPI baseline — bearerAuth description, + * apiKeyAuth alias, SuccessResponse + ErrorResponse schema descriptions. + * (T13 Fix B — infra#38) + */ +describe('express initSwagger — core/doc/index.yml OpenAPI baseline (T13 Fix B):', () => { + const mockCoreYamlDoc = { + openapi: '3.0.0', + info: { version: '1.0.0' }, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Authenticate by sending an HTTP `Authorization: Bearer ` header.', + }, + apiKeyAuth: { + type: 'http', + scheme: 'bearer', + description: 'Legacy alias for `bearerAuth`. New integrations should use `bearerAuth`.', + }, + }, + schemas: { + SuccessResponse: { + type: 'object', + description: 'Standard success envelope. data type varies by endpoint.', + properties: { + type: { type: 'string', enum: ['success'] }, + message: { type: 'string' }, + data: { description: 'Response payload (type varies by endpoint)' }, + }, + }, + ErrorResponse: { + type: 'object', + description: 'Standard error envelope returned on 4xx/5xx.', + properties: { + type: { type: 'string', enum: ['error'] }, + message: { type: 'string' }, + code: { type: 'integer' }, + status: { type: 'integer' }, + errorCode: { type: 'string' }, + description: { type: 'string' }, + error: { type: 'string', description: 'JSON-stringified error details included only in non-production environments.' }, + }, + }, + }, + }, + }; + + const baseConfig = { + swagger: { enable: true }, + files: { swagger: ['/fake/core.yaml'], guides: [] }, + app: { title: 'Test API', description: 'Test', url: 'https://example.com' }, + domain: 'https://example.com', + }; + + const buildMockApp = () => { + const routes = {}; + return { get: (path, handler) => { routes[path] = handler; }, _routes: routes }; + }; + + const callSpecRoute = (app) => { + const handler = app._routes['/api/spec.json']; + if (!handler) throw new Error('/api/spec.json route not registered'); + let spec = null; + const res = { json: (body) => { spec = body; } }; + handler({}, res); + return spec; + }; + + beforeEach(() => { + jest.resetModules(); + jest.unstable_mockModule('fs', () => ({ + default: { readFileSync: jest.fn().mockReturnValue('mocked') }, + readFileSync: jest.fn().mockReturnValue('mocked'), + })); + jest.unstable_mockModule('js-yaml', () => ({ + default: { load: jest.fn().mockReturnValue(mockCoreYamlDoc) }, + load: jest.fn().mockReturnValue(mockCoreYamlDoc), + })); + jest.unstable_mockModule('../../helpers/guides.js', () => ({ + default: { loadGuides: jest.fn().mockReturnValue([]), mergeGuidesIntoSpec: jest.fn() }, + })); + jest.unstable_mockModule('../logger.js', () => ({ + default: { warn: jest.fn(), info: jest.fn(), error: jest.fn() }, + })); + }); + + afterEach(() => jest.restoreAllMocks()); + + test('bearerAuth security scheme exists in merged spec', async () => { + jest.unstable_mockModule('../../../config/index.js', () => ({ default: baseConfig })); + const { default: expressService } = await import('../express.js'); + const app = buildMockApp(); + expressService.initSwagger(app); + const spec = callSpecRoute(app); + expect(spec.components.securitySchemes.bearerAuth).toBeDefined(); + }); + + test('bearerAuth has a description field', async () => { + jest.unstable_mockModule('../../../config/index.js', () => ({ default: baseConfig })); + const { default: expressService } = await import('../express.js'); + const app = buildMockApp(); + expressService.initSwagger(app); + const spec = callSpecRoute(app); + expect(typeof spec.components.securitySchemes.bearerAuth.description).toBe('string'); + expect(spec.components.securitySchemes.bearerAuth.description.length).toBeGreaterThan(0); + }); + + test('apiKeyAuth security scheme exists as legacy alias', async () => { + jest.unstable_mockModule('../../../config/index.js', () => ({ default: baseConfig })); + const { default: expressService } = await import('../express.js'); + const app = buildMockApp(); + expressService.initSwagger(app); + const spec = callSpecRoute(app); + expect(spec.components.securitySchemes.apiKeyAuth).toBeDefined(); + }); + + test('apiKeyAuth description mentions bearerAuth as the canonical scheme', async () => { + jest.unstable_mockModule('../../../config/index.js', () => ({ default: baseConfig })); + const { default: expressService } = await import('../express.js'); + const app = buildMockApp(); + expressService.initSwagger(app); + const spec = callSpecRoute(app); + expect(spec.components.securitySchemes.apiKeyAuth.description).toMatch(/bearerAuth/i); + }); + + test('SuccessResponse schema has a description', async () => { + jest.unstable_mockModule('../../../config/index.js', () => ({ default: baseConfig })); + const { default: expressService } = await import('../express.js'); + const app = buildMockApp(); + expressService.initSwagger(app); + const spec = callSpecRoute(app); + expect(typeof spec.components.schemas.SuccessResponse.description).toBe('string'); + expect(spec.components.schemas.SuccessResponse.description.length).toBeGreaterThan(0); + }); + + test('ErrorResponse schema has a description', async () => { + jest.unstable_mockModule('../../../config/index.js', () => ({ default: baseConfig })); + const { default: expressService } = await import('../express.js'); + const app = buildMockApp(); + expressService.initSwagger(app); + const spec = callSpecRoute(app); + expect(typeof spec.components.schemas.ErrorResponse.description).toBe('string'); + expect(spec.components.schemas.ErrorResponse.description.length).toBeGreaterThan(0); + }); +}); + /** * Unit tests for express.js initSwagger — Redoc theme polish (issue #3686). * diff --git a/modules/core/doc/index.yml b/modules/core/doc/index.yml index 5219efff2..6a9b5787a 100644 --- a/modules/core/doc/index.yml +++ b/modules/core/doc/index.yml @@ -4,13 +4,21 @@ info: components: securitySchemes: bearerAuth: - type: "http" - scheme: "bearer" - bearerFormat: "JWT" + type: http + scheme: bearer + bearerFormat: JWT + description: | + Authenticate by sending an HTTP `Authorization: Bearer ` header. + apiKeyAuth: + type: http + scheme: bearer + description: | + Legacy alias for `bearerAuth`. New integrations should use `bearerAuth`. schemas: SuccessResponse: type: object + description: Standard success envelope. data type varies by endpoint. properties: type: type: string @@ -23,6 +31,7 @@ components: ErrorResponse: type: object + description: Standard error envelope returned on 4xx/5xx. properties: type: type: string