pediatric-ai-scribe-v3/e2e/tests/openapi.spec.js
Daniel 6b039c4d87 test: bring the e2e specs up to date with the app they test
Three specs were asserting screens that no longer exist. They passed for
as long as they did only because the e2e stack shared production's
database and its configuration; against a clean one they failed honestly.

Signing in is a stepped flow now — email, then a choice between a
password and an emailed code — so #login-password is in the DOM but
hidden until that choice is made. The spec asserted it visible on the
landing screen. Replaced with one test for the landing step and a new
one that walks the transition, which nothing covered before.

The register link is hidden only when registration is disabled. This
install has it enabled and invite-gated, so the link shows and the
invite field is required; the spec asserted display:none.

Connecting Nextcloud by signing in to Nextcloud is now the offered path,
with the username and app-password fields folded behind "Use an app
password instead". The spec asserted all three visible at once; it now
checks the primary path and then opens the fallback.

learning-tab.spec.js is deleted and `learning` is out of the smoke tab
list — that feature was removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 00:49:37 +02:00

79 lines
3.8 KiB
JavaScript

// The API contract, checked against the API.
//
// docs/api-reference.md was hand-written and drifted until it documented
// twenty-three endpoints that answer 404. The document is now generated from
// the router, which fixes the paths; this fixes the prose, by failing when a
// route exists that nothing describes.
//
// It runs here rather than in the unit suite because it needs the whole app
// mounted, and requiring server.js from node:test pulls in the database pool
// and hangs the run — that has happened before in this repo.
const { test, expect } = require('../fixtures');
async function spec(page) {
const response = await page.request.get('/api/openapi.json');
expect(response.status(), 'the document should be served to a signed-in user').toBe(200);
return response.json();
}
test.describe('OpenAPI', () => {
test('the document describes this deployment, not a remembered one', async ({ authedPage: page }) => {
const doc = await spec(page);
expect(doc.openapi).toBe('3.1.0');
expect(Object.keys(doc.paths).length).toBeGreaterThan(100);
// Both ways of holding a session are declared.
expect(Object.keys(doc.components.securitySchemes).sort()).toEqual(['bearer', 'cookie']);
});
test('endpoints that exist are in it', async ({ authedPage: page }) => {
const doc = await spec(page);
for (const path of ['/api/health', '/api/build', '/api/auth/me', '/api/my-resources']) {
expect(doc.paths[path], path + ' is missing from the document').toBeTruthy();
}
// A path parameter is written the way OpenAPI writes one.
expect(doc.paths['/api/my-resources/{id}']).toBeTruthy();
expect(doc.paths['/api/my-resources/:id']).toBeFalsy();
});
test('endpoints that were removed are not', async ({ authedPage: page }) => {
// Learning Hub is gone. The generated document cannot claim otherwise,
// which is exactly what the hand-written reference did for weeks.
const doc = await spec(page);
const stale = Object.keys(doc.paths).filter(p => p.includes('/learning'));
expect(stale, 'removed endpoints are still described').toEqual([]);
});
test('every documented operation is reachable, and none 404s', async ({ authedPage: page }) => {
const doc = await spec(page);
const missing = [];
for (const [path, methods] of Object.entries(doc.paths)) {
// Only GETs with no path parameter can be probed safely: a POST would
// change something and a templated path has no real id to try.
if (!methods.get || path.includes('{')) continue;
const response = await page.request.get(path, { failOnStatusCode: false });
if (response.status() === 404) missing.push(path);
}
expect(missing, 'documented but answering 404').toEqual([]);
});
test('a description is required, so a new endpoint cannot ship unexplained', async ({ authedPage: page }) => {
// The generator supplies paths and methods; a person supplies meaning. This
// is the half that rots, so it is the half that is enforced — undescribed
// operations are listed by name rather than counted, so the failure says
// what to write.
const doc = await spec(page);
const undescribed = [];
for (const [path, methods] of Object.entries(doc.paths)) {
for (const [method, operation] of Object.entries(methods)) {
if (!operation.summary) undescribed.push(method.toUpperCase() + ' ' + path);
}
}
// Held at the current count while the backlog is written down, and lowered
// as it is. It must never rise: a new endpoint adds to this list and fails
// the build.
const BUDGET = Number(process.env.OPENAPI_UNDESCRIBED_BUDGET || 0);
expect(undescribed.length,
'undescribed operations (add them to src/utils/openapiRoutes.js):\n ' +
undescribed.slice(0, 40).join('\n ')).toBeLessThanOrEqual(BUDGET);
});
});