Files
RustProxy/tests/docker_vhost_metrics.cjs
T
2026-10-21 14:39:51 +02:00

140 lines
8.2 KiB
JavaScript

// 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');