@bongos/core 1.21.155 → 1.21.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +43 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +159 -2
- package/docs/api-reference.md +5 -2
- package/docs/copy-inventory.md +31 -25
- package/docs/copy-registry.json +104 -50
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-readings.json +82 -77
- package/modules/government/catalog.js +1 -1
- package/modules/hall-ui/public/hall-widgets.js +1 -1
- package/modules/hall-ui/public/modules.html +11 -0
- package/modules/hall-ui/public/modules.js +104 -0
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/records/catalog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/src/bongos/module-testing.js +73 -0
- package/src/bongos/project-settings.js +3 -0
- package/src/bongos/routes/modules.js +43 -0
- package/src/module-api.js +1 -1
- package/tests/hall_catalog_world.mjs +18 -2
- package/tests/module_testing.mjs +171 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/bongos/module-testing.js — whether this project tests new module versions
|
|
2
|
+
// first (task 1003802, criterion wa5 staged rollout, ADR 0344 D1).
|
|
3
|
+
//
|
|
4
|
+
// A PROJECT opts in, once, and it is the project's own admin who does it: the
|
|
5
|
+
// project is what runs the module, so no builder can volunteer someone else's
|
|
6
|
+
// project. A tester project gets tester-channel versions of every module it has,
|
|
7
|
+
// except the modules it marked "general only". Both choices are reversible, and
|
|
8
|
+
// "willing" is load-bearing: a project is never a tester by default, and a choice
|
|
9
|
+
// that cannot be read reads as NOT a tester (an early version nobody chose is an
|
|
10
|
+
// early victim, not a tester).
|
|
11
|
+
//
|
|
12
|
+
// Storage is two project_settings rows (src/bongos/project-settings.js), the same
|
|
13
|
+
// durable, survives-a-deploy store the project's other knobs use, so no new table:
|
|
14
|
+
//
|
|
15
|
+
// modules.tester 'on' | 'off' absent = off
|
|
16
|
+
// modules.general_only JSON array of module keys absent = []
|
|
17
|
+
//
|
|
18
|
+
// This file records the choice and answers it. Handing a tester project tester
|
|
19
|
+
// versions is task 1003805, which asks testsVersionsOf(key).
|
|
20
|
+
|
|
21
|
+
const { getSetting, setSetting, SETTING_KEYS } = require('./project-settings');
|
|
22
|
+
const { KEY_RE } = require('../module-loader/manifest-schema');
|
|
23
|
+
|
|
24
|
+
function parseTester(raw) {
|
|
25
|
+
return raw === 'on';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A stored list that will not parse, or holds something that is not a key, keeps
|
|
29
|
+
// only the keys it can read: losing a "general only" mark would hand a project an
|
|
30
|
+
// early version of a module it asked to keep on general.
|
|
31
|
+
function parseGeneralOnly(raw) {
|
|
32
|
+
if (raw == null) return [];
|
|
33
|
+
let list;
|
|
34
|
+
try { list = JSON.parse(raw); } catch { return []; }
|
|
35
|
+
if (!Array.isArray(list)) return [];
|
|
36
|
+
return [...new Set(list.filter((k) => typeof k === 'string' && KEY_RE.test(k)))].sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// { tester, general_only, updated_at, updated_by } — updated_* from the switch row.
|
|
40
|
+
async function readTesting(deps = {}) {
|
|
41
|
+
const get = deps.getSetting || getSetting;
|
|
42
|
+
const [t, g] = await Promise.all([get(SETTING_KEYS.MODULE_TESTER), get(SETTING_KEYS.MODULE_GENERAL_ONLY)]);
|
|
43
|
+
return {
|
|
44
|
+
tester: parseTester(t && t.value),
|
|
45
|
+
general_only: parseGeneralOnly(g && g.value),
|
|
46
|
+
updated_at: t ? t.updated_at : null,
|
|
47
|
+
updated_by: t && t.updated_by != null ? String(t.updated_by) : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function setTester(on, builderId = null, deps = {}) {
|
|
52
|
+
const set = deps.setSetting || setSetting;
|
|
53
|
+
await set(SETTING_KEYS.MODULE_TESTER, on ? 'on' : 'off', builderId);
|
|
54
|
+
return readTesting(deps);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function setGeneralOnly(moduleKey, on, builderId = null, deps = {}) {
|
|
58
|
+
if (!KEY_RE.test(String(moduleKey))) throw new Error(`not a module key: ${moduleKey}`);
|
|
59
|
+
const set = deps.setSetting || setSetting;
|
|
60
|
+
const now = await readTesting(deps);
|
|
61
|
+
const keys = new Set(now.general_only);
|
|
62
|
+
if (on) keys.add(moduleKey); else keys.delete(moduleKey);
|
|
63
|
+
await set(SETTING_KEYS.MODULE_GENERAL_ONLY, JSON.stringify([...keys].sort()), builderId);
|
|
64
|
+
return readTesting(deps);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Whether this project takes tester-channel versions of one module. Pure over a
|
|
68
|
+
// readTesting() result.
|
|
69
|
+
function testsVersionsOf(testing, moduleKey) {
|
|
70
|
+
return !!(testing && testing.tester && !testing.general_only.includes(moduleKey));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { readTesting, setTester, setGeneralOnly, testsVersionsOf, parseGeneralOnly };
|
|
@@ -53,6 +53,9 @@ const SETTING_KEYS = {
|
|
|
53
53
|
TWEAK_AUTOPICKUP: 'tweak.autopickup',
|
|
54
54
|
TWEAK_AUTOPICKUP_CAP: 'tweak.autopickup_monthly_cap',
|
|
55
55
|
SPEND_CAP: 'treasury.spend_cap_usd',
|
|
56
|
+
// Testing new module versions first (task 1003802, ADR 0344 D1): module-testing.js.
|
|
57
|
+
MODULE_TESTER: 'modules.tester',
|
|
58
|
+
MODULE_GENERAL_ONLY: 'modules.general_only',
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
// SHIP MODE (task 1004628, ADR 0369) — WHO starts a ship, never HOW it lands.
|
|
@@ -111,6 +111,7 @@ const log = require('../logger').logger.child({ src: 'modules' });
|
|
|
111
111
|
// as unused exports and tripped the dead-code ratchet that held this branch out of main.
|
|
112
112
|
const { listOverrides, setOverride } = require('../module-overrides');
|
|
113
113
|
const { listCatalog, SORT_PARTS } = require('../module-listing');
|
|
114
|
+
const { readTesting, setTester, setGeneralOnly } = require('../module-testing');
|
|
114
115
|
|
|
115
116
|
// The sentence for a toggle that waits on a restart (task 1004463). 'routes' are the
|
|
116
117
|
// module's pages, 'pollers' its background jobs. PURE.
|
|
@@ -333,6 +334,48 @@ module.exports = function buildModulesRouter() {
|
|
|
333
334
|
});
|
|
334
335
|
}));
|
|
335
336
|
|
|
337
|
+
// Testing new module versions first (task 1003802, ADR 0344 D1): whether THIS
|
|
338
|
+
// project gets tester-channel versions, and which modules it keeps on general.
|
|
339
|
+
// A read anyone signed in can make: the switch is meant to be visible.
|
|
340
|
+
//
|
|
341
|
+
// rank: any-builder — a read of this project's own setting; nothing changes.
|
|
342
|
+
router.get('/modules/testing', auth.requireBuilder,
|
|
343
|
+
asyncHandler('GET /modules/testing', async (_req, res) => {
|
|
344
|
+
res.set('Cache-Control', 'no-store');
|
|
345
|
+
res.json(await readTesting());
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
// rank: metic+archon — the project's admin volunteers the whole project, so it
|
|
349
|
+
// takes the same permission that chooses which modules this project runs
|
|
350
|
+
// (module.enable): a builder cannot opt in a project they cannot run modules for.
|
|
351
|
+
router.post('/modules/testing', auth.requireBuilder, auth.requirePermission('module.enable'),
|
|
352
|
+
asyncHandler('POST /modules/testing', async (req, res) => {
|
|
353
|
+
if (validateOrRespond(req, res, { tester: { type: 'boolean', required: true } })) return;
|
|
354
|
+
const out = await setTester(req.body.tester, req.builder && req.builder.id);
|
|
355
|
+
log.info({ tester: out.tester, builder: req.builder && req.builder.id }, 'module testing switched');
|
|
356
|
+
res.json({
|
|
357
|
+
ok: true,
|
|
358
|
+
...out,
|
|
359
|
+
note: out.tester
|
|
360
|
+
? 'This project now gets new module versions first, except the modules marked general only.'
|
|
361
|
+
: 'This project is back on general versions from its next update.',
|
|
362
|
+
});
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
// rank: metic+archon — same reasoning as POST /modules/testing: which versions
|
|
366
|
+
// a module arrives in is part of choosing the modules this project runs.
|
|
367
|
+
router.post('/modules/:key/general-only', auth.requireBuilder, auth.requirePermission('module.enable'),
|
|
368
|
+
asyncHandler('POST /modules/:key/general-only', async (req, res) => {
|
|
369
|
+
const { key } = req.params;
|
|
370
|
+
if (!KEY_RE.test(key)) return res.fail('bad_module_key', { status: 400, message: 'A module key is lowercase kebab-case.' });
|
|
371
|
+
if (validateOrRespond(req, res, { general_only: { type: 'boolean', required: true } })) return;
|
|
372
|
+
if (!listModules().some((m) => m.key === key)) {
|
|
373
|
+
return res.fail('not_found', { status: 404, message: `This project has no module "${key}".` });
|
|
374
|
+
}
|
|
375
|
+
const out = await setGeneralOnly(key, req.body.general_only, req.builder && req.builder.id);
|
|
376
|
+
res.json({ ok: true, key, general_only_this: out.general_only.includes(key), ...out });
|
|
377
|
+
}));
|
|
378
|
+
|
|
336
379
|
// POST /api/bongos/modules/:key/submit — ADR 0107 §1 upstreaming submission.
|
|
337
380
|
//
|
|
338
381
|
// task 1770. Body: { signed_by?: "Name <email>" }. Re-runs the §4
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.157'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -39,7 +39,7 @@ const noAlias = (rawCss, where) => {
|
|
|
39
39
|
|
|
40
40
|
// ---- modules ----------------------------------------------------------------
|
|
41
41
|
|
|
42
|
-
test('modules: ONE panel of family rows — the filter bar, the figure band and two groups on one .scroll (
|
|
42
|
+
test('modules: ONE panel of family rows — the filter bar, the figure band and two groups on one .scroll (the store since task 1003799, testing since task 1003802)', () => {
|
|
43
43
|
const html = read('modules.html');
|
|
44
44
|
assert.match(html, /<link rel="stylesheet" href="\/builders\/oversight\.css">\s*<link rel="stylesheet" href="\/builders\/modules\.css">/, 'the family sheet loads before the page sheet');
|
|
45
45
|
assert.equal((html.match(/class="scroll[ "]/g) || []).length, 1, 'one panel, not three');
|
|
@@ -47,7 +47,7 @@ test('modules: ONE panel of family rows — the filter bar, the figure band and
|
|
|
47
47
|
assert.match(html, /class="ov-figs" id="mod-figs"/, 'the figure band is the family\'s');
|
|
48
48
|
assert.match(html, /<ul class="ov-rows" id="core-grid">/, 'the core catalog is a run of rows');
|
|
49
49
|
assert.match(html, /<ul class="ov-rows" id="own-grid">/, 'the project\'s own modules are a run of rows');
|
|
50
|
-
assert.equal((html.match(/class="ov-group__head"/g) || []).length,
|
|
50
|
+
assert.equal((html.match(/class="ov-group__head"/g) || []).length, 4, 'four group heads: the core, this project, testing, the store');
|
|
51
51
|
assert.doesNotMatch(decomment(html), /mod-grid|mod-card|mod-browse|submit-scroll|class="settings-status"/, 'the card grid, the boxed search and the third panel are gone');
|
|
52
52
|
assert.doesNotMatch(html, /\d+ module\(s\)/, 'the page head no longer says the numbers as prose');
|
|
53
53
|
});
|
|
@@ -106,6 +106,22 @@ test('modules: the store group reads strengths and weaknesses off the server row
|
|
|
106
106
|
assert.notDeepEqual(two[0].strengths.map((s) => s.part), two[1].strengths.map((s) => s.part), '…that are strong at different things');
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
test('modules: the project opts in to testing new versions, and can keep a module on general (task 1003802)', () => {
|
|
110
|
+
const js = read('modules.js');
|
|
111
|
+
const html = read('modules.html');
|
|
112
|
+
assert.match(html, /<div class="ov-group" id="testing-group" hidden>/, 'hidden until the switch is read');
|
|
113
|
+
assert.match(html, /It is never switched on for you\./, 'the page says a project is never a tester by default');
|
|
114
|
+
assert.match(js, /api\.request\('GET', `\$\{API\}\/modules\/testing`\)/, 'it reads GET /modules/testing');
|
|
115
|
+
assert.match(js, /`\$\{API\}\/modules\/testing`, \{ tester: next \}/, 'the switch posts an explicit true or false');
|
|
116
|
+
assert.match(js, /\/modules\/\$\{encodeURIComponent\(key\)\}\/general-only`, \{ general_only: on \}/, 'a module is kept on general by its own key');
|
|
117
|
+
assert.match(js, /class="ov-toggle mod-testing"/, 'the switch is the family grant toggle');
|
|
118
|
+
assert.match(js, /if \(!testing \|\| !testing\.tester\) return '';/, 'the per-module line appears only while the project is testing');
|
|
119
|
+
assert.match(js, /if \(!canToggle\) return escapeHtml\(says\);/, 'a builder who cannot switch it reads the state, with no dead control');
|
|
120
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'modules__testing.json'), 'utf8'));
|
|
121
|
+
assert.equal(fixture.tester, true, 'the harness demos the switch on');
|
|
122
|
+
assert.ok(fixture.general_only.length >= 1, '…with a module kept on general');
|
|
123
|
+
});
|
|
124
|
+
|
|
109
125
|
test('modules: the buyer sorts the store by the part that matters to them (task 1003800)', () => {
|
|
110
126
|
const js = read('modules.js');
|
|
111
127
|
const html = read('modules.html');
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// tests/module_testing.mjs — a project elects to test new module versions first
|
|
2
|
+
// (task 1003802, ADR 0344 D1). Two halves, no port and no real DB:
|
|
3
|
+
// - the rule: module-testing.js over an in-memory project_settings, through
|
|
4
|
+
// its deps seam — never a tester by default, reversible, "general only";
|
|
5
|
+
// - the routes: the real handlers off the real router, with pool.js replaced
|
|
6
|
+
// in the require cache (the tests/module_store_listing.mjs harness).
|
|
7
|
+
//
|
|
8
|
+
// Run: node tests/module_testing.mjs
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
// ---- an in-memory project_settings, behind the real setSetting/getSetting SQL ----
|
|
17
|
+
const rows = new Map();
|
|
18
|
+
const pool = {
|
|
19
|
+
async query(sql, params = []) {
|
|
20
|
+
if (/FROM project_settings WHERE key = \$1/.test(sql)) {
|
|
21
|
+
const r = rows.get(params[0]);
|
|
22
|
+
return { rows: r ? [r] : [] };
|
|
23
|
+
}
|
|
24
|
+
if (/INSERT INTO project_settings/.test(sql)) {
|
|
25
|
+
const r = { key: params[0], value: params[1], updated_by: params[2], updated_at: 't' };
|
|
26
|
+
rows.set(params[0], r);
|
|
27
|
+
return { rows: [r] };
|
|
28
|
+
}
|
|
29
|
+
return { rows: [] };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
33
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
|
|
34
|
+
|
|
35
|
+
const { readTesting, setTester, setGeneralOnly, testsVersionsOf, parseGeneralOnly } = require('../src/bongos/module-testing.js');
|
|
36
|
+
const buildModulesRouter = require('../src/bongos/routes/modules.js');
|
|
37
|
+
const { listModules } = buildModulesRouter;
|
|
38
|
+
|
|
39
|
+
function handlerFor(router, method, p) {
|
|
40
|
+
const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
|
|
41
|
+
assert.ok(layer, `no ${method.toUpperCase()} ${p} on the router`);
|
|
42
|
+
const stack = layer.route.stack;
|
|
43
|
+
return { handle: stack[stack.length - 1].handle, names: stack.map((s) => s.name) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function invoke(handle, req) {
|
|
47
|
+
const res = {
|
|
48
|
+
statusCode: 200, body: undefined, headers: {},
|
|
49
|
+
json(b) { this.body = b; return this; },
|
|
50
|
+
status(c) { this.statusCode = c; return this; },
|
|
51
|
+
set(k, v) { this.headers[k] = v; return this; },
|
|
52
|
+
// Both shapes res.fail is called with: (code, { status, message }) and the
|
|
53
|
+
// validator's (code, 400, details).
|
|
54
|
+
fail(code, opts) {
|
|
55
|
+
const o = typeof opts === 'number' ? { status: opts } : opts;
|
|
56
|
+
this.statusCode = o.status; this.body = { error: code, ...o }; return this;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
let nextErr = null;
|
|
60
|
+
await handle({ params: {}, body: {}, builder: { id: 7 }, ...req }, res, (err) => { nextErr = err; });
|
|
61
|
+
if (nextErr) throw nextErr;
|
|
62
|
+
return res;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// the rule
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
test('a project is never a tester by default', async () => {
|
|
70
|
+
rows.clear();
|
|
71
|
+
const t = await readTesting();
|
|
72
|
+
assert.deepEqual(t, { tester: false, general_only: [], updated_at: null, updated_by: null });
|
|
73
|
+
assert.equal(testsVersionsOf(t, 'discord'), false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('opting in is an act, and it is reversible', async () => {
|
|
77
|
+
rows.clear();
|
|
78
|
+
const on = await setTester(true, 7);
|
|
79
|
+
assert.equal(on.tester, true);
|
|
80
|
+
assert.equal(on.updated_by, '7', 'who switched it is recorded');
|
|
81
|
+
assert.equal(testsVersionsOf(on, 'discord'), true);
|
|
82
|
+
const off = await setTester(false, 7);
|
|
83
|
+
assert.equal(off.tester, false);
|
|
84
|
+
assert.equal(testsVersionsOf(off, 'discord'), false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a module marked general only keeps general versions while the rest test', async () => {
|
|
88
|
+
rows.clear();
|
|
89
|
+
await setTester(true, 7);
|
|
90
|
+
await setGeneralOnly('economy', true, 7);
|
|
91
|
+
const t = await setGeneralOnly('discord', true, 7);
|
|
92
|
+
assert.deepEqual(t.general_only, ['discord', 'economy']);
|
|
93
|
+
assert.equal(testsVersionsOf(t, 'economy'), false);
|
|
94
|
+
assert.equal(testsVersionsOf(t, 'memory'), true);
|
|
95
|
+
const back = await setGeneralOnly('economy', false, 7);
|
|
96
|
+
assert.deepEqual(back.general_only, ['discord']);
|
|
97
|
+
assert.equal(testsVersionsOf(back, 'economy'), true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('a "general only" mark survives the switch going off and on', async () => {
|
|
101
|
+
rows.clear();
|
|
102
|
+
await setTester(true, 7);
|
|
103
|
+
await setGeneralOnly('economy', true, 7);
|
|
104
|
+
await setTester(false, 7);
|
|
105
|
+
const t = await setTester(true, 7);
|
|
106
|
+
assert.deepEqual(t.general_only, ['economy']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('an unreadable stored value reads as NOT a tester, and keeps the keys it can read', async () => {
|
|
110
|
+
rows.clear();
|
|
111
|
+
rows.set('modules.tester', { key: 'modules.tester', value: 'yes please', updated_by: null, updated_at: 't' });
|
|
112
|
+
assert.equal((await readTesting()).tester, false, 'only "on" is on');
|
|
113
|
+
assert.deepEqual(parseGeneralOnly('not json'), []);
|
|
114
|
+
assert.deepEqual(parseGeneralOnly('{"a":1}'), []);
|
|
115
|
+
assert.deepEqual(parseGeneralOnly('["economy", 4, "Bad Key", "economy", "discord"]'), ['discord', 'economy']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('setGeneralOnly refuses something that is not a module key', async () => {
|
|
119
|
+
await assert.rejects(() => setGeneralOnly('../etc', true, 7), /not a module key/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// the routes
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
test('GET /modules/testing: any signed-in builder can see the switch', async () => {
|
|
127
|
+
rows.clear();
|
|
128
|
+
const { handle, names } = handlerFor(buildModulesRouter(), 'get', '/modules/testing');
|
|
129
|
+
assert.equal(names.length, 2, `expected requireBuilder + handler, got ${names.join(', ')}`);
|
|
130
|
+
const res = await invoke(handle, {});
|
|
131
|
+
assert.equal(res.body.tester, false);
|
|
132
|
+
assert.equal(res.headers['Cache-Control'], 'no-store');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('POST /modules/testing and /modules/:key/general-only take the module.enable permission', () => {
|
|
136
|
+
const router = buildModulesRouter();
|
|
137
|
+
for (const p of ['/modules/testing', '/modules/:key/general-only']) {
|
|
138
|
+
const { names } = handlerFor(router, 'post', p);
|
|
139
|
+
assert.equal(names.length, 3, `${p}: requireBuilder + requirePermission + handler, got ${names.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
const src = require('node:fs').readFileSync(require.resolve('../src/bongos/routes/modules.js'), 'utf8');
|
|
142
|
+
assert.match(src, /router\.post\('\/modules\/testing', auth\.requireBuilder, auth\.requirePermission\('module\.enable'\)/);
|
|
143
|
+
assert.match(src, /router\.post\('\/modules\/:key\/general-only', auth\.requireBuilder, auth\.requirePermission\('module\.enable'\)/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('POST /modules/testing: switches on and off, and says what it means', async () => {
|
|
147
|
+
rows.clear();
|
|
148
|
+
const { handle } = handlerFor(buildModulesRouter(), 'post', '/modules/testing');
|
|
149
|
+
const on = await invoke(handle, { body: { tester: true } });
|
|
150
|
+
assert.equal(on.body.tester, true);
|
|
151
|
+
assert.match(on.body.note, /new module versions first/);
|
|
152
|
+
const off = await invoke(handle, { body: { tester: false } });
|
|
153
|
+
assert.equal(off.body.tester, false);
|
|
154
|
+
assert.match(off.body.note, /back on general versions from its next update/);
|
|
155
|
+
const bad = await invoke(handle, { body: { tester: 'on' } });
|
|
156
|
+
assert.equal(bad.statusCode, 400, 'an affirmative act is a boolean, not a string that looks like one');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('POST /modules/:key/general-only: only a module this project has', async () => {
|
|
160
|
+
rows.clear();
|
|
161
|
+
const { handle } = handlerFor(buildModulesRouter(), 'post', '/modules/:key/general-only');
|
|
162
|
+
const known = listModules()[0].key;
|
|
163
|
+
const ok = await invoke(handle, { params: { key: known }, body: { general_only: true } });
|
|
164
|
+
assert.equal(ok.statusCode, 200);
|
|
165
|
+
assert.equal(ok.body.general_only_this, true);
|
|
166
|
+
assert.deepEqual(ok.body.general_only, [known]);
|
|
167
|
+
const missing = await invoke(handle, { params: { key: 'no-such-module-here' }, body: { general_only: true } });
|
|
168
|
+
assert.equal(missing.statusCode, 404);
|
|
169
|
+
const badKey = await invoke(handle, { params: { key: 'Not_A_Key' }, body: { general_only: true } });
|
|
170
|
+
assert.equal(badKey.statusCode, 400);
|
|
171
|
+
});
|