115 lines
3.6 KiB
TypeScript
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/);
|
|
});
|