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
79 lines
3.8 KiB
JavaScript
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);
|
|
});
|
|
});
|