39948-vm/backend/tests/openapi-document.test.ts
2026-07-02 13:03:05 +02:00

115 lines
3.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import type { NextFunction } from 'express';
import { createRequest, createResponse } from 'node-mocks-http';
import * as swaggerUI from 'swagger-ui-express';
import { createOpenApiDocument } from '../src/openapi/document.ts';
import type { OpenApiDocument } from '../src/openapi/document.ts';
function createTestDocument(): OpenApiDocument {
return createOpenApiDocument({
serverUrl: 'http://localhost:3000',
});
}
function collectRefs(value: unknown): Set<string> {
const refs = new Set<string>();
const serialized = JSON.stringify(value);
const refPattern = /"\$ref"\s*:\s*"([^"]+)"/g;
for (const match of serialized.matchAll(refPattern)) {
const ref = match[1];
if (ref) {
refs.add(ref);
}
}
return refs;
}
function hasComponentRef(document: OpenApiDocument, ref: string): boolean {
const match = /^#\/components\/(schemas|responses|parameters|securitySchemes)\/(.+)$/.exec(ref);
if (!match) return false;
const section = match[1];
const name = match[2];
if (!section || !name) return false;
switch (section) {
case 'schemas':
return name in document.components.schemas;
case 'responses':
return name in document.components.responses;
case 'parameters':
return name in document.components.parameters;
case 'securitySchemes':
return name in document.components.securitySchemes;
default:
return false;
}
}
void test('OpenAPI document exposes comprehensive route coverage', () => {
const document = createTestDocument();
const requiredPaths = [
'/api/health',
'/api/auth/signin/local',
'/api/users',
'/api/users/count',
'/api/users/autocomplete',
'/api/projects/{id}/clone',
'/api/file/presign',
'/api/file/upload-sessions/{sessionId}/chunks/{chunkIndex}',
'/api/runtime-access/me',
'/api/project-ui-control-settings/project/{projectId}/env/{environment}',
'/api/tour_pages/reverse-video-status',
];
assert.equal(document.openapi, '3.0.0');
for (const path of requiredPaths) {
assert.ok(document.paths[path], `Missing OpenAPI path: ${path}`);
}
});
void test('OpenAPI document resolves all internal refs', () => {
const document = createTestDocument();
const refs = collectRefs(document);
const missingRefs = [...refs].filter((ref) => !hasComponentRef(document, ref));
assert.equal(missingRefs.length, 0, `Missing refs: ${missingRefs.join(', ')}`);
});
void test('OpenAPI factory CRUD paths are generated consistently', () => {
const document = createTestDocument();
const resourcePath = '/api/assets';
assert.ok(document.paths[resourcePath]?.post);
assert.ok(document.paths[resourcePath]?.get);
assert.ok(document.paths[`${resourcePath}/bulk-import`]?.post);
assert.ok(document.paths[`${resourcePath}/deleteByIds`]?.post);
assert.ok(document.paths[`${resourcePath}/count`]?.get);
assert.ok(document.paths[`${resourcePath}/autocomplete`]?.get);
assert.ok(document.paths[`${resourcePath}/{id}`]?.get);
assert.ok(document.paths[`${resourcePath}/{id}`]?.put);
assert.ok(document.paths[`${resourcePath}/{id}`]?.delete);
});
void test('Swagger UI setup serves documentation HTML without throwing', () => {
const document = createTestDocument();
const req = createRequest({ url: '/api-docs/' });
const res = createResponse();
const handler = swaggerUI.setup(document);
let nextError: unknown = null;
const next: NextFunction = (error?: unknown) => {
nextError = error;
};
handler(req, res, next);
assert.equal(nextError, null);
assert.equal(res.statusCode, 200);
assert.match(String(res._getData()), /swagger-ui/);
});