66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { readFile } from 'node:fs/promises';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import YAML from 'yaml';
|
|
import { buildApp } from '../../src/app.js';
|
|
import { loadConfig } from '../../src/config.js';
|
|
|
|
interface OpenApiDocument {
|
|
servers: Array<{ url: string }>;
|
|
paths: Record<string, unknown>;
|
|
}
|
|
|
|
function assertOpenApiDocument(value: unknown): asserts value is OpenApiDocument {
|
|
if (
|
|
typeof value !== 'object' ||
|
|
value === null ||
|
|
!('servers' in value) ||
|
|
!Array.isArray(value.servers) ||
|
|
!('paths' in value) ||
|
|
typeof value.paths !== 'object' ||
|
|
value.paths === null
|
|
) {
|
|
throw new Error('api/openapi.yaml is not an OpenAPI document');
|
|
}
|
|
}
|
|
|
|
const config = loadConfig({
|
|
DATABASE_URL: 'postgres://localhost/staff',
|
|
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
|
STANDALONE_ACTOR_ID: '01J00000000000000000000001',
|
|
LOG_LEVEL: 'silent',
|
|
NODE_ENV: 'test',
|
|
});
|
|
|
|
describe('OpenAPI runtime contract', () => {
|
|
it('exposes the documented operational paths under the module prefix', async () => {
|
|
const database = {
|
|
ping: vi.fn(async () => undefined),
|
|
close: vi.fn(async () => undefined),
|
|
} as never;
|
|
const app = await buildApp({ config, database });
|
|
await app.ready();
|
|
const documented: unknown = YAML.parse(await readFile('api/openapi.yaml', 'utf8'));
|
|
assertOpenApiDocument(documented);
|
|
const runtime = app.swagger();
|
|
const prefix = documented.servers[0]?.url;
|
|
if (!prefix) throw new Error('OpenAPI server prefix is missing');
|
|
const documentedPaths = Object.keys(documented.paths).sort();
|
|
const runtimePaths = Object.keys(runtime.paths ?? {})
|
|
.filter((path) => path !== '/' && path !== '/openapi.json' && !path.startsWith('/staff'))
|
|
.sort();
|
|
expect(runtimePaths).toEqual(documentedPaths);
|
|
for (const path of documentedPaths) {
|
|
const runtimePath = path
|
|
.replace('{employeeId}', '01J00000000000000000000000')
|
|
.replace('{workDate}', '2026-09-24')
|
|
.replace('{runId}', '01J00000000000000000000000');
|
|
const operations = documented.paths[path] as Record<string, unknown>;
|
|
const methods = ['get', 'post', 'put', 'patch', 'delete'] as const;
|
|
const method = methods.find((candidate) => candidate in operations);
|
|
if (!method) throw new Error(`No HTTP operation documented for ${path}`);
|
|
const response = await app.inject({ method, url: `${prefix}${runtimePath}` });
|
|
expect(response.statusCode).not.toBe(404);
|
|
}
|
|
await app.close();
|
|
});
|
|
});
|