Files
RustProxy/tools/checks/dashboard_boot_0311_ui_test.mjs
T
2026-10-10 21:48:39 +02:00

90 lines
5.0 KiB
JavaScript

#!/usr/bin/env node
// Release-independent, real-source startup ordering and recovery contract.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const read = path => readFileSync(resolve(root, path), 'utf8');
const html = read('src/admin/web/index.html');
const runtime = read('src/admin/web/app/runtime.js');
const core = read('src/admin/web/app/core.js');
const router = read('src/admin/router.rs');
const bundle = read('tools/checks/web_assets.py');
const version = read('Cargo.toml').match(/^version\s*=\s*"([\d.]+)"/m)?.[1];
assert(version);
assert.equal((html.match(/<script defer="" src="\/assets\//g) || []).length, 1, 'one atomic dashboard script');
assert.match(html, /src="\/assets\/app\.js\?v=__RUSTPROXY_VERSION__"/);
assert.match(html, /href="\/assets\/style\.css\?v=__RUSTPROXY_VERSION__"/);
assert.match(router, /admin_vhosts_javascript\(\)/);
assert.match(bundle, /"integrations\.js", "vhosts\.js"/);
assert(router.indexOf('include_str!("web/integrations.js")') < router.indexOf('admin_vhosts_javascript(),'));
assert(router.indexOf('admin_vhosts_javascript(),') < router.indexOf('include_str!("web/host404.js")'));
const prefix = runtime.slice(0, runtime.indexOf('function certificateUsesExternalFiles'));
assert(prefix.includes('document.addEventListener("DOMContentLoaded", startAdminPanel'));
function simulate(complete) {
let bootstrap, calls = 0, activated = 0, failed = null;
const modules = {};
for (const [name, action] of [
['rustProxyIntegrations','bindings'], ['rustProxyVhosts','render'],
['rustProxySettings','render'], ['rustProxyCachePanel','refresh'],
['rustProxyTrafficPanel','refresh'], ['rustProxyPreferences','load'],
['rustProxyRefresh','start'],
]) if (complete || name !== 'rustProxyIntegrations') modules[name] = { [action]() {} };
const ctx = {
window: modules, document: { readyState: 'loading', addEventListener(name, fn) {
assert.equal(name, 'DOMContentLoaded'); bootstrap = fn;
} },
api: async () => { calls++; return { csrf_token: 'abc' }; },
clearInvalidOriginRecovery() {}, loadLanguages: async () => {},
translate() {}, syncAuthForm() {}, activate: async () => { activated++; },
recoverInvalidOrigin: () => false, showStartupFailure: e => { failed = e; },
location: { pathname: '/overview', search: '', replace() { throw new Error('unexpected redirect'); } },
};
runInNewContext(prefix, ctx);
assert.equal(calls, 0, 'zero API calls before all defer/module readiness');
assert.equal(activated, 0);
assert(bootstrap, 'registered DOMContentLoaded bootstrap');
return { bootstrap, state: () => ({ calls, activated, failed }) };
}
const success = simulate(true);
success.bootstrap();
for (let i = 0; i < 8; i++) await Promise.resolve();
assert.equal(success.state().calls, 1);
assert.equal(success.state().activated, 1);
assert.equal(success.state().failed, null);
const missing = simulate(false);
missing.bootstrap();
for (let i = 0; i < 8; i++) await Promise.resolve();
assert.equal(missing.state().calls, 0, 'do not initialize partially loaded modules');
assert.match(missing.state().failed.message, /rustProxyIntegrations/);
// Exercise real activate on temporary config API errors and on persistent failure.
const activation = core.slice(core.indexOf('async function activate(info) {'), core.indexOf('async function loadAudit()'));
assert(activation.startsWith('async function activate(info)'));
async function configRetry(failures) {
const nodes = new Map();
const node = key => { if (!nodes.has(key)) nodes.set(key, { hidden: true, value: '' }); return nodes.get(key); };
let config = 0, fallback = 0, status = 0;
const ctx = {
$: node, authenticated: false, csrf: '', authEpoch: 0, sessionInfo: null, pendingAuthDestination: null,
window: {}, document: { documentElement: { classList: { remove() {} } } },
authMessage() {}, renderSession() {}, rustProxyRefresh: { start() {} },
loadAdminPreferences: async () => ({}),
loadConfig: async () => { config++; if (config <= failures) throw new Error('Temporary proxy failure'); },
refresh: async () => { status++; },
setTimeout: fn => { fn(); return 1; },
showStartupFailure: () => { fallback++; },
toast() { throw new Error('A transient failure must not be hidden in a toast'); },
};
runInNewContext(activation, ctx);
await ctx.activate({ username: 'owner', csrf_token: 'token' });
for (let i=0;i<4;i++) await Promise.resolve();
return { config, fallback, status };
}
assert.deepEqual(await configRetry(2), { config: 3, fallback: 0, status: 1 });
assert.deepEqual(await configRetry(3), { config: 3, fallback: 1, status: 0 });
console.log(`PASS: ${version} one versioned JS payload, DOM/module-gated startup, retries and visible failure`);