@bongos/core 1.19.570 → 1.19.572

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.
@@ -0,0 +1,141 @@
1
+ // tests/hall_landing_boot.mjs — the front door's boot order (task 1003673).
2
+ //
3
+ // Regression guard for the bug that locked every invite-only instance out of its
4
+ // own first builder: a signed-out visitor to /builders never saw the landing.
5
+ // builders.js mounted the hall widgets BEFORE /me resolved, a widget's own boot
6
+ // read 401'd, that read treated 401 as "go and sign in" and NAVIGATED — so the
7
+ // page bounced to GitHub, which refuses a first-timer with "request access
8
+ // first" and points them back at /builders to be bounced again. The request form
9
+ // sat behind that loop, unreachable, so the approval queue could never fill.
10
+ //
11
+ // The invariant this locks down: AUTH IS RESOLVED BEFORE THE HALL MOUNTS
12
+ // ANYTHING. A signed-out visitor gets the landing and nothing else; only a
13
+ // signed-in one mounts widgets. Executes the real builders.js in a minimal DOM
14
+ // stub (the hall_signout.mjs / hall_nav.mjs pattern) and drives DOMContentLoaded.
15
+ // Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
16
+ import assert from 'node:assert/strict';
17
+ import { test } from 'node:test';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import vm from 'node:vm';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/builders.js'), 'utf8');
25
+
26
+ // Boot builders.js against a /me that answers `meStatus`, then fire
27
+ // DOMContentLoaded and let the microtasks drain. Returns what the page DID.
28
+ async function boot({ meStatus }) {
29
+ const calls = { assign: [], requests: [], rendered: [] };
30
+
31
+ const makeEl = () => ({
32
+ dataset: {}, style: {}, title: '', textContent: '', hidden: false, value: '',
33
+ classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
34
+ setAttribute() {}, getAttribute() { return null; }, removeAttribute() {},
35
+ addEventListener() {}, querySelector() { return null; }, querySelectorAll() { return []; },
36
+ appendChild() {}, focus() {}, click() {}, closest() { return null; },
37
+ set innerHTML(_v) {}, get innerHTML() { return ''; },
38
+ });
39
+
40
+ const location = { pathname: '/builders', search: '', hash: '', href: 'https://x/builders', assign(u) { calls.assign.push(u); } };
41
+
42
+ const api = {
43
+ request(method, p) {
44
+ calls.requests.push(`${method} ${p}`);
45
+ const isMe = /\/me(\?|$)/.test(p);
46
+ const status = isMe ? meStatus : 200;
47
+ if (status === 401) return Promise.resolve({ status: 401, ok: false, data: null });
48
+ // A shape generic enough for the boot path; the renderers are spies below.
49
+ return Promise.resolve({ status: 200, ok: true, data: { builder: { id: '1', rank: 'metic' }, onboarding: null, tasks: [], goals: [], items: [] } });
50
+ },
51
+ };
52
+
53
+ const spy = (name) => (...args) => { calls.rendered.push(name); return args; };
54
+ const documentObj = {
55
+ body: { dataset: { page: 'home' } },
56
+ documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
57
+ getElementById() { return makeEl(); },
58
+ querySelector() { return makeEl(); },
59
+ querySelectorAll() { return []; },
60
+ createElement() { return makeEl(); },
61
+ listeners: {},
62
+ addEventListener(type, fn) { (this.listeners[type] || (this.listeners[type] = [])).push(fn); },
63
+ get cookie() { return ''; }, set cookie(_v) {},
64
+ };
65
+
66
+ const windowObj = {
67
+ __BRANDING__: { identity: { worldName: 'W', productName: 'W' }, currency: { label: 'credits' } },
68
+ __MODULES__: { enabled: {} },
69
+ BongosClient: { createClient: () => api },
70
+ OTB: {
71
+ moduleOn: () => true,
72
+ escapeHtml: (s) => String(s == null ? '' : s),
73
+ $: () => makeEl(),
74
+ fmtNum: (n) => String(n),
75
+ fmtDate: () => '',
76
+ rankLabel: (r) => String(r || '—'),
77
+ toast: () => {},
78
+ hallWidgets: () => [],
79
+ contributeHallWidget: () => {},
80
+ },
81
+ OTBKit: {
82
+ makePager: () => ({ page: 1, perPage: 10 }),
83
+ paginate: (rows) => rows,
84
+ wirePager: () => {},
85
+ },
86
+ OTBHallRender: () => ({
87
+ SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {}, onboardingBox: () => makeEl(),
88
+ shipState: {}, shipsPager: { page: 1 }, welcomeDismissalKey: () => 'k',
89
+ mountHallWidgets: spy('mountHallWidgets'),
90
+ renderLanding: spy('renderLanding'),
91
+ renderCurveBody: spy('renderCurveBody'), renderGreeting: spy('renderGreeting'),
92
+ renderNav: spy('renderNav'), renderOnboarding: spy('renderOnboarding'),
93
+ renderProfile: spy('renderProfile'), renderRecentShips: spy('renderRecentShips'),
94
+ renderWelcomeCore: spy('renderWelcomeCore'),
95
+ }),
96
+ matchMedia: () => ({ matches: false, addEventListener() {} }),
97
+ localStorage: { getItem() { return null; }, setItem() {}, removeItem() {} },
98
+ location,
99
+ addEventListener() {},
100
+ };
101
+
102
+ const sandbox = {
103
+ window: windowObj, document: documentObj, location, console,
104
+ setTimeout, clearTimeout, setInterval, clearInterval, fetch: () => Promise.reject(new Error('no network in this test')),
105
+ };
106
+ sandbox.globalThis = sandbox;
107
+ vm.createContext(sandbox);
108
+ vm.runInContext(SRC, sandbox, { filename: 'builders.js' });
109
+
110
+ for (const fn of documentObj.listeners.DOMContentLoaded || []) fn();
111
+ // Drain the boot promise chain (a handful of awaits, no timers).
112
+ for (let i = 0; i < 25; i += 1) await Promise.resolve();
113
+ return calls;
114
+ }
115
+
116
+ test('a signed-out visitor gets the landing — and is NEVER navigated away from it', async () => {
117
+ const calls = await boot({ meStatus: 401 });
118
+
119
+ assert.deepEqual(calls.assign, [],
120
+ 'the signed-out front door navigated somewhere. That is the dead end: GitHub refuses a '
121
+ + 'first-timer and sends them back here, so the request-access form is unreachable and '
122
+ + 'an invite-only instance can never admit anyone.');
123
+ assert.ok(calls.rendered.includes('renderLanding'), 'the landing (sign in / request access) never rendered');
124
+ assert.ok(!calls.rendered.includes('mountHallWidgets'),
125
+ 'widgets mounted for a signed-out visitor — each one boots its own authenticated read, '
126
+ + 'and any of those may navigate. The landing must be the whole page.');
127
+ });
128
+
129
+ test('auth is resolved BEFORE anything else is asked for', async () => {
130
+ const calls = await boot({ meStatus: 401 });
131
+ assert.equal(calls.requests.length, 1, `the page asked for more than /me while signed out: ${calls.requests.join(', ')}`);
132
+ assert.match(calls.requests[0], /\/me$/);
133
+ });
134
+
135
+ test('a signed-in builder still gets the hall', async () => {
136
+ const calls = await boot({ meStatus: 200 });
137
+ assert.ok(calls.rendered.includes('mountHallWidgets'), 'the hall did not mount for a signed-in builder');
138
+ assert.deepEqual(calls.assign, [], 'a signed-in boot navigated away');
139
+ assert.equal(calls.requests.filter((r) => /\/me$/.test(r)).length, 1,
140
+ 'boot asked for /me more than once — the resolved payload is handed to loadAll, not re-fetched');
141
+ });