openvibers/openvibe.casino Commits

/openapi.json: the API as OpenAPI 3.1 (SEO-3) (#43)

OP OpenVibers committed 10 days ago · 47ba47d8d8 · 3 files, +50 −0

It answered 404. It serves the document /api/v1/openapi.json already built.
The agent descriptor names it.

Changes

ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
iindex 8a408df..71fe653 100644
---- a/server/agent-descriptor.js
++++ b/server/agent-descriptor.js
@@@ -9,6 +9,7 @@ module.exports = {
site: "openvibe.casino",
api: "/api/v1",
docs: "/developers",
++ openapi: '/openapi.json',
policy: {
auth: ["none", "Network OIDC"],
},
ddiff --git a/server/app.js b/server/app.js
iindex 55c5d54..382aef0 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -146,6 +146,8 @@ async function createApp(opts = {}) {
// ── API ─────────────────────────────────────────────────
app.use('/api/v1', rateLimit({ windowMs: 60_000, limit: Number(process.env.CASINO_API_RATE_LIMIT_PER_MIN) || 240, standardHeaders: true, legacyHeaders: false, handler: (req, res) => contracts.http.sendProblem(res, 429, 'rate_limited', { detail: 'too many requests from this address; retry shortly', ctx: req.ov }) }));
app.use('/api/v1', createApi(ctx));
++ // GET /openapi.json (SEO-3): the API's OpenAPI 3.1 document, the same one /api/v1/openapi.json serves.
++ app.get('/openapi.json', (_req, res) => res.set('Cache-Control', 'public, max-age=3600').set('Access-Control-Allow-Origin', '*').json(require('./http/api').openapi(config)));
app.use('/api', (req, res) => contracts.http.sendProblem(res, 404, 'route.not_found', { detail: 'No such API route', ctx: req.ov }));
// ── Pages ───────────────────────────────────────────────
ddiff --git a/test/openapi-root.test.js b/test/openapi-root.test.js
nnew file mode 100644
iindex 0000000..8c1f83e
---- /dev/null
++++ b/test/openapi-root.test.js
@@@ -0,0 +1,47 @@
++'use strict';
++// SEO-3: /openapi.json answered 404 here (11-03). It is this service's API as OpenAPI 3.1 (serve: 'serve' is the
++// document /api/v1/openapi.json already served; 'routes' is read from the routes the app mounts).
++const assert = require('assert');
++const { boot } = require('./helpers/boot');
++
++(async () => {
++ const t = await boot();
++ let failed = 0;
++ try {
++ const r = await t.get('/openapi.json');
++ assert.strictEqual(r.status, 200, r.text.slice(0, 200));
++ assert.match(String(r.headers.get('content-type')), /^application\/json/);
++ assert.strictEqual(r.headers.get('access-control-allow-origin'), '*', 'any agent may read it');
++ const doc = JSON.parse(r.text);
++ assert.match(doc.openapi, /^3\.1/);
++ assert.ok(doc.info && doc.info.title && doc.info.version, 'a title and a version');
++ const paths = Object.keys(doc.paths || {});
++ assert.ok(paths.length >= 2, `paths: ${paths.join(' ')}`);
++ for (const p of paths) {
++ for (const [m, op] of Object.entries(doc.paths[p])) {
++ if (!['get', 'put', 'post', 'delete', 'patch'].includes(m)) continue;
++ assert.ok(op.responses && Object.keys(op.responses).length, `${m} ${p} has responses`);
++ for (const name of [...p.matchAll(/\{([^}]+)\}/g)].map((x) => x[1])) {
++ assert.ok((op.parameters || []).some((q) => q.name === name && q.in === 'path') || (doc.paths[p].parameters || []).some((q) => q.name === name), `${m} ${p} declares {${name}}`);
++ }
++ }
++ }
++ if ('serve' === 'routes') {
++ assert.ok(paths.every((p) => p.startsWith('/api/')), 'only the API');
++ assert.ok(!paths.some((p) => /\/(?:health|ready)$|openapi\.json$/.test(p)), 'no probes, not itself');
++ assert.ok(Object.values(doc.paths).some((item) => Object.values(item).some((op) => op.summary)), 'the header comments give summaries');
++ const again = JSON.parse((await t.get('/openapi.json')).text);
++ assert.deepStrictEqual(Object.keys(again.paths), paths, 'the same document each time');
++ } else {
++ const api = await t.get('/api/v1/openapi.json');
++ assert.deepStrictEqual(JSON.parse(api.text), doc, 'the same document as /api/v1/openapi.json');
++ }
++ console.log(` ✓ /openapi.json: ${paths.length} paths`);
++ } catch (e) {
++ failed++;
++ console.log(` ✗ /openapi.json\n ${String(e.stack || e).split('\n').slice(0, 6).join('\n ')}`);
++ } finally {
++ await t.close();
++ }
++ process.exit(failed ? 1 : 0);
++})();