Files
RustProxy/tools/checks/system_info_0301_ui_test.mjs
T
2026-10-13 00:00:06 +02:00

128 lines
7.8 KiB
JavaScript

#!/usr/bin/env node
// Simulated status and tab DOM. No Rust process or visual browser is run here.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const html = fs.readFileSync('src/admin/web/index.html', 'utf8');
const css = fs.readFileSync('src/admin/web/style.css', 'utf8');
const js = fs.readFileSync('src/admin/web/settings.js', 'utf8');
const core = fs.readFileSync('src/admin/web/app/core.js', 'utf8');
const api = JSON.parse(fs.readFileSync('src/admin/openapi.json', 'utf8'));
const has = (haystack, needle) => assert.ok(haystack.includes(needle), `Missing ${needle}`);
// The two mini charts share the exact H1/H2/H3 palette as their SVG lines.
for (const version of ['h1', 'h2', 'h3']) {
assert.match(css, new RegExp(`\\.stat-chart\\s+\\.stat-chart-legend\\s+\\.legend-${version}\\s+i\\s*\\{\\s*background:\\s*var\\(--traffic-${version}\\);`));
assert.match(css, new RegExp(`\\.proto-${version}-line\\s*\\{[^}]*stroke:\\s*var\\(--traffic-${version}\\);`));
has(html, `id="http-protocol-${version}"`);
has(html, `id="ws-protocol-${version}"`);
}
assert.equal((html.match(/class="stat-chart-legend"/g) || []).length, 2);
const about = html.split('<dialog aria-labelledby="about-title"')[1].split('</dialog>')[0];
for (const forbidden of ['about-acme-mode', 'about-python-runtime', 'about-python-details', 'runtimeAcmeMode']) {
assert.ok(!about.includes(forbidden), `ACME details left in About: ${forbidden}`);
}
assert.ok(!core.includes('renderAboutRuntime('), 'Obsolete About runtime renderer must be removed');
for (const marker of ['aboutAuthor', 'aboutLicense', 'aboutRepository', 'aboutPackagesRepository']) has(about, marker);
has(html, 'data-settings-tab="system-info"');
has(html, 'id="settings-page-system-info"');
const tab = html.split('id="settings-page-system-info"')[1].split('id="settings-page-certificate-api"')[0];
for (const field of ['runtime-operating-system', 'runtime-kernel-version', 'runtime-architecture',
'runtime-logical-cpus', 'runtime-system-memory', 'runtime-process-memory',
'runtime-acme-mode', 'runtime-python', 'runtime-pip', 'runtime-certbot',
'runtime-acme-directory', 'runtime-data-directory', 'runtime-database-path']) has(tab, `id="${field}"`);
assert.ok(!tab.includes('<form'), 'The System information page must be read-only');
const admin = html.split('id="settings-page-administration"')[1].split('id="settings-page-system-info"')[0];
assert.ok(!admin.includes('runtime-information-card'), 'Runtime data must be relocated, not duplicated');
// Execute the real status-to-settings rendering code against a synthetic status payload.
const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {};'), js.indexOf(' const policy = envelope.config.tls?', js.indexOf(' const runtime = latestStatus?.runtime || {};')));
const elements = new Map();
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
const status = {
version: '0.32.0', uptime_seconds: 4201,
runtime: {
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
certificate_runtime: { status: 'ready', mode: 'managed', message: 'acme.runtime_ready',
python: true, pip: true, certbot: true, python_version: '3.13.5', pip_version: '25.2',
certbot_version: '5.8.0', managed_directory: '/var/lib/rustproxy/acme-python' }
},
system: { logical_cpus: 8, cpu_percent: 17.2, memory_total_bytes: 16_384, memory_used_bytes: 8_192,
process_rss_bytes: 2_048, system_uptime_seconds: 100, process_threads: 9, open_fds: 67 }
};
const context = {
latestStatus: status, $: dollar, locale: () => 'en-US',
t: key => ({ runtimeAvailable: 'Available', runtimeUnavailable: 'Unavailable',
certificateRuntimeManaged: 'App-managed Python', 'acme.runtime_ready': 'Ready', unavailable: 'N/A' })[key] || key,
bytes: value => `${value} B`, uptime: value => `${value}s`,
};
vm.runInNewContext(code, context);
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
assert.equal(dollar('runtime-version').textContent, '0.32.0');
assert.equal(dollar('runtime-logical-cpus').textContent, '8');
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
assert.equal(dollar('runtime-pip').textContent, 'Available · 25.2');
assert.equal(dollar('runtime-certbot').textContent, 'Available · 5.8.0');
assert.equal(dollar('runtime-acme-directory').textContent, '/var/lib/rustproxy/acme-python');
// A working ACME runtime can have no trustworthy version metadata on some OSes.
elements.clear();
vm.runInNewContext(code, { ...context,
latestStatus: { ...status, runtime: { ...status.runtime, certificate_runtime: {
...status.runtime.certificate_runtime, python_version: null, pip_version: null, certbot_version: null
} } }
});
for (const field of ['runtime-python', 'runtime-pip', 'runtime-certbot']) {
assert.equal(dollar(field).textContent, 'Available');
}
// Status fields remain safely unavailable rather than displaying fake zeros.
elements.clear();
vm.runInNewContext(code, { ...context,
latestStatus: { runtime: { certificate_runtime: { status: 'unavailable', mode: 'none',
message: 'acme.runtime_python_missing', python: false, pip: false, certbot: false } }, system: {} }
});
for (const field of ['runtime-cpu-usage', 'runtime-logical-cpus', 'runtime-system-memory', 'runtime-python']) {
assert.ok(!/NaN|0 B/.test(dollar(field).textContent));
}
// Exercise real tab switching, deep-linking and search code without a browser.
const entry = js.slice(js.indexOf(' const buttons ='), js.indexOf(' const cacheTabs ='));
const buttons = ['administration', 'system-info', 'certificate-api'].map(name => ({
dataset: {settingsTab:name}, textContent:name, hidden:false, tabIndex:-1,
listeners:{}, setAttribute(name,value){this[name]=value;}, addEventListener(name,fn){this.listeners[name]=fn;}, focus(){} }));
const pages = buttons.map(b => ({dataset:{settingsPage:b.dataset.settingsTab}, hidden:true,textContent:b.dataset.settingsTab}));
const search={value:'',listeners:{},addEventListener(name,fn){this.listeners[name]=fn;}};
const searchStatus={textContent:''};
const loc = new URL('https://localhost/settings?tab=system-info');
let lastUrl='';
vm.runInNewContext(`(() => { ${entry} return true; })()`, {
document: {querySelectorAll: sel => sel === '[data-settings-tab]' ? buttons : pages,
querySelector: sel => pages.find(p => sel.includes(p.dataset.settingsPage))},
$: id => id === 'settings-search' ? search : searchStatus,
location: loc, history: {state:null,replaceState:(_state,_unused,url) => {lastUrl=url;}},
CSS:{escape:x=>x}, URL, URLSearchParams, t:x=>x,
});
buttons[1].listeners.click();
assert.equal(pages[1].hidden,false);
assert.equal(pages[0].hidden,true);
assert.equal(buttons[1]['aria-selected'],'true');
assert.equal(lastUrl,''); // Already at canonical deep link; no unnecessary history rewrite.
buttons[0].listeners.click();
assert.equal(pages[1].hidden,true);
assert.equal(lastUrl,'/settings?tab=administration');
const runtimeSchema = api.components.schemas.RuntimeInformation;
for (const key of ['operating_system','kernel_version','architecture']) assert.ok(runtimeSchema.properties[key]);
for (const key of ['python_version','pip_version','certbot_version']) {
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
}
console.log('PASS: 0.31.9 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');