krona-staff/tests/integration/openapi-runtime.test.ts

67 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: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
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')
.replace('{deviceId}', '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();
});
});