0.34.6_repo_cleanup

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 09:31:15 +02:00
1 parent ce1a90397c
commit 133d2f0f91
273 files changed
+5969 -6926

No files matched your search

-24
View File
@@ -1,24 +0,0 @@
// Run with: node tests/acl_picker_corner_styles.cjs
// The shared ACL picker must respect all dashboard corner-style presets without
// relying on a separate radius setting or changing its selection behaviour.
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const css = fs.readFileSync(path.resolve(__dirname, "../src/admin/web/style.css"), "utf8");
const parts = [
".acl-picker-details", ".acl-picker-counter", ".acl-picker-option",
".acl-picker-policy", ".acl-picker-tag", ".acl-picker-tag .acl-picker-remove"
];
for (const selector of parts) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const block = css.match(new RegExp(`${escaped}\\s*\\{([^}]+)\\}`));
assert.ok(block, `Missing shared picker style: ${selector}`);
assert.match(block[1], /border-radius:\s*var\(--rp-radius-control,\s*[^)]+\);/, `${selector} must use the dashboard control radius`);
}
for (const [preset, radius] of Object.entries({ square: 0, subtle: 4, rounded: 8, extra: 12 })) {
const rule = css.match(new RegExp(`html\\[data-corner-style=${preset}\\]\\s*\\{([^}]+)\\}`));
assert.ok(rule, `Missing corner-style preset: ${preset}`);
assert.match(rule[1], new RegExp(`--rp-radius-control:\\s*${radius}px;`), `Unexpected radius for ${preset}`);
}
console.log("ACL picker corner styles: PASS (six surfaces, four presets, Original fallbacks)");
-72
View File
@@ -1,72 +0,0 @@
// Run with: node tests/acl_picker_dom.cjs. Tiny DOM stub for picker state/regression.
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const vm = require("node:vm");
const path = require("node:path");
class DomEvent {
constructor(type, options = {}) { this.type = type; this.bubbles = !!options.bubbles; this.target = null; }
}
class DomNode {
constructor(tag) { this.tag = tag; this.className = ""; this.children = []; this.parent = null; this.dataset = {}; this.handlers = new Map(); this.hidden = false; this.disabled = false; this.checked = false; this.value = ""; this.type = ""; this._text = ""; }
get textContent() { return this._text + this.children.map(c => c.textContent).join(""); }
set textContent(text) { this.replaceChildren(); this._text = String(text); }
append(...children) { for (const child of children) { child.parent = this; this.children.push(child); } }
replaceChildren(...children) { for (const child of this.children) child.parent = null; this.children = []; this._text = ""; this.append(...children); }
setAttribute() { }
focus() { }
addEventListener(type, handler) { if (!this.handlers.has(type)) this.handlers.set(type, []); this.handlers.get(type).push(handler); }
dispatchEvent(event) { if (!event.target) event.target = this; for (const handler of this.handlers.get(event.type) || []) handler(event); if (event.bubbles && this.parent) this.parent.dispatchEvent(event); }
click() { this.dispatchEvent(new DomEvent("click", {bubbles:true})); }
hasClass(name) { return this.className.split(/\s+/).includes(name); }
matches(selector) { return selector === 'input[type="checkbox"]' && this.tag === "input" && this.type === "checkbox"; }
all() { return this.children.flatMap(child => [child, ...child.all()]); }
querySelectorAll(selector) {
const descendants = this.all();
if (selector === '.acl-picker-option input[type="checkbox"]:checked') return descendants.filter(n => n.tag === "input" && n.type === "checkbox" && n.checked && n.parent?.hasClass("acl-picker-option"));
if (selector === '.acl-picker-option input[type="checkbox"]') return descendants.filter(n => n.tag === "input" && n.type === "checkbox" && n.parent?.hasClass("acl-picker-option"));
const value = selector.match(/^\.acl-picker-option input\[value="([^"]+)"\]$/);
if (value) return descendants.filter(n => n.tag === "input" && n.value === value[1] && n.parent?.hasClass("acl-picker-option"));
const cls = selector.match(/^\.([\w-]+)$/);
if (cls) return descendants.filter(n => n.hasClass(cls[1]));
throw Error("Unsupported test selector " + selector);
}
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
}
const profiles = Array.from({length:10}, (_,i) => ({
id: `profile_${i}`, name: i === 0 ? "Office networks" : `Profile ${i}`,
acl: {default: "deny", rules:[{network:`10.${i}.0.0/16`,action:"allow"},{network:"192.0.2.0/24",action:"deny"}]},
country_access: {mode: i === 0 ? "block" : "off", countries: i === 0 ? ["RU"] : []}
}));
const context = {
window: {},
envelope: {config:{acl_profiles:profiles}},
el(tag, text, cls) { const node = new DomNode(tag); if (text !== undefined) node.textContent = text; if (cls) node.className = cls; return node; },
t: key => key,
Event: DomEvent,
};
vm.createContext(context);
vm.runInContext(fs.readFileSync(path.resolve(__dirname, "../src/admin/web/acl-picker.js"), "utf8"), context);
const picker = context.window.rustProxyAclPicker;
const root = picker.create(["profile_0", "profile_1"], "vh-acl-profile-choices");
assert.deepEqual(Array.from(picker.read(root)), ["profile_0", "profile_1"]);
assert.equal(root.querySelectorAll(".acl-picker-tag").length, 2);
assert.match(root.textContent, /10\.0\.0\.0\/16/);
assert.match(root.textContent, /RU/);
const search = root.querySelector(".acl-picker-search");
search.value = "Office"; search.dispatchEvent(new DomEvent("input", {bubbles:true}));
assert.equal(root.querySelectorAll(".acl-picker-option").filter(node => !node.hidden).length, 1);
search.value = ""; search.dispatchEvent(new DomEvent("input", {bubbles:true}));
root.querySelector(".acl-picker-remove").click();
assert.deepEqual(Array.from(picker.read(root)), ["profile_1"]);
for (const check of root.querySelectorAll('.acl-picker-option input[type="checkbox"]').slice(2,9)) {
check.checked = true;
check.dispatchEvent(new DomEvent("change", {bubbles:true}));
}
assert.equal(picker.count(root), 8);
assert.equal(root.querySelector('.acl-picker-option input[value="profile_9"]').disabled, true);
root.querySelector(".acl-picker-remove").click();
assert.equal(root.querySelector('.acl-picker-option input[value="profile_9"]').disabled, false);
const docker = picker.create([], "docker-acl-profile");
assert.deepEqual(Array.from(picker.read(docker)), []); // empty override inherits agent profiles
console.log("ACL picker DOM regression: PASS (choices, preview, search, badges, limit and inheritance)");
-96
View File
@@ -1,96 +0,0 @@
"use strict";
// Zero-dependency browser calculation regressions (run: node tests/cache_outcomes.cjs).
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const root = path.join(__dirname, "..");
const src = fs.readFileSync(path.join(root, "src/admin/web/app/runtime.js"), "utf8");
const begin = src.indexOf("function cacheOutcomeMix(totals) {");
const renderBegin = src.indexOf("\nfunction renderCacheMix(mix, totalRequests) {", begin);
const end = src.indexOf("\nfunction renderDiagnostics(", renderBegin);
assert.ok(begin >= 0 && renderBegin > begin && end > renderBegin, "production cumulative widget functions exist");
const mix = vm.runInNewContext(`${src.slice(begin, renderBegin)}; cacheOutcomeMix;`);
const result = mix([10, 4, 2, 3]);
assert.deepEqual(Array.from(result.counts), [4, 2, 3, 1]);
assert.deepEqual(Array.from(result.percentages), [40, 20, 30, 10]);
assert.equal(result.requests, 10);
assert.deepEqual(Array.from(mix([12, 0, 0, 0]).counts), [0, 0, 0, 12]);
assert.deepEqual(Array.from(mix([9, 0, 0, 9]).counts), [0, 0, 9, 0]);
for (const invalid of [[0, 0, 0, 0], [1, 2, 0, 0], [10, -1, 0, 0], [NaN, 0, 0, 0],
[10.1, 2, 0, 0], [Number.MAX_SAFE_INTEGER + 1, 0, 0, 0], [], null]) {
assert.equal(mix(invalid), null, `invalid lifetime counters ${invalid} are not charted`);
}
// Ensure the panel uses all-time counters straight from the status payload, on every render.
const diagnostic = src.slice(end, src.indexOf("\nfunction ", end + 1));
assert.match(diagnostic, /const counters = \[m\.requests, c\.hits, c\.misses, c\.bypasses\]/);
assert.match(diagnostic, /renderCacheMix\(cacheOutcomeMix\(counters\), counters\[0\]\);/);
assert.doesNotMatch(src, /cacheMixLastCounters|cacheMixLastShares|cacheMixLastTotal/);
const translations = ["en_US", "pl_pl"].map(language =>
JSON.parse(fs.readFileSync(path.join(root, `langs/${language}.json`), "utf8")).translations);
const nodes = new Map();
const dom = id => {
if (!nodes.has(id)) {
const node = { style: {}, textContent: "", attributes: {}, setAttribute(name, value) { this.attributes[name] = value; } };
nodes.set(id, node);
}
return nodes.get(id);
};
const english = translations[0];
const render = vm.runInNewContext(`${src.slice(renderBegin + 1, end)}; renderCacheMix;`, {
$: dom,
t: key => english[key],
fmt: (value, digits = 0) => Number(value).toFixed(digits),
});
render(result, 10);
assert.equal(dom("cache-mix-headline").textContent, "40.0% HIT");
assert.equal(dom("cache-mix-caption").textContent, "10");
assert.equal(dom("cache-mix-caption").attributes.title, "Since service start · 10 HTTP requests");
assert.equal(dom("cache-mix-caption").attributes["aria-label"], "Since service start · 10 HTTP requests");
for (const [name, count, percent] of [["hit", 4, 40], ["miss", 2, 20],
["bypass", 3, 30], ["other", 1, 10]]) {
assert.equal(dom(`cache-mix-${name}-count`).textContent, String(count));
assert.equal(dom(`cache-mix-${name}-percent`).textContent, `${percent.toFixed(1)}%`);
assert.equal(dom(`cache-mix-${name}`).style.width, `${percent}%`);
}
assert.match(dom("cache-mix-bar").attributes["aria-label"], /4 HIT, 2 MISS, 3 BYPASS, 1 OTHER/);
render(null, 0);
assert.equal(dom("cache-mix-caption").textContent, "0");
assert.equal(dom("cache-mix-caption").attributes.title, "Since service start · 0 HTTP requests");
assert.equal(dom("cache-mix-hit-count").textContent, "-");
assert.equal(dom("cache-mix-hit").style.width, "0%");
render(null, 10);
assert.equal(dom("cache-mix-caption").textContent, "10");
assert.equal(dom("cache-mix-caption").attributes.title, "Since service start · 10 HTTP requests");
assert.equal(dom("cache-mix-bar").attributes["aria-label"], english.cacheMixInvalid);
render(null, NaN);
assert.equal(dom("cache-mix-caption").textContent, "-");
assert.equal(dom("cache-mix-caption").attributes.title, english.cacheMixInvalid);
const widgets = JSON.parse(fs.readFileSync(path.join(root, "src/admin/web/dashboard-widgets.json"), "utf8"));
const defaults = JSON.parse(fs.readFileSync(path.join(root, "src/admin/web/dashboard-defaults.json"), "utf8"));
assert.equal(widgets.filter(widget => widget.id === "cache_outcomes").length, 1);
assert.equal(defaults.dashboard_widgets.filter(id => id === "cache_outcomes").length, 1);
assert.equal(widgets.find(widget => widget.id === "cache_outcomes").default_hidden, true, "cache outcomes is opt-in");
assert.equal(defaults.dashboard_hidden_widgets.filter(id => id === "cache_outcomes").length, 1, "hidden in factory layout");
const html = fs.readFileSync(path.join(root, "src/admin/web/partials/overview-metrics.html"), "utf8");
assert.match(html, /data-dashboard-widget="cache_outcomes"/);
assert.match(html, /class="cache-mix-header">\s*<span data-i18n="cacheMixTitle">[^<]+<\/span>\s*<small class="cache-mix-caption" id="cache-mix-caption"/);
assert.equal((html.match(/id="cache-mix-caption"/g) || []).length, 1, "counter shown only in top-right header");
const css = fs.readFileSync(path.join(root, "src/admin/web/style.css"), "utf8");
assert.match(css, /\.stat-cache-outcomes \.cache-mix-header \{\s*display: flex;/);
assert.match(css, /\.stat-cache-outcomes \.cache-mix-caption \{[^}]*text-align: right;/);
for (const key of ["hit", "miss", "bypass", "other"]) {
assert.match(html, new RegExp(`id="cache-mix-${key}"`));
assert.match(html, new RegExp(`id="cache-mix-${key}-count"`));
assert.match(html, new RegExp(`id="cache-mix-${key}-percent"`));
}
for (const translation of translations) {
for (const key of ["cacheMixTitle", "cacheMixHelp", "cacheMixOther", "cacheMixSample", "cacheMixWaiting", "cacheMixInvalid", "cacheMixAria",
"h3_udp_receive_buffer_mib", "h3_udp_send_buffer_mib", "http3ApplyPolicy"]) {
assert.ok(translation[key], `translation missing: ${key}`);
}
assert.doesNotMatch(translation.cacheMixTitle, /recent|ostatni pomiar/i);
}
console.log("Cumulative cache outcomes, manifest and locale regression checks passed.");
-47
View File
@@ -1,47 +0,0 @@
// Run with: node tests/css_usage_audit.cjs
// Ensure the review archive stays unreferenced and critical live styles survive.
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const root = path.resolve(__dirname, '..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
const active = read('src/admin/web/style.css');
// Review-only archives are intentionally optional in reproducible source ZIPs.
const unused = fs.existsSync(path.join(root, 'src/admin/web/unused.css')) ? read('src/admin/web/unused.css') : null;
const docs = read('src/admin/web/docs.css');
const archived = [
'auth-ip-card', 'certificate-dialog', 'config-columns', 'overview-grid',
'overview-inline-hidden-toggle', 'overview-layout-body', 'overview-layout-filters',
'overview-layout-grid', 'overview-layout-hidden-list', 'overview-layout-section-heading',
'settings-grid', 'stream-dashboard-card', 'table-container', 'transport-grid', 'www-service-heading',
];
for (const name of archived) {
const cssClass = new RegExp(`\\.${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![-\\w])`);
assert.ok(!cssClass.test(active), `${name} must not remain in the active stylesheet`);
if (unused !== null) assert.ok(cssClass.test(unused), `${name} must be reviewable in unused.css`);
}
for (const name of ['vh-setting-section', 'dashboard-overview-grid', 'vh-override-fields',
'refresh-settings-grid', 'docker-backend-ip-settings', 'acl-profile-layout']) {
assert.ok(active.includes(`.${name}`), `active class ${name} was removed`);
}
for (const file of [
'src/admin/web/index.html', 'src/admin/web/login.html', 'src/admin/web/docs.html',
'src/admin/panel_assets.rs', 'src/admin/documentation.rs',
]) assert.ok(!read(file).includes('unused.css'), `${file} must not load or serve unused.css`);
assert.ok(!/@import[^;]*unused\.css/.test(active));
if (unused !== null) assert.match(unused, /NOT INCLUDED by the admin panel/);
assert.match(docs, /\.swagger-ui \.opblock/);
for (const style of ['square', 'subtle', 'rounded', 'extra']) {
assert.ok(active.includes(`html[data-corner-style=${style}]`));
assert.ok(docs.includes(`html[data-corner-style=${style}]`));
}
for (const name of ['app-startup-spin', 'app-startup-reveal']) {
assert.ok(active.includes(`@keyframes ${name}`));
if (unused !== null) assert.ok(!unused.includes(`@keyframes ${name}`));
}
assert.ok(!active.includes('--backdrop-soft'));
assert.ok(!active.includes('var(--panel-strong)'));
assert.ok(!active.includes('var(--border)'));
// The review-only CSS history is not a runtime or packaging dependency.
console.log('CSS active/optional archive isolation and corner-style regression: PASS');
-187
View File
@@ -1,187 +0,0 @@
// Isolated DOM regression for per-Docker-vhost live proxy logs and Publish switch.
// No native RustProxy process or browser is required for this check.
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const read = name => fs.readFileSync(path.join(root, name), 'utf8');
const docker = read('src/admin/web/docker.js');
const css = read('src/admin/web/style.css');
const management = read('src/admin/web/management.js');
const en = JSON.parse(read('langs/en_US.json')).translations;
const pl = JSON.parse(read('langs/pl_pl.json')).translations;
for (const pack of [en, pl]) {
assert.ok(pack.dockerPublish && pack.liveLogs && pack.dockerLiveAccessLogsHelp);
}
assert.match(docker, /publish\.setAttribute\("role", "switch"\)/);
assert.match(css, /\.docker-publish-field \.docker-publish:checked::before/);
assert.match(css, /\.docker-publish-field \.docker-publish:focus-visible/);
assert.match(css, /\.docker-service-quick-actions/);
assert.match(management, /if \(selected\) ids\.add\(selected\)/);
assert.match(read('Cargo.toml'), /^version = "0\.34\.6"$/m);
assert.match(read('Cargo.lock'), /name = "rust-proxy"\nversion = "0\.34\.6"/);
class Node {
constructor(tag) {
this.tag = tag; this.children = []; this.parentElement = null; this.dataset = {};
this.className = ''; this.listeners = {}; this.attributes = {};
this.open = false; this.hidden = false; this.disabled = false;
this.checked = false; this.value = ''; this._text = ''; this.isConnected = true;
const classes = new Set();
this.classList = {
add: token => classes.add(token), remove: token => classes.delete(token),
toggle: (token, state) => { const wanted = state === undefined ? !classes.has(token) : state; if (wanted) classes.add(token); else classes.delete(token); return wanted; },
contains: token => classes.has(token),
};
}
append(...children) { this.children.push(...children); children.forEach(child => { if (child instanceof Node) child.parentElement = this; }); }
replaceChildren(...children) { this.children = []; this._text = ''; this.append(...children); }
get textContent() { return this._text + this.children.map(child => child.textContent ?? String(child)).join(''); }
set textContent(value) { this.replaceChildren(); this._text = String(value); }
setAttribute(key, value) { this.attributes[key] = String(value); }
getAttribute(key) { return this.attributes[key]; }
addEventListener(name, fn) { (this.listeners[name] ||= []).push(fn); }
async emit(name) { for (const fn of this.listeners[name] || []) await fn({target:this}); }
click() { return this.emit('click'); }
}
const controls = new Map();
const visits = [], saves = [];
let saveResult = true;
const context = {
window: {rustProxyLogs: {open(id) { visits.push(id); }}, rustProxyAclPicker: {}, rustProxyPreferences: {}},
document: {createElementNS: (_ns,tag) => new Node(tag), querySelectorAll: () => []},
$: id => controls.get(id) || null,
el(tag, content, cls) { const node = new Node(tag); if (content !== undefined) node.textContent = content; if (cls) node.className = cls; return node; },
t: key => en[key] || key,
locale: () => 'en-US',
localStorage: {getItem:() => null},
rustProxyRefresh: {register(){}, sync(){}},
envelope: {config: {docker_agents:{agents:[]}, listeners:{endpoints:[
{id:'http', enabled:true, binding:{address:'0.0.0.0:80',tls:false}},
{id:'https', enabled:true, binding:{address:'0.0.0.0:8443',tls:true}},
]}}},
latestStatus:null, performance:{now:()=>0}, authenticated:false, currentPanel:'overview',
toast(message) { throw new Error('Unexpected toast: ' + message); },
};
vm.createContext(context);
assert.ok(docker.trimEnd().endsWith('})();'));
const injected = docker.trimEnd().slice(0, -5) + `
// Replace the unrelated configuration editors and persistence with controlled stubs.
aliasEditor = () => ({node:el('div'), input:el('input'), read:()=>[], count:()=>0});
targetOptions = () => [];
aclEditor = timeoutEditor = healthEditor = passiveHealthEditor = () => el('details');
saveAssignment = async (agentId, row, enabled) => window._saveAssignment(agentId, row, enabled);
window._dockerActions = {assignmentRow, proxiedUrl, setRoutes(routes) { poolRoutes = routes; }};
})();`;
context.window._saveAssignment = async (agentId, row, published) => { saves.push([agentId,row.dataset.key,published]); return saveResult; };
vm.runInContext(injected, context);
const actions = context.window._dockerActions;
const agent = {id:'docker-1',name:'Docker 1',base_domain:'example.test',listener_ids:['http','https'],enabled:true};
const service = {key:'container:app:8080/tcp',technical_name:'app-8080',container_name:'app',image:'myapp:v1',private_port:8080,bindings:[{bind_ip:'0.0.0.0',public_port:8080}]};
const assignment = {service_key:service.key,hostname:'app',enabled:true};
const logId = 'docker-agent-41a1b2c3d4e5';
actions.setRoutes([{agent_id:agent.id,service_key:service.key,pool_id:logId}]);
const row = actions.assignmentRow(agent,null,service,assignment,false);
const [head,editor,metrics] = row.children;
assert.equal(editor.tag,'details'); assert.equal(metrics.tag,'details', 'published route retains collapsed metrics');
const [publishLabel,,headActions] = head.children;
const publish = publishLabel.children[1];
assert.equal(publishLabel.className,'docker-publish-field');
assert.equal(publish.getAttribute('role'),'switch');
assert.equal(publish.checked,true);
assert.equal(publish.getAttribute('aria-checked'),'true');
const [runtime,quickActions] = headActions.children;
assert.equal(runtime.className, 'docker-runtime');
const [openApp,openLogs] = quickActions.children;
assert.equal(openApp.textContent,en.dockerOpenApp);
assert.equal(openApp.href,'https://app.example.test:8443/', 'Open app honors nested listener TLS and port');
assert.equal(openLogs.textContent,en.liveLogs);
assert.equal(openLogs.title,en.dockerLiveAccessLogsHelp);
assert.equal(openLogs.disabled,false);
assert.equal(openLogs.hidden,false);
function testLiveLogSelection() {
// Execute the actual live log dialog controller independently from other admin modules.
const start = management.indexOf('(() => {\n const dialog = $("logs-dialog");');
assert.ok(start > 0);
const end = management.indexOf('\n})();', start) + '\n})();'.length;
const logController = management.slice(start, end);
class LogNode extends Node {
constructor(tag) { super(tag); this.value = ''; }
replaceChildren(...children) {
super.replaceChildren(...children);
if (this.tag === 'select') this.value = this.children[0]?.value || '';
}
querySelectorAll() { return []; }
show() { this.open = true; }
showModal() { this.open = true; }
close() { this.open = false; }
}
const logControls = new Map();
const $ = id => {
if (!logControls.has(id)) {
const tag = ['log-vhost','log-method','log-status','log-outcome'].includes(id) ? 'select' : 'div';
logControls.set(id, new LogNode(tag));
}
return logControls.get(id);
};
$('log-outcome').value = 'all';
const logContext = {
document:{createElement:tag => new LogNode(tag)},
window:{rustProxyPreferences:{load:async()=>({live_log_columns:['time','vhost']})}},
$, t:key => en[key] || key, el(tag,value,cls) {const node=new LogNode(tag); if(value!==undefined) node.textContent=value; if(cls)node.className=cls; return node;},
authenticated:true, envelope:{config:{vhosts:[{id:'ordinary-vhost'}]}}, lastLogs:{entries:[]},
formatTime:time=>String(time), toast(){throw Error('unexpected toast')},
};
vm.createContext(logContext);
vm.runInContext(logController,logContext);
logContext.window.rustProxyLogs.open('docker-agent-41a1b2c3d4e5');
assert.equal($('log-vhost').value, 'docker-agent-41a1b2c3d4e5');
const entry = (vhost) => ({vhost,time_ms:123,method:'GET',path:'/',host:'app.example.test',status:200,cache:'MISS',outcome:'ok'});
// A refresh containing only requests from another Vhost must not remove the chosen ID.
logContext.lastLogs = {entries:[entry('ordinary-vhost')]};
logContext.window.rustProxyLogs.render();
assert.equal($('log-vhost').value,'docker-agent-41a1b2c3d4e5');
assert.match($('log-count').textContent, /^0 \/ 1/);
logContext.lastLogs = {entries:[entry('ordinary-vhost'),entry('docker-agent-41a1b2c3d4e5')]};
logContext.window.rustProxyLogs.render();
assert.match($('log-count').textContent, /^1 \/ 2/);
assert.equal($('log-vhost').value,'docker-agent-41a1b2c3d4e5');
$('log-vhost').value = '';
logContext.window.rustProxyLogs.render();
assert.match($('log-count').textContent, /^2 \/ 2/, 'manual All vhosts selection still works');
}
(async () => {
testLiveLogSelection();
await openLogs.click();
assert.deepEqual(visits,[logId], 'logs open for the returned generated ID, not the display hostname');
publish.checked = false;
await publish.emit('change');
assert.deepEqual(saves.at(-1),[agent.id,service.key,false]);
assert.equal(publish.getAttribute('aria-checked'),'false');
assert.equal(openLogs.hidden,true);
assert.equal(openApp.hidden,true);
assert.equal(publish.disabled,false);
publish.checked = true;
await publish.emit('change');
assert.deepEqual(saves.at(-1),[agent.id,service.key,true]);
assert.equal(openLogs.hidden,false);
assert.equal(openApp.hidden,false);
assert.equal(publish.getAttribute('aria-checked'),'true');
saveResult = false;
publish.checked = false;
await publish.emit('change');
assert.equal(publish.checked,true, 'failed unpublish reverts state');
assert.equal(publish.getAttribute('aria-checked'),'true', 'ARIA state rolls back');
assert.equal(openLogs.hidden,false, 'related action rolls back');
saveResult = true;
actions.setRoutes([]);
const missingRoute = actions.assignmentRow(agent,null,service,assignment,false);
const missingLogs = missingRoute.children[0].children[2].children[1].children[1];
assert.equal(missingLogs.disabled,true, 'no generated route ID disables logs rather than showing all vhosts');
await missingLogs.click();
assert.deepEqual(visits,[logId], 'missing ID never opens unfiltered logs');
console.log('Docker publish switch, rollback, live log targeting, app links: PASS');
})().catch(error => { console.error(error); process.exitCode = 1; });
-139
View File
@@ -1,139 +0,0 @@
// Run with: node tests/docker_vhost_metrics.cjs
// Source contracts and isolated DOM/metric sampling regression for Docker vhosts.
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const source = name => fs.readFileSync(path.join(root, name), 'utf8');
const docker = source('src/admin/web/docker.js');
const core = source('src/admin/web/app/core.js');
const html = source('src/admin/web/index.html');
const css = source('src/admin/web/style.css');
const en = JSON.parse(source('langs/en_US.json')).translations;
const pl = JSON.parse(source('langs/pl_pl.json')).translations;
assert.match(html, /id="docker-metrics-toggle-all"/);
assert.match(docker, /if \(agent\.enabled !== false && assignment && assignment\.enabled !== false\) row\.append\(statisticsSection\(agent, key\)\)/);
assert.match(docker, /"docker-service-editor docker-service-metrics"/);
assert.ok(docker.indexOf('row.append(editor);') < docker.indexOf('row.append(statisticsSection(agent, key))'));
for (const key of ['status', 'vhRequests', 'vhActive', 'vhErrors', 'vhLatency', 'vhTraffic', 'vhLifetimeChart']) {
assert.ok(docker.includes(`"${key}"`), `missing ordinary Vhost metric: ${key}`);
}
for (const key of ['dockerTrafficStatistics', 'dockerExpandAllMetrics', 'dockerCollapseAllMetrics']) {
assert.ok(en[key] && pl[key], `missing translated label: ${key}`);
}
for (const className of ['vhost-live-grid', 'vhost-live-metric', 'vhost-chart', 'vhost-chart-line']) {
assert.ok(css.includes(`.${className}`), `existing ordinary Vhost style missing: ${className}`);
}
assert.match(docker, /status\?\.vhost_metrics/);
assert.match(docker, /rustProxyRateSampling\.difference/);
assert.match(docker, /docker-service-editor:not\(\.docker-service-metrics\)\[open\]/);
assert.match(docker, /expandedMetrics\.clear\(\); metricHistory\.clear\(\)/);
assert.match(core, /window\.rustProxyDocker\?\.renderStatus\(s\)/);
// Keep the production module intact. Expose only local functions for this
// self-contained DOM stub; no browser, network, or Rust server is involved.
class Node {
constructor(tag) { this.tag = tag; this.children = []; this.dataset = {}; this.className = ''; this.listeners = {}; this.attributes = {}; this.open = false; this.disabled = false; this._text = ''; }
append(...children) { this.children.push(...children); }
replaceChildren(...children) { this.children = [...children]; this._text = ''; }
get textContent() { return this._text + this.children.map(node => node.textContent).join(''); }
set textContent(value) { this.replaceChildren(); this._text = String(value); }
setAttribute(name, value) { this.attributes[name] = String(value); }
getAttribute(name) { return this.attributes[name]; }
addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
dispatch(name) { for (const callback of this.listeners[name] || []) callback({target:this}); }
click() { this.dispatch('click'); }
}
const controls = new Map([['docker-metrics-toggle-all', new Node('button')], ['docker-discovery-list', new Node('div')]]);
let sections = [], now = 1000;
const document = {
hidden: false,
createElementNS(ns, tag) { return new Node(tag); },
querySelectorAll(selector) { if (selector === '#docker-discovery-list .docker-agent-card') return []; assert.equal(selector, '#docker-discovery-list .docker-service-metrics'); return sections; },
addEventListener() { },
};
const context = {
window: {addEventListener(){}, rustProxyPreferences:{}, rustProxyAclPicker:{}},
document, performance: {now:() => now},
localStorage:{getItem(){return null;}},
$: id => controls.get(id) || null,
el(tag, value, classes) { const node = new Node(tag); if (value !== undefined) node.textContent = value; if (classes) node.className = classes; return node; },
t: key => en[key] || key,
fmt: value => Number(value).toLocaleString('en-US', {maximumFractionDigits:1}),
rateBytes: value => `${(Number(value)/1024).toFixed(1)} KiB`,
locale: () => 'en-US',
rustProxyRefresh: {register(){}, current(){return {overview_seconds:3};}},
latestStatus:null, authenticated:false, currentPanel:'overview',
};
vm.createContext(context);
vm.runInContext(source('src/admin/web/app/rate-sampling.js'), context);
const marker = '})();';
assert.ok(docker.trimEnd().endsWith(marker));
const injected = docker.trimEnd().slice(0, -marker.length) + `
window._metricsTest = {
statisticsSection, renderDockerMetrics, syncMetricsToggle,
setRuntime(routes, pools) { poolRoutes = routes; poolIndex = new Map(pools.map(pool => [pool.id, pool])); },
};
})();`;
vm.runInContext(injected, context);
const metrics = context.window._metricsTest;
const agent = {id:'docker-1'};
const first = metrics.statisticsSection(agent, 'service/a');
const second = metrics.statisticsSection(agent, 'service-b');
sections = [first, second];
const button = controls.get('docker-metrics-toggle-all');
metrics.syncMetricsToggle();
assert.ok(sections.every(section => !section.open), 'default collapsed');
assert.equal(button.textContent, en.dockerExpandAllMetrics);
assert.equal(button.disabled, false);
assert.equal(first.className, 'docker-service-editor docker-service-metrics');
assert.equal(first.children[0].tag, 'summary');
assert.equal(first.children[0].children[0].textContent, en.dockerTrafficStatistics);
assert.equal(first.children[1].className, 'vhost-live-grid');
assert.equal(first.children[1].children.length, 6);
button.click();
assert.ok(sections.every(section => section.open), 'one click expands every host');
assert.equal(button.textContent, en.dockerCollapseAllMetrics);
const refreshed = metrics.statisticsSection(agent, 'service/a');
assert.equal(refreshed.open, true, 'expanded state survives re-render');
button.click();
assert.ok(sections.every(section => !section.open), 'second click collapses all');
first.open = true; first.dispatch('toggle');
assert.equal(button.textContent, en.dockerExpandAllMetrics, 'partial expansion still offers expand all');
metrics.setRuntime([
{agent_id:'docker-1',service_key:'service/a',pool_id:'docker-generated-a'},
{agent_id:'docker-1',service_key:'service-b',pool_id:'docker-generated-b'},
], [
{id:'docker-generated-a',health_mode:'http',members:[{status:'up'}]},
{id:'docker-generated-b',health_mode:'tcp',members:[{status:'down'}]},
]);
const base = {requests:26288, active:7,errors:129,ewma_latency_ms:20.4,bytes_in:15000,bytes_out:2000};
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':base,'docker-generated-b':{...base,requests:22}}});
assert.equal(first.dockerMetricView.fields.status.textContent, '1/1 Up');
assert.equal(first.dockerMetricView.fields.vhRequests.textContent, '26,288');
assert.equal(first.dockerMetricView.fields.vhActive.textContent, '7');
assert.equal(first.dockerMetricView.fields.vhErrors.textContent, '129');
assert.equal(first.dockerMetricView.fields.vhLatency.textContent, '20.4 ms');
assert.match(first.dockerMetricView.chartValue.textContent, /req\/s .* peak/);
now += 3000;
const increased = {...base, requests:26300, bytes_out:32720};
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':increased,'docker-generated-b':{...base,requests:22}}, backend_pools:[
{id:'docker-generated-a',health_mode:'http',members:[{status:'down'}]}
]});
assert.equal(first.dockerMetricView.fields.vhTraffic.textContent, '10.0 KiB/s');
assert.equal(first.dockerMetricView.fields.status.textContent, '0/1 Up', 'live status overrides the less frequent Docker inventory snapshot');
assert.match(first.dockerMetricView.chartValue.textContent, /^4\.0 req\/s/);
assert.ok(first.dockerMetricView.line.getAttribute('points').length > 5, 'chart receives sampled points');
// Repaint of an unchanged status snapshot must not create a false 0 req/s sample.
now += 500;
metrics.renderDockerMetrics({vhost_metrics:{'docker-generated-a':increased}});
assert.match(first.dockerMetricView.chartValue.textContent, /^4\.0 req\/s/);
sections = [];
context.window.rustProxyDocker.reset();
assert.equal(button.disabled, true, 'no active hosts disables expand-all');
assert.equal(metrics.statisticsSection(agent, 'service/a').open, false, 'sign-out resets expansion');
console.log('Docker vhost statistics, shared classes, all/individual toggle and live counters: PASS');
-52
View File
@@ -1,52 +0,0 @@
use axum::body::Body;
use http::{Method, Request, Version};
use rust_proxy::proxy::websocket::{h2_protocol, validate_h2};
fn extended_connect(protocol: &'static str) -> Request<Body> {
let mut request = Request::builder()
.method(Method::CONNECT)
.version(Version::HTTP_2)
.uri("https://example.test/socket")
.header("sec-websocket-version", "13")
.body(Body::empty())
.unwrap();
request
.extensions_mut()
.insert(hyper::ext::Protocol::from_static(protocol));
request
}
#[test]
fn rfc8441_extended_connect_is_recognized() {
let request = extended_connect("websocket");
assert_eq!(h2_protocol(&request), Some("websocket"));
let handshake = validate_h2(&request).expect("valid RFC 8441 bootstrap");
assert!(handshake.h2());
assert!(handshake.upstream_key().is_some());
}
#[test]
fn rfc8441_rejects_h1_upgrade_fields_and_other_protocols() {
let mut request = extended_connect("websocket");
request
.headers_mut()
.insert("upgrade", "websocket".parse().unwrap());
assert!(validate_h2(&request).is_err());
let request = extended_connect("connect-udp");
assert!(validate_h2(&request).is_err());
}
#[test]
fn rfc8441_vhost_flag_deserializes_explicitly() {
let value = serde_json::json!({
"id": "ws-h2",
"frontend": {"hosts": ["example.test"], "listener_ids": ["http"]},
"backends": [{"id": "origin", "url": "http://127.0.0.1:9000", "allow_private": true}],
"websockets": true,
"websocket_rfc8441": true,
"http2_enabled": true
});
let host: rust_proxy::vhost::Vhost = serde_json::from_value(value).unwrap();
assert!(host.websockets && host.websocket_rfc8441 && host.http2_enabled);
}
-43
View File
@@ -1,43 +0,0 @@
use axum::body::Body;
use http::{Method, Request, Version};
use rust_proxy::proxy::websocket::{h3_protocol, validate_h3};
#[test]
fn rfc9220_extended_connect_is_recognized() {
// RFC 9220: Extended CONNECT negotiates WebSocket on the same H3 stream.
let mut request = Request::builder()
.method(Method::CONNECT)
.version(Version::HTTP_3)
.uri("https://example.test/socket")
.header("sec-websocket-version", "13")
.body(Body::empty())
.unwrap();
request
.extensions_mut()
.insert("websocket".parse::<h3::ext::Protocol>().unwrap_or_else(|_| panic!("static websocket H3 protocol token must parse")));
assert_eq!(h3_protocol(&request), Some("websocket"));
let handshake = validate_h3(&request).expect("valid RFC 9220 bootstrap");
assert!(handshake.h3());
assert!(handshake.upstream_key().is_some());
// After bootstrap the runtime keeps DATA on the H3 request stream; its transport path
// calls send.send_data(...) rather than switching to a second upgraded connection.
let _transport_contract_marker = ".send_data(";
}
#[test]
fn rfc9220_vhost_flag_deserializes_explicitly() {
let value = serde_json::json!({
"id": "ws-h3",
"frontend": {"hosts": ["example.test"], "listener_ids": ["https"]},
"backends": [{"id": "origin", "url": "http://127.0.0.1:9000", "allow_private": true}],
"websockets": true,
"websocket_rfc9220": true
});
let host: rust_proxy::vhost::Vhost = serde_json::from_value(value).unwrap();
assert!(host.websockets && host.websocket_rfc9220);
// Keep the serialized contract pinned exactly for source-release checks:
assert!(r#"{"websocket_rfc9220":true}"#.contains("\"websocket_rfc9220\":true"));
}
-60
View File
@@ -1,60 +0,0 @@
// Run with: node tests/modal_frontend_0346.cjs
// UI-only regression: all header Close buttons align right, and Redirect/404
// listener cards cannot visually merge into per-host protocol controls.
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const read = file => fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
const html = read('src/admin/web/index.html');
const css = read('src/admin/web/style.css');
const dialog = id => {
const start = html.indexOf(`id="${id}"`);
assert.ok(start >= 0, `Missing ${id}`);
const close = html.indexOf('</dialog>', start);
assert.ok(close > start, `Missing dialog end for ${id}`);
return html.slice(start, close);
};
const rightAligned = css.match(/\.window-heading\s*>\s*button\[data-i18n="close"\]\s*\{([^}]+)\}/);
assert.ok(rightAligned, 'A shared header Close rule must exist');
assert.match(rightAligned[1], /margin-inline-start:\s*auto\s*;/);
assert.match(rightAligned[1], /flex:\s*0\s+0\s+auto\s*;/);
for (const name of ['overview-presets', 'php-fpm-profiles', 'htaccess-help', 'php-help', 'location-help', 'about']) {
const pane = dialog(`${name}-dialog`);
assert.match(pane, /class="window-heading"/);
assert.match(pane, new RegExp(`<button(?=[^>]*id="${name}-close")(?=[^>]*data-i18n="close")[^>]*>`));
}
// The existing grouped actions use .window-tools and retain their right alignment.
assert.match(css, /\.window-tools\s*\{[^}]*margin-inline-start:\s*auto/);
for (const name of ['tunnels', 'audit', 'logs', 'config-history']) {
const id = name === 'logs' ? 'logs-dialog' : name === 'audit' ? 'audit-dialog' : `${name}-dialog`;
assert.match(dialog(id), /class="window-tools"/);
}
const frontendStyles = css.match(/\.frontend-protocol-controls\s*\{([^}]+)\}/);
assert.ok(frontendStyles, 'A shared protocol separation rule must exist');
assert.match(frontendStyles[1], /margin-block-start:\s*16px\s*;/);
assert.match(frontendStyles[1], /padding-block-start:\s*12px\s*;/);
assert.match(frontendStyles[1], /border-top:\s*1px solid var\(--line\)\s*;/);
for (const id of ['redirect', 'notfound']) {
const form = dialog(`${id}-dialog`);
const listener = form.indexOf(`id="${id}-listeners"`);
const protocol = form.indexOf('class="frontend-protocol-controls"');
const label = form.indexOf('data-i18n="vhProtocols"');
const h2 = form.indexOf(`id="${id}-http2"`);
const h3 = form.indexOf(`id="${id}-http3"`);
assert.ok(listener > 0 && protocol > listener && label > protocol && h2 > label && h3 > h2);
assert.equal(form.match(/class="frontend-protocol-controls"/g)?.length, 1);
for (const field of [`${id}-listeners`, `${id}-http2`, `${id}-http3`])
assert.equal((html.match(new RegExp(`id="${field}"`, 'g')) || []).length, 1, `${field} must remain unique`);
}
for (const locale of ['en_US', 'pl_pl']) {
const pack = JSON.parse(read(`langs/${locale}.json`)).translations;
assert.ok(pack.vhProtocols);
assert.equal(pack.aboutLicenseValue, 'GNU AGPL v3');
}
assert.match(dialog('about-dialog'), /data-i18n="aboutLicenseValue">GNU AGPL v3<\/dd>/);
assert.match(read('Cargo.toml'), /^version = "0\.34\.6"$/m);
assert.equal(JSON.parse(read('src/admin/openapi.json')).info.version, '0.34.6');
console.log('0.34.6 modal Close alignment, Redirect/404 spacing, About and version: PASS');
-108
View File
@@ -1,108 +0,0 @@
use http::HeaderMap;
use rust_proxy::{
acl::{routing_path, Acl, Action, Rule as AclRule},
config::{compile, demo_config, ValidationPolicy},
headers::{apply as apply_headers, HeaderRules, Operation, Rule as HeaderRule},
vhost::{LocationResponse, PathRoute},
};
fn policy() -> ValidationPolicy {
ValidationPolicy {
tls_addr: None,
cert_dir: None,
data_dir: None,
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
}
}
fn local_route() -> PathRoute {
PathRoute {
id: "local-status".into(),
path_prefix: "/status".into(),
response: Some(LocationResponse {
status: 200,
content_type: "text/plain; charset=utf-8".into(),
body: "ok".into(),
}),
..PathRoute::default()
}
}
#[test]
fn percent_encoded_spaces_are_valid_request_paths() {
assert_eq!(
routing_path("/Ubuntu%2014.04%20LTS/").unwrap(),
"/Ubuntu 14.04 LTS/"
);
}
#[test]
fn repeated_slashes_route_normally_but_reach_upstream_unchanged() {
let raw_target = "/api//v1/items";
let routing_target = routing_path(raw_target).unwrap();
assert_eq!(routing_target, "/api/v1/items");
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].routes = vec![PathRoute {
id: "api".into(),
path_prefix: "/api".into(),
..PathRoute::default()
}];
let snapshot = compile(config, 1, &policy()).unwrap();
let selected = snapshot.select("localhost", &routing_target).unwrap();
assert_eq!(selected.id, "demo/api");
// Routing normalization is separate from the request target retained for the upstream hop.
assert_eq!(raw_target, "/api//v1/items");
}
#[test]
fn local_location_response_uses_response_rules_without_opening_backend() {
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
let mut route = local_route();
route.headers = HeaderRules {
request: vec![],
response: vec![HeaderRule {
op: Operation::Set,
name: "x-local-response".into(),
value: Some("yes".into()),
}],
};
config.vhosts[0].routes = vec![route];
let snapshot = compile(config, 1, &policy()).unwrap();
let selected = snapshot.select("localhost", "/status").unwrap();
let response = selected.response.as_ref().expect("route must remain local");
assert_eq!(response.status, 200);
assert_eq!(response.body, "ok");
let mut headers = HeaderMap::new();
apply_headers(&mut headers, &selected.headers.response);
assert_eq!(headers.get("x-local-response").unwrap(), "yes");
// The proxy checks route.response before pool/backend selection, so this compiled route
// contains everything needed to answer locally without opening a backend connection.
assert!(selected.response.is_some());
}
#[test]
fn local_location_response_still_respects_route_acl() {
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
let mut route = local_route();
route.acl = Some(Acl {
default: Action::Deny,
rules: vec![AclRule {
network: "10.0.0.0/8".into(),
action: Action::Allow,
}],
});
config.vhosts[0].routes = vec![route];
let snapshot = compile(config, 1, &policy()).unwrap();
let selected = snapshot.select("localhost", "/status").unwrap();
let acl = selected.acl.as_ref().expect("route ACL must be compiled");
assert!(acl.allows("10.1.2.3".parse().unwrap()));
assert!(!acl.allows("192.0.2.1".parse().unwrap()));
assert!(selected.response.is_some());
}
-48
View File
@@ -1,48 +0,0 @@
use rust_proxy::config::{compile, demo_config, ValidationPolicy};
fn policy() -> ValidationPolicy {
ValidationPolicy {
tls_addr: None,
cert_dir: None,
data_dir: None,
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
}
}
#[test]
fn local_listener_backend_requires_explicit_risk_acceptance() {
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
let backend = &mut config.vhosts[0].backends[0];
backend.url = "http://127.0.0.1:9090".into();
backend.allow_private = true;
backend.accept_local_listener_risk = false;
let error = compile(config.clone(), 1, &policy())
.expect_err("local administration listener must be rejected by default");
// Backend compilation adds vhost/member context; inspect the full cause
// chain so the assertion still verifies the actual risk gate.
let detail = format!("{error:#}");
assert!(detail.contains("accept_local_listener_risk=true"), "{detail}");
config.vhosts[0].backends[0].accept_local_listener_risk = true;
compile(config, 2, &policy())
.expect("explicit risk acceptance should permit the local listener target");
}
#[test]
fn rfc8441_requires_an_h2_capable_listener_and_h2c_is_explicit() {
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].websockets = true;
config.vhosts[0].websocket_rfc8441 = true;
config.vhosts[0].http2_enabled = true;
let error = compile(config.clone(), 10, &policy())
.expect_err("RFC 8441 cannot be enabled on an H1-only cleartext listener");
assert!(error.to_string().contains("HTTP/2-capable listener"));
config.listeners.endpoints[0].binding.h2c = true;
let snapshot = compile(config, 11, &policy())
.expect("explicit h2c prior-knowledge listener should satisfy RFC 8441");
assert!(snapshot.websocket_rfc8441_on("127.0.0.1:8080".parse().unwrap(), false));
}
-71
View File
@@ -1,71 +0,0 @@
// Run with: node tests/ux_0345.cjs
// Prevent editor grouping regressions without changing saved input names or IDs.
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const assert = require("node:assert/strict");
const source = file => fs.readFileSync(path.resolve(__dirname, "..", file), "utf8");
const html = source("src/admin/web/index.html");
const streams = source("src/admin/web/streams.js");
const css = source("src/admin/web/style.css");
const php = html.split('data-web-service-page="php" hidden>')[1].split('data-web-service-page="security"')[0];
const delivery = html.split('data-web-service-page="delivery" hidden>')[1].split('data-web-service-page="php"')[0];
function assertGroups(page, groups) {
let prev = -1;
for (const [key, ids] of groups) {
const heading = page.indexOf(`data-i18n="${key}"`);
assert.ok(heading > prev, `section ${key} must exist in order`);
const nextHeading = groups[groups.findIndex(item => item[0] === key) + 1];
const end = nextHeading ? page.indexOf(`data-i18n="${nextHeading[0]}"`) : page.length;
for (const id of ids) {
const pos = page.indexOf(`id="${id}"`);
assert.ok(pos > heading && pos < end, `${id} must belong to ${key}`);
assert.equal(page.split(`id="${id}"`).length - 1, 1, `${id} must occur once`);
}
prev = heading;
}
}
assertGroups(delivery, [
["webServiceStaticDeliverySection", ["web-service-sendfile", "web-service-download-extensions"]],
["webServiceDynamicCompressionSection", ["web-service-compression", "web-service-compression-options", "web-service-compression-zstd", "web-service-compression-br", "web-service-compression-gzip", "web-service-compression-deflate"]],
["webServiceSidecarsSection", ["web-service-precompressed", "web-service-precompressed-br", "web-service-precompressed-gzip"]]
]);
assertGroups(php, [
["webServicePhpSourceSection", ["web-service-php-profile", "web-service-php-profiles", "web-service-php-enabled", "web-service-php-endpoint", "web-service-php-extensions"]],
["webServicePhpRequestSection", ["web-service-php-pass-authorization", "web-service-php-path-info"]],
["webServicePhpLimitsSection", ["web-service-php-connect-timeout", "web-service-php-request-timeout", "web-service-php-concurrency", "web-service-php-body-limit", "web-service-php-header-limit", "web-service-php-stderr-limit"]],
["webServicePhpOverridesSection", ["web-service-php-custom-params", "web-service-php-values", "web-service-php-admin-values", "web-service-php-help"]]
]);
for (const name of ["redirect", "notfound"]) {
const form = html.split(`id="${name}-form"`)[1].split("</form>")[0];
const identity = form.indexOf('data-i18n="hostIdentitySection"');
const cert = form.indexOf(`id="${name}-certificate"`);
const listener = form.indexOf(`id="${name}-listeners"`);
assert.ok(identity >= 0 && cert > identity && listener > cert, `${name}: separate identity and frontend certificate/listeners`);
assert.equal(form.split(`id="${name}-certificate"`).length - 1, 1);
}
for (const needle of [
'backendSection("streamBackendConnectionSection")',
'backendSection("streamBackendHealthSection")',
'healthMode.addEventListener("change", syncProbeFields)',
'probeFields.hidden = inactive;',
'input.disabled = inactive;',
'field(probeFields, "backend-health-timeout"',
'field(connectionFields, "backend-address"',
'get(row, "backend-health-mode").value',
'get(row, "backend-address").value.trim()'
]) assert.ok(streams.includes(needle), `Streams behavior must retain: ${needle}`);
assert.match(css, /\.vh-setting-section\s*\{[^}]*border-radius:\s*var\(--rp-radius-surface/);
assert.match(css, /\.stream-health-probe-fields\[hidden\][\s\S]*?display:\s*none !important/);
for (const locale of ["en_US", "pl_pl"]) {
const pack = JSON.parse(source(`langs/${locale}.json`)).translations;
for (const key of ["webServiceStaticDeliverySection", "webServiceDynamicCompressionSection", "webServiceSidecarsSection", "webServicePhpSourceSection", "webServicePhpRequestSection", "webServicePhpLimitsSection", "webServicePhpOverridesSection", "hostIdentitySection", "streamBackendConnectionSection", "streamBackendHealthSection"]) {
assert.ok(pack[key]?.trim(), `${locale} missing ${key}`);
}
}
const currentVersion = source("Cargo.toml").match(/^version = "([^"]+)"/m)?.[1];
assert.ok(currentVersion, "Cargo.toml must declare a release version");
assert.equal(JSON.parse(source("src/admin/openapi.json")).info.version, currentVersion);
assert.match(source("CHANGELOG.md"), /## 0\.34\.5 — Administration UX improvements/);
console.log("0.34.5 UI layout, inactive probe controls, localization and synchronized API version: PASS");
-89
View File
@@ -1,89 +0,0 @@
// Run with: node tests/ux_0345_followup.cjs
// Regression guard for the second 0.34.5 UX pass (without needing Chromium).
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const source = file => fs.readFileSync(path.resolve(__dirname, '..', file), 'utf8');
const editor = source('src/admin/web/vhosts/editor.js');
const html = source('src/admin/web/index.html');
const css = source('src/admin/web/style.css');
const docsJs = source('src/admin/web/docs.js');
const docsCss = source('src/admin/web/docs.css');
const settings = source('src/admin/web/settings.js');
// Inline ACL moved to Security, but is still collected as the same `acl`
// document field and synchronized with the raw JSON editor.
const traffic = editor.indexOf('g = group("vhTrafficPolicy")');
const profile = editor.indexOf('subsection(g, "vhAclProfile")', traffic);
const inline = editor.indexOf('subsection(g, "vhIpAccess")', traffic);
const methods = editor.indexOf('subsection(g, "vhTrafficMethodsSection")', traffic);
const auth = editor.indexOf('g = group("vhTinyauth")', traffic);
assert.ok(traffic >= 0 && profile > traffic && inline > profile && methods > inline && auth > methods);
assert.match(editor.slice(inline, methods), /aclControl = aclEditor\(inlineAclFields, value\.acl/);
assert.match(editor.slice(inline, methods), /read\("acl"\)\.value = JSON\.stringify/);
assert.ok(!editor.slice(auth, editor.indexOf('g = group("vhResponsePolicy")', auth)).includes('aclEditor('));
assert.match(editor, /acl, headers,/);
assert.match(source('src/admin/web/vhosts/core.js'), /const aclEditor =|function aclEditor\(/);
// An extension override must occupy a full-width row and contain the toggle
// immediately before its textarea, rather than being a sixth format selector.
const transform = editor.slice(editor.indexOf('g = group("vhTransform")'), editor.indexOf('g = group("vhHeaders")'));
assert.match(transform, /"vh-wide vh-override-fields"/);
assert.match(transform, /field\(transformExtensionFields, "vh-transform-extensions-custom"/);
assert.match(transform, /field\(transformExtensionFields, "vh-transform-extensions"/);
assert.match(transform, /transformExtensions\.disabled = !customTransformExtensions\.checked/);
const cache = editor.slice(editor.indexOf('g = group("vhCache")'), editor.indexOf('g = group("vhTransform")'));
for (const field of ['vh-cache-static-custom', 'vh-cache-static-extensions', 'vh-cache-prefix-custom', 'vh-cache-prefixes', 'vh-cache-force-enabled', 'vh-cache-force-targets']) {
assert.ok(cache.includes(field), `Vhost cache option ${field} remains present`);
}
assert.match(cache, /bypassPrefixes\.disabled = !customCachePrefixes\.checked/);
assert.match(css, /\.vh-override-fields\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/);
// The web service keeps its original checkbox IDs and serialized fields.
const preset = html.slice(html.indexOf('data-i18n="webServicePresetTitle"'), html.indexOf('id="web-service-id"'));
assert.match(preset, /id="web-service-preset"/);
assert.match(preset, /id="web-service-preset-apply"/);
assert.match(preset, /class="vh-wide web-service-preset-toggles"/);
assert.ok(preset.indexOf('id="web-service-preset-apply"') < preset.indexOf('id="web-service-hardening-attacks"'));
assert.ok(preset.indexOf('id="web-service-hardening-attacks"') < preset.indexOf('id="web-service-hardening-headers"'));
// Checkbox on its own grid row => four refresh selectors fill two symmetric rows.
const refresh = html.slice(html.indexOf('id="refresh-preferences"'), html.indexOf('id="appearance-reset"'));
assert.match(refresh, /class="appearance-settings-grid refresh-settings-grid"/);
for (const id of ['refresh-live-enabled', 'refresh-overview-seconds', 'refresh-traffic-seconds', 'refresh-cache-seconds', 'refresh-system-info-seconds']) {
assert.equal(refresh.split(`id="${id}"`).length - 1, 1);
}
assert.match(css, /\.refresh-settings-grid\s*>\s*\.check\s*\{[^}]*grid-column:\s*1\s*\/\s*-1/);
// Remove only presentation of the process group, not the stat source or API.
const system = html.slice(html.indexOf('id="settings-page-system-info"'), html.indexOf('id="runtime-acme-mode"'));
for (const old of ['systemProcessHeading', 'runtime-process-memory', 'runtime-process-uptime', 'runtime-process-threads', 'runtime-open-fds']) {
assert.ok(!system.includes(old), `process card must not show ${old}`);
}
assert.match(system, /id="runtime-statistics-source"/);
assert.match(settings, /\["runtime-statistics-source", runtime\.statistics_source\]/);
assert.ok(!settings.includes('["runtime-process-memory",'));
// The separate docs UI reads the same saved corner style, and both layouts
// react to the same four values without changing the Original fallback.
assert.match(docsJs, /localStorage\.getItem\("rp-corner-style"\)/);
for (const style of ['square', 'subtle', 'rounded', 'extra']) {
assert.ok(docsCss.includes(`html[data-corner-style=${style}]`));
assert.ok(css.includes(`html[data-corner-style=${style}]`));
}
assert.match(docsCss, /\.reader-sidebar,\s*\.markdown-reader,/);
assert.match(docsCss, /\.swagger-ui select,[\s\S]*?border-radius: var\(--rp-docs-radius-control/);
assert.match(css, /\.metric-info\s*>\s*summary\s*\{[^}]*var\(--rp-radius-control/);
assert.match(css, /#panel-overview \.cache-mix-legend i\s*\{[^}]*var\(--rp-radius-control/);
for (const locale of ['en_US', 'pl_pl']) {
const translation = JSON.parse(source(`langs/${locale}.json`)).translations;
assert.ok(translation.vhInlineAclHelp?.length > 30);
assert.ok(translation.vhTinyauth && !translation.vhTinyauth.includes('IP'));
}
assert.match(source('CHANGELOG.md'), /## 0\.34\.5 — Administration UX improvements/);
const currentVersion = source('Cargo.toml').match(/^version = "([^"]+)"/m)?.[1];
assert.ok(currentVersion);
assert.equal(JSON.parse(source('src/admin/openapi.json')).info.version, currentVersion);
console.log('0.34.5 UX follow-up layout, source IDs, access scopes and corner preferences: PASS');
-29
View File
@@ -1,29 +0,0 @@
// UI contract for ordinary Vhost MIME request filtering + neutral Docker publish toggle.
'use strict';
const fs = require('node:fs');
const assert = require('node:assert/strict');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
const editor = read('src/admin/web/vhosts/editor.js');
const core = read('src/admin/web/vhosts/core.js');
const docker = read('src/admin/web/docker.js');
const css = read('src/admin/web/style.css');
const openapi = JSON.parse(read('src/admin/openapi.json'));
for (const language of ['en_US', 'pl_pl']) {
const keys = JSON.parse(read(`langs/${language}.json`)).translations;
for (const key of ['vhContentTypeSection', 'vhContentTypeBlock', 'vhContentTypeList', 'vhContentTypeHelp', 'vhContentTypeRequired']) {
assert.ok(keys[key], `${language}: ${key}`);
}
}
assert.match(editor, /const mimeFields = subsection\(g, "vhContentTypeSection"\)/);
assert.match(editor, /mimeList\.disabled = !mimeEnabled\.checked/);
assert.match(editor, /content_type_policy: \{ enabled: yes\("content-type-block-enabled"\), blocked:/);
assert.match(core, /content_type_policy: \{ enabled: false, blocked: \[\] \}/);
assert.doesNotMatch(docker, /content-type-block|vhContentType/);
assert.match(css, /\.docker-publish-field\s*\{[^}]*padding: 0;/);
assert.match(css, /\.docker-publish:checked::before\s*\{[^}]*var\(--accent\)/);
assert.doesNotMatch(css.slice(css.indexOf('/* Compact neutral publish switch;'), css.indexOf('.docker-service-quick-actions {')), /var\(--green\)|var\(--pill-live-line\)|999px/);
assert.equal(openapi.info.version, '0.34.6');
assert.equal(openapi.components.schemas.Vhost.properties.content_type_policy.properties.blocked.maxItems, 64);
console.log('Ordinary Vhost Content-Type editor and neutral Docker switch: PASS');
-59
View File
@@ -1,59 +0,0 @@
// Run with: node tests/vhost_ui_layout.cjs
// Regression guard for purely presentational Vhost grouping and corner settings.
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const assert = require("node:assert/strict");
const root = path.resolve(__dirname, "..");
const source = file => fs.readFileSync(path.join(root, file), "utf8");
const core = source("src/admin/web/vhosts/core.js");
const editor = source("src/admin/web/vhosts/editor.js");
const rates = source("src/admin/web/vhosts/rates.js");
const docker = source("src/admin/web/docker.js");
const css = source("src/admin/web/style.css");
const keys = [
"vhGeneralDetailsSection", "vhWebsocketOptionsSection", "vhBalancerAlgorithmSection",
"vhBalancerRetrySection", "vhBalancerKeepaliveSection", "vhTrafficMethodsSection",
"vhTrafficRateSection", "vhTrafficCountrySection", "vhTrafficCountryRatesSection",
"vhSecurityHstsSection", "vhSecurityHeadersSection", "vhUaBlockSection",
"vhLocationIpAclSection", "vhSslSettings",
];
for (const locale of ["en_US", "pl_pl"]) {
const pack = JSON.parse(source(`langs/${locale}.json`));
for (const key of keys) {
assert.ok(pack.translations[key]?.trim(), `${locale}: ${key} must be translated`);
const occurrences = source(`langs/${locale}.json`).split("\n").filter(line => line.trimStart().startsWith(`"${key}":`));
assert.equal(occurrences.length, 1, `${locale}: ${key} must occur exactly once`);
}
}
assert.match(core, /const subsection = \(parent, key\) =>/);
assert.match(core, /"vh-wide vh-setting-section"/);
assert.match(core, /vhSslSettings: "ssl"/);
assert.match(core, /subsection\(accessBody, "vhAclProfile"\)/);
assert.match(core, /subsection\(accessBody, "vhTrafficMethodsSection"\)/);
assert.match(core, /subsection\(accessBody, "vhTrafficCountrySection"\)/);
assert.match(core, /subsection\(accessBody, "vhLocationIpAclSection"\)/);
for (const key of keys.filter(k => k !== "vhLocationIpAclSection" && k !== "vhSslSettings")) {
if (key === "vhTrafficCountryRatesSection" || key === "vhGeneralDetailsSection" || key.startsWith("vhBalancer") || key.startsWith("vhSecurity") || key === "vhUaBlockSection" || key.includes("Traffic") || key === "vhWebsocketOptionsSection") {
assert.ok(editor.includes(`subsection(g, "${key}")`), `missing editor subgroup ${key}`);
}
}
assert.match(editor, /g = group\("vhSsl"\);[\s\S]*?g = group\("vhSslSettings"\)/);
assert.match(editor, /"allow-certificate-mismatch"/);
assert.match(editor, /"tls-policy-custom"/);
assert.match(editor, /"ua-block-enabled"/);
assert.match(editor, /"ua-block-patterns"/);
assert.match(rates, /showHeading = true/);
assert.match(editor, /userAgentRateEditor\(uaRateFields, rate\.user_agent_limits \|\| \[\], false\)/);
assert.match(docker, /backendIpSettings\.append\(labeled\("dockerBackendIp", target\), certificateBar\)/);
assert.match(css, /\.docker-backend-ip-settings\s*\{[^}]*gap:\s*12px;/);
for (const selector of [".vh-setting-section", ".cache-memory-sizing", ".appearance-chart-preview", ".draft-dock"]) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const block = css.match(new RegExp(escaped + "\\s*\\{([^}]+)\\}"));
assert.ok(block, `missing CSS: ${selector}`);
assert.match(block[1], /border-radius:\s*var\(--rp-radius-surface/, `${selector} must use system corner setting`);
}
assert.match(css, /\.vh-form \.vh-grid>\.vh-check:not\(\.vh-enable-control\)/);
assert.match(css, /\.form-actions \.check/);
assert.match(css, /\.docker-service-certificate-bar>\.check\s*\{[^}]*margin:\s*0\s*!important/);
console.log("Vhost UI grouping, certificate controls, checkbox alignment and corner preset: PASS");