mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-28 12:36:33 +00:00
feat(api): clean up published OpenAPI spec for downstream codegens (#7714)
* feat(api): clean up published OpenAPI spec for downstream codegens
The /api/openapi.json doc had four issues that broke generated tooling
(printingpress.dev, openapi-generator, Postman): empty top-level tags
array, every operation duplicated as GET+POST, 14 operations missing
from the resources map (drift since API 1.2.8), and empty summaries on
several tracked ops.
This PR splits the runtime spec from the published spec via a {public}
flag on generateDefinitionForVersion: the runtime definition fed to
openapi-backend keeps both verbs (existing third-party clients that
call GET /api/x.x.x/foo?apikey=... continue to work), while the spec
served at /api/openapi.json, /rest/openapi.json, and per-version paths
advertises only POST.
Other changes:
- top-level tags array declares pad/author/session/group/chat/server
- per-op tags override added to SwaggerUIResource type so chat ops
(still nested under pad for routing) and checkToken can be tagged
without changing existing REST URLs
- 14 missing ops (getAttributePool, getRevisionChangeset, copyPad,
movePad, getPadID, getSavedRevisionsCount, listSavedRevisions,
saveRevision, restoreRevision, appendText, copyPadWithoutHistory,
compactPad, anonymizeAuthor, getStats) backfilled with summaries
- empty summaries on listSessionsOfGroup, listAllGroups,
createDiffHTML, createPad filled in
- new backend tests assert the public spec shape (tags, summaries,
POST-only) and that runtime routing still resolves both verbs
Driven by integrating Etherpad with printingpress.dev: pointing the
generator at the previous spec produced a 96-command CLI with no
resource grouping and many empty descriptions. Design notes in
docs/superpowers/specs/2026-05-10-openapi-cleanup-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api): keep checkToken at /rest/<ver>/pad/checkToken (Qodo #2)
Moving checkToken to a new `server` resource broke REST-style
backward compat: existing callers of /rest/<ver>/pad/checkToken
would have hit `code: 3` (no such function). The whole point of
per-op tag overrides is to preserve REST URLs while still grouping
correctly in OpenAPI tags — checkToken should follow the same
pattern as the chat ops.
Keep checkToken in `resources.pad`, give it `tags: ['server']`,
and add a regression test asserting /rest/<ver>/pad/checkToken
still resolves. The `server` resource still exists for `getStats`
(genuinely new server-level op with no prior REST URL).
Updates the design doc accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
df0ecec834
commit
cbf71285a2
4 changed files with 369 additions and 25 deletions
|
|
@ -112,4 +112,109 @@ describe(__filename, function () {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('public OpenAPI spec shape (for downstream codegens)', function () {
|
||||
let spec: any;
|
||||
|
||||
before(async function () {
|
||||
this.timeout(15000);
|
||||
spec = (await agent.get('/api/openapi.json').expect(200)).body;
|
||||
});
|
||||
|
||||
it('declares a top-level tags array with all expected resource groups', function () {
|
||||
if (!Array.isArray(spec.tags)) {
|
||||
throw new Error(`Expected top-level tags to be an array, got ${typeof spec.tags}`);
|
||||
}
|
||||
const names = spec.tags.map((t: any) => t.name);
|
||||
const expected = ['pad', 'author', 'session', 'group', 'chat', 'server'];
|
||||
const missing = expected.filter((n) => !names.includes(n));
|
||||
if (missing.length) {
|
||||
throw new Error(`Top-level tags missing entries: ${missing.join(', ')}; got: ${names}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('tags every operation with at least one non-empty tag', function () {
|
||||
const untagged: string[] = [];
|
||||
for (const [path, methods] of Object.entries(spec.paths)) {
|
||||
for (const [method, op] of Object.entries(methods as any)) {
|
||||
const tags = (op as any).tags;
|
||||
if (!Array.isArray(tags) || tags.length === 0 || tags.some((t) => !t)) {
|
||||
untagged.push(`${method.toUpperCase()} ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (untagged.length) {
|
||||
throw new Error(`${untagged.length} operations are untagged: ${untagged.join(', ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('summarizes every operation', function () {
|
||||
const unsummarized: string[] = [];
|
||||
for (const [path, methods] of Object.entries(spec.paths)) {
|
||||
for (const [method, op] of Object.entries(methods as any)) {
|
||||
const summary = (op as any).summary;
|
||||
if (typeof summary !== 'string' || summary.trim().length < 3) {
|
||||
unsummarized.push(
|
||||
`${method.toUpperCase()} ${path} (summary=${JSON.stringify(summary)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unsummarized.length) {
|
||||
throw new Error(
|
||||
`${unsummarized.length} operations have empty/missing summaries: ` +
|
||||
unsummarized.join(', '));
|
||||
}
|
||||
});
|
||||
|
||||
it('advertises only POST per path (downstream tooling cleanliness)', function () {
|
||||
const offenders: string[] = [];
|
||||
for (const [path, methods] of Object.entries(spec.paths)) {
|
||||
const verbs = Object.keys(methods as any);
|
||||
if (verbs.length !== 1 || verbs[0] !== 'post') {
|
||||
offenders.push(`${path} has methods: ${verbs.join(', ')}`);
|
||||
}
|
||||
}
|
||||
if (offenders.length) {
|
||||
throw new Error(
|
||||
`Public spec must advertise only POST per path; offenders:\n ${
|
||||
offenders.join('\n ')}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime backward compatibility (GET + POST still routed)', function () {
|
||||
// The runtime spec used by openapi-backend keeps both verbs even though the
|
||||
// public /api/openapi.json advertises POST only. The point of these tests
|
||||
// is to prove openapi-backend still resolves both verbs to the handler
|
||||
// — not to exercise auth. A 401 (or any non-`code 3` body) proves the
|
||||
// request reached the handler. `code: 3` is Etherpad's "no such function"
|
||||
// response, returned by openapi-backend's notFound when a method is not
|
||||
// declared in the runtime spec.
|
||||
|
||||
const assertResolved = (path: string, body: any) => {
|
||||
if (body && body.code === 3) {
|
||||
throw new Error(
|
||||
`${path} got 'no such function' (code 3) — runtime spec dropped the ` +
|
||||
`verb. Response body: ${JSON.stringify(body)}`);
|
||||
}
|
||||
};
|
||||
|
||||
it('GET requests still reach the API handler', async function () {
|
||||
const r = await agent.get(endPoint('checkToken'));
|
||||
assertResolved('GET checkToken', r.body);
|
||||
});
|
||||
|
||||
it('POST requests still reach the API handler', async function () {
|
||||
const r = await agent.post(endPoint('checkToken'));
|
||||
assertResolved('POST checkToken', r.body);
|
||||
});
|
||||
|
||||
// Regression for the REST-style routes — checkToken's _restPath is
|
||||
// derived from its position in the resources map (pad/checkToken).
|
||||
// Tagging it as 'server' must not move it to /rest/X/server/checkToken.
|
||||
it('REST-style /rest/<ver>/pad/checkToken still resolves', async function () {
|
||||
const r = await agent.get(`/rest/${apiVersion}/pad/checkToken`);
|
||||
assertResolved('GET /rest pad/checkToken', r.body);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue