128 lines
7.8 KiB
JavaScript
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']) {
|
|
has(css, `.stat-chart .stat-chart-legend .legend-${version} i { background: 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.31.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.31.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.0 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|