pediatric-ai-scribe-v3/test/openapi.test.js
Daniel 75f5486beb
Some checks failed
Forgejo Docker Build / Root app tests (push) Successful in 51s
Forgejo Docker Build / Build Docker image (push) Successful in 8s
Forgejo Docker Build / End-to-end (browser) (push) Failing after 7s
fix: an array of route paths is several operations, pages are none, and the prompt-pool build says what it did
app.get(['/', '/index.html', '/assistant']) reached the OpenAPI generator as
one route whose path was the array, joined with commas; the e2e reachability
check then probed "/,/index.html,/assistant" and found a 404. Each path is
now its own route, and routes outside /api/ are left out: the document
describes what a client calls, and a client does not call index.html.

The starter-question pool logs when a build starts, what each category kept,
and how long it took, so a build that produces nothing can be traced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 06:35:25 +02:00

126 lines
5.5 KiB
JavaScript

// docs/api-reference.md was hand-written, and by the time anyone checked it
// documented twenty-three endpoints that answer 404. Reference material written
// by hand is correct on the day it is written and silently wrong afterwards.
//
// So the document is generated from the Express router stack. These tests pin
// the two things that can break that: the shape of the stack (undocumented, and
// an Express upgrade could change it) and the rule that every route says what
// it is for.
const test = require('node:test');
const assert = require('node:assert/strict');
const express = require('express');
const openapi = require('../src/utils/openapi');
// A stand-in for the real app: mounted routers, a route straight on the app,
// plain middleware that is not a router, and a duplicate path across two
// routers mounted on the same prefix.
function syntheticApp() {
const app = express();
const users = express.Router();
users.get('/me', (req, res) => res.end());
users.post('/login', (req, res) => res.end());
users.get('/thing/:id', (req, res) => res.end());
const also = express.Router();
also.get('/me', (req, res) => res.end()); // duplicate, loses to the first
also.delete('/thing/:id', (req, res) => res.end());
app.use((req, res, next) => next()); // not a router
app.use('/api/auth', users);
app.use('/api/auth', also);
app.get('/api/health', (req, res) => res.end()); // straight on the app
app.get(['/', '/index.html', '/assistant'], (req, res) => res.end()); // pages, not API
return app;
}
test('every mounted route is found, at its full path', () => {
const found = openapi.routes(syntheticApp());
const seen = found.map(r => r.method + ' ' + r.path).sort();
assert.deepEqual(seen, [
'DELETE /api/auth/thing/:id',
'GET /api/auth/me',
'GET /api/auth/thing/:id',
'GET /api/health',
'POST /api/auth/login'
]);
});
test('a duplicate path is documented once — Express answers with the first', () => {
const found = openapi.routes(syntheticApp());
assert.equal(found.filter(r => r.method === 'GET' && r.path === '/api/auth/me').length, 1);
});
test('plain middleware is not mistaken for a router', () => {
// app.use(fn) has a regexp matching everything; treating it as a mount would
// invent a path for every route under it.
assert.equal(openapi.mountPath({ regexp: /^\/?(?=\/|$)/i }), '');
});
test('a mount regexp reads back as the path it was mounted at', () => {
const app = syntheticApp();
const mounts = app._router.stack.filter(l => l.handle && l.handle.stack).map(openapi.mountPath);
assert.ok(mounts.includes('/api/auth'), 'got: ' + JSON.stringify(mounts));
});
test('an app with no router yields nothing rather than throwing', () => {
// The endpoint must fail soft: a broken document is better than a 500 on a
// page that was only ever informational.
assert.deepEqual(openapi.routes(null), []);
assert.deepEqual(openapi.routes({}), []);
});
test('path params become OpenAPI templates and are declared', () => {
assert.equal(openapi.toTemplate('/api/my-resources/:id/export'), '/api/my-resources/{id}/export');
const doc = openapi.document(syntheticApp());
const op = doc.paths['/api/auth/thing/{id}'].get;
assert.equal(op.parameters.length, 1);
assert.equal(op.parameters[0].name, 'id');
assert.equal(op.parameters[0].in, 'path');
assert.equal(op.parameters[0].required, true);
});
test('the document is valid OpenAPI 3.1 with both auth schemes declared', () => {
const doc = openapi.document(syntheticApp(), { version: '1.2.3' });
assert.equal(doc.openapi, '3.1.0');
assert.equal(doc.info.version, '1.2.3');
assert.ok(doc.paths['/api/health']);
assert.deepEqual(Object.keys(doc.components.securitySchemes).sort(), ['bearer', 'cookie']);
// Cookie and bearer are alternatives, not both required.
assert.equal(doc.security.length, 2);
});
test('routes are grouped so a reader can find them', () => {
const doc = openapi.document(syntheticApp());
assert.deepEqual(doc.paths['/api/auth/me'].get.tags, ['auth']);
assert.deepEqual(doc.paths['/api/health'].get.tags, ['health']);
});
// ---- the metadata half -----------------------------------------------------
test('metadata is keyed exactly as routes are mounted', () => {
const meta = require('../src/utils/openapiRoutes');
for (const key of Object.keys(meta.operations)) {
assert.match(key, /^(GET|POST|PUT|DELETE|PATCH) \/api\//, 'malformed key: ' + key);
}
});
test('a public operation says so, and is the only kind that does', () => {
// Guessing this from middleware gets it wrong in the direction that hides a
// hole, so it is stated per route. Registration status must be public — the
// sign-in screen reads it before anyone has a session.
const meta = require('../src/utils/openapiRoutes');
assert.equal(meta.operations['GET /api/auth/registration-status'].public, true);
assert.equal(meta.operations['GET /api/health'].public, true);
assert.equal(meta.operations['POST /api/auth/logout'].public, undefined);
});
test('the endpoint is mounted after every router, and behind auth', () => {
const fs = require('fs');
const server = fs.readFileSync(require('path').join(__dirname, '..', 'server.js'), 'utf8');
const at = server.indexOf("app.get('/api/openapi.json'");
assert.ok(at > -1, 'the endpoint is not mounted');
assert.match(server.slice(at, at + 200), /authMiddleware/);
// Anything mounted below it would be missing from the document.
const lastMount = server.lastIndexOf("app.use('/api");
assert.ok(lastMount < at, 'a router is mounted after the openapi endpoint');
});