Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions lib/services/tests/express.docs.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>` 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;
};
Comment on lines +63 to +75

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),
}));
Comment on lines +77 to +86
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).
*
Expand Down
15 changes: 12 additions & 3 deletions modules/core/doc/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>` header.
apiKeyAuth:
type: http
scheme: bearer
description: |
Legacy alias for `bearerAuth`. New integrations should use `bearerAuth`.
Comment on lines +12 to +16

schemas:
SuccessResponse:
type: object
description: Standard success envelope. data type varies by endpoint.
properties:
type:
type: string
Expand All @@ -23,6 +31,7 @@ components:

ErrorResponse:
type: object
description: Standard error envelope returned on 4xx/5xx.
properties:
type:
type: string
Expand Down
Loading