@bongos/core 1.19.533 → 1.19.534
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 +106 -46
- package/clients/bongos-client/index.d.ts +1 -1
- package/config/branding.neutral.json +3 -2
- package/docs/adr/0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md +144 -0
- package/docs/adr/0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md +96 -0
- package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +202 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +8 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +129 -126
- package/docs/copy-registry.json +189 -161
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +14 -0
- package/modules/onboarding/module.json +1 -1
- package/modules/onboarding/routes/access-requests.js +21 -6
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-catalog.js +202 -0
- package/modules/platform-identity/routes/my-projects.js +7 -2
- package/modules/platform-identity/routes/projects.js +6 -0
- package/modules/platform-identity/routes/sso.js +10 -0
- package/modules/provisioning/catalog-bridge.js +105 -0
- package/modules/provisioning/provisioning.js +28 -2
- package/modules/provisioning/routes/provisioning.js +45 -11
- package/modules/provisioning/tests/provisioning.mjs +11 -8
- package/modules/public-landing/public/assets/cosmos.css +15 -0
- package/modules/public-landing/public/projects.html +162 -3
- package/modules/ui-design/kit/serve.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/provision-net.js +7 -0
- package/src/bongos/project-door.js +138 -0
- package/src/branding.js +8 -0
- package/src/module-api.js +12 -1
- package/tests/access_requests_invite.mjs +2 -2
- package/tests/application_lifecycle.mjs +1 -0
- package/tests/catalog_only_client.mjs +242 -0
- package/tests/currency_label.mjs +8 -5
- package/tests/module_api.mjs +1 -0
- package/tests/project_catalog_bridge.mjs +355 -0
- package/tests/project_catalog_routes.mjs +213 -0
- package/tests/project_join_door.mjs +166 -0
- package/tests/project_join_door_route.mjs +206 -0
- package/tests/project_modules_ui.mjs +318 -0
- package/tests/projects_hub.mjs +6 -1
- package/tests/projects_hub_pre_uat.mjs +2 -2
- package/tests/provision_settings_apply.mjs +6 -5
- package/tests/provisioning_module_selection.mjs +164 -3
- package/tests/provisioning_settings.mjs +10 -7
- package/tests/provisioning_settings_apply.mjs +3 -3
- package/tests/provisioning_settings_env.mjs +7 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// tests/project_join_door.mjs — the composed join/apply door (BV1.R12, task
|
|
2
|
+
// 1002331): every visibility state × every join grant × every joinability, plus
|
|
3
|
+
// the derived dark-matter lock and the fail-closed edges.
|
|
4
|
+
//
|
|
5
|
+
// The matrix is written out rather than sampled because the whole point of ADR
|
|
6
|
+
// 0182 D6 is that two knobs must never negotiate — "the stricter of the two always
|
|
7
|
+
// wins, and neither can widen the other" is a claim about all 27 cells, and the
|
|
8
|
+
// bug it prevents (an owner setting `private` on the map while the public
|
|
9
|
+
// application endpoint keeps taking applications) is invisible in any one of them.
|
|
10
|
+
//
|
|
11
|
+
// ESM `import` on purpose, not createRequire: knip does not credit a CJS module's
|
|
12
|
+
// exports to a test that reaches them through require().
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/project_join_door.mjs
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import {
|
|
19
|
+
JOIN_DOORS, DOOR_RANK, PROJECT_JOIN_GRANTS,
|
|
20
|
+
strictestDoor, visibilityCeiling, projectJoinDoor, effectiveJoinDoor,
|
|
21
|
+
} from '../src/bongos/project-door.js';
|
|
22
|
+
|
|
23
|
+
const VISIBILITIES = ['public', 'private', 'stealth'];
|
|
24
|
+
const JOINABILITIES = ['open', 'apply', 'invite_only'];
|
|
25
|
+
|
|
26
|
+
const door = (visibility, joinGrant, joinability, publishable) =>
|
|
27
|
+
projectJoinDoor({ visibility, joinGrant, joinability, publishable });
|
|
28
|
+
|
|
29
|
+
// ── the vocabulary itself ────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
test('the door vocabulary is ordered widest → strictest, and the rank IS that order', () => {
|
|
32
|
+
assert.deepEqual(JOIN_DOORS, ['open', 'apply', 'invite_only', 'locked']);
|
|
33
|
+
assert.deepEqual(DOOR_RANK, { open: 0, apply: 1, invite_only: 2, locked: 3 });
|
|
34
|
+
assert.deepEqual(PROJECT_JOIN_GRANTS, ['view', 'apply', 'full']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('strictestDoor never widens, and an unknown door name is the strictest there is', () => {
|
|
38
|
+
assert.equal(strictestDoor('open', 'apply'), 'apply');
|
|
39
|
+
assert.equal(strictestDoor('invite_only', 'open'), 'invite_only');
|
|
40
|
+
assert.equal(strictestDoor('locked', 'open'), 'locked');
|
|
41
|
+
assert.equal(strictestDoor('open', 'open'), 'open');
|
|
42
|
+
assert.equal(strictestDoor('open', 'whatever'), 'locked', 'a value the table does not carry cannot widen the door');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── the visibility ceiling (ADR 0182 D6's table) ─────────────────────────────
|
|
46
|
+
|
|
47
|
+
test('visibilityCeiling: public defers to the grant, every other state sets its own', () => {
|
|
48
|
+
assert.equal(visibilityCeiling('public', 'full'), 'open');
|
|
49
|
+
assert.equal(visibilityCeiling('public', 'apply'), 'apply');
|
|
50
|
+
assert.equal(visibilityCeiling('public', 'view'), 'invite_only');
|
|
51
|
+
assert.equal(visibilityCeiling('private', 'full'), 'apply', 'private accepts applications, never a self-join');
|
|
52
|
+
assert.equal(visibilityCeiling('stealth', 'full'), 'invite_only', 'stealth refuses the application at the door');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('visibilityCeiling fails closed on anything it cannot read', () => {
|
|
56
|
+
assert.equal(visibilityCeiling('unlisted', 'full'), 'invite_only', 'a fourth enum value is never a planet');
|
|
57
|
+
assert.equal(visibilityCeiling(undefined, 'full'), 'invite_only');
|
|
58
|
+
assert.equal(visibilityCeiling('public', 'FULL'), 'invite_only', 'a grant this core cannot read is not the wide door');
|
|
59
|
+
assert.equal(visibilityCeiling('public', undefined), 'invite_only');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── the full matrix ──────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
test('public: the join grants exactly what the owner set — until joinability narrows it', () => {
|
|
65
|
+
// grant `full` is the "does not narrow" value, so a public project runs the
|
|
66
|
+
// door its joinability already ran: this is what makes the default a no-op.
|
|
67
|
+
assert.equal(door('public', 'full', 'open'), 'open');
|
|
68
|
+
assert.equal(door('public', 'full', 'apply'), 'apply');
|
|
69
|
+
assert.equal(door('public', 'full', 'invite_only'), 'invite_only');
|
|
70
|
+
// grant `apply`: a self-join is off the table even where enrolment is open.
|
|
71
|
+
assert.equal(door('public', 'apply', 'open'), 'apply');
|
|
72
|
+
assert.equal(door('public', 'apply', 'apply'), 'apply');
|
|
73
|
+
assert.equal(door('public', 'apply', 'invite_only'), 'invite_only');
|
|
74
|
+
// grant `view`: the project is on the map to be looked at; joining is by invite.
|
|
75
|
+
assert.equal(door('public', 'view', 'open'), 'invite_only');
|
|
76
|
+
assert.equal(door('public', 'view', 'apply'), 'invite_only');
|
|
77
|
+
assert.equal(door('public', 'view', 'invite_only'), 'invite_only');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('private: invite-only with applications queued — the grant is a PUBLIC join\'s, and does not reach here', () => {
|
|
81
|
+
for (const grant of PROJECT_JOIN_GRANTS) {
|
|
82
|
+
assert.equal(door('private', grant, 'open'), 'apply', `grant ${grant} cannot widen private into a self-join`);
|
|
83
|
+
assert.equal(door('private', grant, 'apply'), 'apply', 'a private project queues the application for its owner');
|
|
84
|
+
assert.equal(door('private', grant, 'invite_only'), 'invite_only');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('stealth: no application is accepted, whatever the other two knobs say', () => {
|
|
89
|
+
for (const grant of PROJECT_JOIN_GRANTS) {
|
|
90
|
+
for (const joinability of JOINABILITIES) {
|
|
91
|
+
assert.equal(door('stealth', grant, joinability), 'invite_only',
|
|
92
|
+
`stealth + ${grant} + ${joinability} still refuses at the door`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('ADR 0182 D6, cell by cell: neither knob can widen the other', () => {
|
|
98
|
+
let cells = 0;
|
|
99
|
+
for (const visibility of VISIBILITIES) {
|
|
100
|
+
for (const grant of PROJECT_JOIN_GRANTS) {
|
|
101
|
+
for (const joinability of JOINABILITIES) {
|
|
102
|
+
const answer = door(visibility, grant, joinability);
|
|
103
|
+
const ceiling = visibilityCeiling(visibility, grant);
|
|
104
|
+
assert.ok(DOOR_RANK[answer] >= DOOR_RANK[ceiling], 'the visibility ceiling is never widened');
|
|
105
|
+
assert.ok(DOOR_RANK[answer] >= DOOR_RANK[joinability], 'the joinability policy is never widened');
|
|
106
|
+
assert.equal(answer, strictestDoor(ceiling, joinability), 'the answer IS the stricter of the two');
|
|
107
|
+
cells++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
assert.equal(cells, 27, 'every state × grant × policy cell is asserted');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── dark matter ──────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
test('dark matter locks the door outright — there is nothing to knock on', () => {
|
|
117
|
+
for (const visibility of VISIBILITIES) {
|
|
118
|
+
for (const grant of PROJECT_JOIN_GRANTS) {
|
|
119
|
+
for (const joinability of JOINABILITIES) {
|
|
120
|
+
assert.equal(door(visibility, grant, joinability, false), 'locked');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('publishable is a TRI-state: only an explicit false locks, and no verdict decides nothing', () => {
|
|
127
|
+
assert.equal(door('public', 'full', 'open', true), 'open', 'a met publish bar opens nothing on its own');
|
|
128
|
+
assert.equal(door('public', 'full', 'open', null), 'open', 'no verdict → the three stored knobs answer alone');
|
|
129
|
+
assert.equal(door('public', 'full', 'open', undefined), 'open');
|
|
130
|
+
assert.equal(door('public', 'full', 'open'), 'open', 'an absent argument is no verdict, never a lock');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('a door surface holding no inputs at all is refused, not opened — and not mis-explained', () => {
|
|
134
|
+
assert.equal(projectJoinDoor(), 'invite_only');
|
|
135
|
+
assert.equal(projectJoinDoor({}), 'invite_only');
|
|
136
|
+
assert.equal(door('public', 'full', 'whenever'), 'invite_only',
|
|
137
|
+
'an unreadable policy closes to invite-only, never to `locked` — that word claims the project is unpublished');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── the live reader ──────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
test('effectiveJoinDoor composes the resolved pack with the admission reader', () => {
|
|
143
|
+
const pack = (project) => () => ({ project });
|
|
144
|
+
assert.equal(effectiveJoinDoor({
|
|
145
|
+
branding: pack({ visibility: 'public', joinGrant: 'full' }), joinabilityMode: () => 'apply',
|
|
146
|
+
}), 'apply');
|
|
147
|
+
assert.equal(effectiveJoinDoor({
|
|
148
|
+
branding: pack({ visibility: 'stealth', joinGrant: 'full' }), joinabilityMode: () => 'open',
|
|
149
|
+
}), 'invite_only', 'an open stealth project still refuses everything (ADR 0182 D6)');
|
|
150
|
+
assert.equal(effectiveJoinDoor({
|
|
151
|
+
branding: pack({ visibility: 'public', joinGrant: 'apply' }), joinabilityMode: () => 'open',
|
|
152
|
+
}), 'apply', 'a grant narrows a project whose enrolment is open');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('effectiveJoinDoor fails closed when the pack will not load', () => {
|
|
156
|
+
const boom = () => { throw new Error('pack unreadable'); };
|
|
157
|
+
assert.equal(effectiveJoinDoor({ branding: boom, joinabilityMode: () => 'open' }), 'invite_only');
|
|
158
|
+
assert.equal(effectiveJoinDoor({ branding: () => ({}), joinabilityMode: () => 'open' }), 'invite_only',
|
|
159
|
+
'a pack with no project block is unknown policy, not an open door');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('effectiveJoinDoor passes a publish verdict through when its caller holds one', () => {
|
|
163
|
+
const pack = () => ({ project: { visibility: 'public', joinGrant: 'full' } });
|
|
164
|
+
assert.equal(effectiveJoinDoor({ branding: pack, joinabilityMode: () => 'open', publishable: false }), 'locked');
|
|
165
|
+
assert.equal(effectiveJoinDoor({ branding: pack, joinabilityMode: () => 'open', publishable: true }), 'open');
|
|
166
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// tests/project_join_door_route.mjs — the composed door AT THE ROUTE (BV1.R12,
|
|
2
|
+
// task 1002331): every visibility state × every join grant, driven through the
|
|
3
|
+
// real public POST /access-requests against a scripted pool.
|
|
4
|
+
//
|
|
5
|
+
// tests/project_join_door.mjs pins the decision; this pins that the DOOR ACTUALLY
|
|
6
|
+
// RUNS IT — the gap R12 exists to close was a correct policy nothing consulted.
|
|
7
|
+
// So the REAL composer is wired in (only `branding` and the joinability reader are
|
|
8
|
+
// stubbed), and every case asserts the query count too: a refused application must
|
|
9
|
+
// cost no DB work and, per ADR 0182 D6, must never be stored.
|
|
10
|
+
//
|
|
11
|
+
// The doorway is stubbed in require.cache BEFORE the route loads — the route
|
|
12
|
+
// destructures pool/validators at require time (the access_requests_invite pattern).
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/project_join_door_route.mjs
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { test, before, after } from 'node:test';
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { effectiveJoinDoor } from '../src/bongos/project-door.js';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const express = require('express');
|
|
24
|
+
|
|
25
|
+
const apiPath = require.resolve('../src/module-api.js');
|
|
26
|
+
const realApi = require(apiPath);
|
|
27
|
+
|
|
28
|
+
let calls = [];
|
|
29
|
+
let script = [];
|
|
30
|
+
// The policy this project runs — the three knobs the platform pushes into the
|
|
31
|
+
// instance's branding pack, exactly as src/branding.js resolves them.
|
|
32
|
+
let policy = { visibility: 'public', joinGrant: 'full', joinability: 'apply' };
|
|
33
|
+
let publishable;
|
|
34
|
+
|
|
35
|
+
const pool = {
|
|
36
|
+
async query(sql, params) {
|
|
37
|
+
calls.push({ sql, params });
|
|
38
|
+
if (!script.length) throw new Error(`unscripted query: ${sql.trim().slice(0, 40)}`);
|
|
39
|
+
const next = script.shift();
|
|
40
|
+
if (next instanceof Error) throw next;
|
|
41
|
+
return next;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
require.cache[apiPath].exports = {
|
|
45
|
+
...realApi,
|
|
46
|
+
pool,
|
|
47
|
+
// The REAL composition, over a stubbed pack + admission reader. Nothing about
|
|
48
|
+
// the decision is faked here — only where its three inputs come from.
|
|
49
|
+
projectJoinDoor: () => effectiveJoinDoor({
|
|
50
|
+
branding: () => ({ project: { visibility: policy.visibility, joinGrant: policy.joinGrant } }),
|
|
51
|
+
joinabilityMode: () => policy.joinability,
|
|
52
|
+
publishable,
|
|
53
|
+
}),
|
|
54
|
+
requireBuilder: (req, _res, next) => { req.builder = { id: 42, github_login: 'archon' }; next(); },
|
|
55
|
+
requirePermission: () => (_req, _res, next) => next(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const buildAccessRequestsRouter = require('../modules/onboarding/routes/access-requests.js');
|
|
59
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
60
|
+
|
|
61
|
+
const app = express();
|
|
62
|
+
app.use(express.json());
|
|
63
|
+
app.use(attachFail);
|
|
64
|
+
app.use('/api/gds', buildAccessRequestsRouter());
|
|
65
|
+
|
|
66
|
+
let server, port;
|
|
67
|
+
before(async () => {
|
|
68
|
+
server = http.createServer(app);
|
|
69
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
70
|
+
port = server.address().port;
|
|
71
|
+
});
|
|
72
|
+
after(async () => { await new Promise((r) => server.close(r)); });
|
|
73
|
+
|
|
74
|
+
function post(pathname, body) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const payload = JSON.stringify(body);
|
|
77
|
+
const req = http.request({
|
|
78
|
+
host: '127.0.0.1', port, path: pathname, method: 'POST',
|
|
79
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) },
|
|
80
|
+
}, (res) => {
|
|
81
|
+
let data = '';
|
|
82
|
+
res.on('data', (c) => { data += c; });
|
|
83
|
+
res.on('end', () => resolve({ status: res.statusCode, json: data ? JSON.parse(data) : null }));
|
|
84
|
+
});
|
|
85
|
+
req.on('error', reject);
|
|
86
|
+
req.write(payload);
|
|
87
|
+
req.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const none = { rows: [] };
|
|
92
|
+
const filed = [none, none, { rows: [{ id: 9, github_login: 'octocat', status: 'pending' }] }];
|
|
93
|
+
|
|
94
|
+
// Knock on the door as a stranger under one policy, and say what happened.
|
|
95
|
+
async function knock(next, { publishable: verdict } = {}) {
|
|
96
|
+
policy = next;
|
|
97
|
+
publishable = verdict;
|
|
98
|
+
script = [...filed];
|
|
99
|
+
calls = [];
|
|
100
|
+
const r = await post('/api/gds/access-requests', { github_login: 'octocat' });
|
|
101
|
+
return { status: r.status, code: r.json?.error?.code || null, filed: calls.length > 0, queries: calls.length };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const GRANTS = ['view', 'apply', 'full'];
|
|
105
|
+
const REFUSED = { invite_only: 403, open_enrollment: 409, not_published: 403 };
|
|
106
|
+
|
|
107
|
+
// ── public: the join grants exactly what the owner set ───────────────────────
|
|
108
|
+
|
|
109
|
+
test('public + grant full: the door runs whatever joinability runs — the default changes nothing', async () => {
|
|
110
|
+
assert.deepEqual(await knock({ visibility: 'public', joinGrant: 'full', joinability: 'apply' }),
|
|
111
|
+
{ status: 201, code: null, filed: true, queries: 3 });
|
|
112
|
+
assert.deepEqual(await knock({ visibility: 'public', joinGrant: 'full', joinability: 'open' }),
|
|
113
|
+
{ status: 409, code: 'open_enrollment', filed: false, queries: 0 });
|
|
114
|
+
assert.deepEqual(await knock({ visibility: 'public', joinGrant: 'full', joinability: 'invite_only' }),
|
|
115
|
+
{ status: 403, code: 'invite_only', filed: false, queries: 0 });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('public + grant apply: the application is filed even where enrolment is open', async () => {
|
|
119
|
+
const r = await knock({ visibility: 'public', joinGrant: 'apply', joinability: 'open' });
|
|
120
|
+
assert.deepEqual(r, { status: 201, code: null, filed: true, queries: 3 },
|
|
121
|
+
'the grant narrowed a self-join into a request — the knob nothing used to read');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('public + grant view: the project is there to be looked at; joining is by invite', async () => {
|
|
125
|
+
for (const joinability of ['open', 'apply', 'invite_only']) {
|
|
126
|
+
const r = await knock({ visibility: 'public', joinGrant: 'view', joinability });
|
|
127
|
+
assert.deepEqual(r, { status: 403, code: 'invite_only', filed: false, queries: 0 }, `joinability ${joinability}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── private: invite-only, with applications queued for the owner ─────────────
|
|
132
|
+
|
|
133
|
+
test('private: the application queues for the owner, and no grant re-opens a self-join', async () => {
|
|
134
|
+
for (const joinGrant of GRANTS) {
|
|
135
|
+
assert.deepEqual(await knock({ visibility: 'private', joinGrant, joinability: 'open' }),
|
|
136
|
+
{ status: 201, code: null, filed: true, queries: 3 }, `grant ${joinGrant} on an open-enrolment private project`);
|
|
137
|
+
assert.deepEqual(await knock({ visibility: 'private', joinGrant, joinability: 'apply' }),
|
|
138
|
+
{ status: 201, code: null, filed: true, queries: 3 }, `grant ${joinGrant}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('private + invite_only: the stricter knob still wins', async () => {
|
|
143
|
+
assert.deepEqual(await knock({ visibility: 'private', joinGrant: 'full', joinability: 'invite_only' }),
|
|
144
|
+
{ status: 403, code: 'invite_only', filed: false, queries: 0 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── stealth: no application is accepted, and none is stored ──────────────────
|
|
148
|
+
|
|
149
|
+
test('stealth: every combination is refused at the door, before any query', async () => {
|
|
150
|
+
for (const joinGrant of GRANTS) {
|
|
151
|
+
for (const joinability of ['open', 'apply', 'invite_only']) {
|
|
152
|
+
const r = await knock({ visibility: 'stealth', joinGrant, joinability });
|
|
153
|
+
assert.deepEqual(r, { status: 403, code: 'invite_only', filed: false, queries: 0 },
|
|
154
|
+
`stealth + ${joinGrant} + ${joinability}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('the refusal never says WHICH knob closed the door — stealth is indistinguishable from the other two', async () => {
|
|
160
|
+
const stealth = await knock({ visibility: 'stealth', joinGrant: 'full', joinability: 'apply' });
|
|
161
|
+
const viewOnly = await knock({ visibility: 'public', joinGrant: 'view', joinability: 'apply' });
|
|
162
|
+
const inviteOnly = await knock({ visibility: 'public', joinGrant: 'full', joinability: 'invite_only' });
|
|
163
|
+
assert.deepEqual(stealth, viewOnly, 'a stealth project answers exactly as a look-only public one');
|
|
164
|
+
assert.deepEqual(stealth, inviteOnly, 'and exactly as an ordinary invite-only one (ADR 0182 D2a)');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── dark matter ──────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
test('dark matter: locked outright, whatever the three stored knobs say', async () => {
|
|
170
|
+
for (const visibility of ['public', 'private', 'stealth']) {
|
|
171
|
+
const r = await knock({ visibility, joinGrant: 'full', joinability: 'open' }, { publishable: false });
|
|
172
|
+
assert.deepEqual(r, { status: 403, code: 'not_published', filed: false, queries: 0 }, visibility);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('the locked refusal names the PROJECT\'s state, not the applicant\'s — nothing they can retry', async () => {
|
|
177
|
+
policy = { visibility: 'public', joinGrant: 'full', joinability: 'apply' };
|
|
178
|
+
publishable = false;
|
|
179
|
+
script = [];
|
|
180
|
+
calls = [];
|
|
181
|
+
const r = await post('/api/gds/access-requests', { github_login: 'octocat' });
|
|
182
|
+
assert.equal(r.status, 403);
|
|
183
|
+
assert.match(r.json.error.message, /isn’t published yet/);
|
|
184
|
+
assert.doesNotMatch(r.json.error.message, /sign in|invite/i, 'it offers no way in, because there is none yet');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── the refusal costs nothing, and reveals nothing ───────────────────────────
|
|
188
|
+
|
|
189
|
+
test('every refusal answers before the body is even validated — no applicant lookup, no oracle', async () => {
|
|
190
|
+
for (const [code, status] of Object.entries(REFUSED)) {
|
|
191
|
+
const closed = code === 'not_published'
|
|
192
|
+
? { policy: { visibility: 'public', joinGrant: 'full', joinability: 'apply' }, verdict: false }
|
|
193
|
+
: code === 'open_enrollment'
|
|
194
|
+
? { policy: { visibility: 'public', joinGrant: 'full', joinability: 'open' }, verdict: undefined }
|
|
195
|
+
: { policy: { visibility: 'stealth', joinGrant: 'full', joinability: 'apply' }, verdict: undefined };
|
|
196
|
+
policy = closed.policy;
|
|
197
|
+
publishable = closed.verdict;
|
|
198
|
+
script = [];
|
|
199
|
+
calls = [];
|
|
200
|
+
// A body that would 400 on a working door: the refusal must come first.
|
|
201
|
+
const r = await post('/api/gds/access-requests', { github_login: 'not a login!' });
|
|
202
|
+
assert.equal(r.status, status, code);
|
|
203
|
+
assert.equal(r.json.error.code, code);
|
|
204
|
+
assert.equal(calls.length, 0, `${code} consulted the database`);
|
|
205
|
+
}
|
|
206
|
+
});
|