v0.32.2
This commit is contained in:
1 parent
bb025dcf44
commit
b0f211555a
74 files changed
+6089
-556
No files matched your search
@@ -0,0 +1,172 @@
|
||||
// HTTP backend Overview widgets: offline simulated status feed, no Rust execution.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const names = ['backend_health', 'backend_activity', 'backend_errors', 'backend_latency', 'backend_traffic', 'backend_protocols', 'backend_requests', 'backend_capacity', 'backend_http5xx', 'backend_timeouts', 'backend_network', 'backend_retries', 'backend_queue', 'backend_rotations'];
|
||||
const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8'));
|
||||
for (const name of names) assert(registry.some(widget => widget.id === name && widget.default_hidden && widget.data_source === 'backend_status'));
|
||||
const source = fs.readFileSync('src/admin/web/app/backend-dashboard.js', 'utf8');
|
||||
assert(!/\b(fetch|api)\s*\(/.test(source), 'Overview must not fetch per-widget metrics');
|
||||
const cards = new Map(names.map(name => [name, { hidden: true }]));
|
||||
const nodes = new Map();
|
||||
const get = id => {
|
||||
if (!nodes.has(id)) nodes.set(id, { textContent: '-', style: {}, attrs: {}, children: [], setAttribute(k,v) { this.attrs[k] = v; } });
|
||||
return nodes.get(id);
|
||||
};
|
||||
for (const type of ['h1', 'auto', 'compat', 'h2']) {
|
||||
const node = get('backend-mode-' + type);
|
||||
node.parentElement = get('backend-mode-bar');
|
||||
node.parentElement.children.push(node);
|
||||
}
|
||||
let now = 1000;
|
||||
const window = { rustProxyPreferences: { current: () => null } };
|
||||
const ctx = {
|
||||
window,
|
||||
ADMIN_APPEARANCE_DEFAULTS: { dashboard_hidden_widgets: names },
|
||||
document: { querySelector(selector) { return cards.get(selector.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null; } },
|
||||
$: get, t: key => key, fmt: (value, precision = 0) => Number(value).toFixed(precision),
|
||||
bytes: v => `${v} B`, currentPanel: 'overview', performance: { now: () => now },
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(source, ctx);
|
||||
const module = window.rustProxyBackendDashboard;
|
||||
const data = {
|
||||
revision: 1,
|
||||
backend_pools: [{ id: 'pool1', members: [
|
||||
{ id: 'alpha', url: 'https://example.invalid', protocol: 'auto', status: 'up', active: 2, max_concurrent_requests: 4, capacity_rejections_total: 1, requests: 10, errors: 1, bytes_in: 1000, bytes_out: 2000, health_mode: 'tcp', last_probe_ms: 1234, latency_ms: 10, http_5xx_total: 2, timeouts_total: 1, connect_errors_total: 1, dns_errors_total: 1, tls_errors_total: 1, io_errors_total: 1, retry_attempts_total: 2, retry_successes_total: 1, queue_timeouts_total: 1, upstream_client_rotations_total: 2, upstream_client_generation: 3, upstream_client_generation_age_seconds: 500 },
|
||||
{ id: 'beta', url: 'http://example.invalid', protocol: 'http2', status: 'down', active: 0, requests: 5, errors: 2, bytes_in: 100, bytes_out: 400, health_mode: 'http', last_probe_ms: 0, latency_ms: 0 },
|
||||
{ id: 'gamma', url: 'http://example.invalid', protocol: 'http1', status: 'checking', active: 1, max_concurrent_requests: 2, requests: 0, errors: 0, bytes_in: 0, bytes_out: 0, health_mode: 'tcp', last_probe_ms: 0, latency_ms: 0 },
|
||||
{ id: 'delta', url: 'http://example.invalid', status: 'draining', active: 0, requests: 0, errors: 0, bytes_in: 0, bytes_out: 0, health_mode: 'off', last_probe_ms: 0, latency_ms: 0 },
|
||||
] }],
|
||||
};
|
||||
const text = id => get(`backend-dashboard-${id}`).textContent;
|
||||
|
||||
module.render(data); // Hidden by default: should not start collecting samples.
|
||||
assert(names.every(name => cards.get(name).hidden));
|
||||
assert.equal(text('health'), '-');
|
||||
module.render(data, []);
|
||||
assert(names.every(name => !cards.get(name).hidden));
|
||||
assert.equal(text('health'), '1 / 3');
|
||||
assert.match(text('health-detail'), /backendWidgetDown: 1/);
|
||||
assert.equal(text('activity'), '3');
|
||||
assert.equal(text('errors'), '3');
|
||||
assert.equal(text('latency'), '10.0 ms');
|
||||
assert.equal(text('traffic'), '0 B/s');
|
||||
assert.equal(text('protocols'), '4');
|
||||
assert.match(text('protocols-detail'), /H1 1.*Auto 1.*backendWidgetModeCompat 1.*backendWidgetModeStrict 0.*backendWidgetH2c: 1.*backendWidgetModeUnknown: 1/);
|
||||
assert.equal(get('backend-mode-compat').style.flexGrow, '1');
|
||||
assert.equal(get('backend-mode-h2').style.flexGrow, '0');
|
||||
assert.equal(text('requests'), '0.0/s');
|
||||
assert.match(text('requests-detail'), /backendWidgetRequestsTotal: 15/);
|
||||
assert.equal(text('capacity'), '50.0%');
|
||||
assert.match(text('capacity-detail'), /3\/6.*backendWidgetCapacityRejects: 1/);
|
||||
assert.equal(text('http5xx'), '2');
|
||||
assert.equal(text('timeouts'), '1');
|
||||
assert.equal(text('network'), '4');
|
||||
assert.equal(text('retries'), '2');
|
||||
assert.equal(text('queue'), '2');
|
||||
assert.equal(text('rotations'), '2');
|
||||
assert.match(text('rotations-detail'), /backendWidgetPoolGeneration: 3.*backendWidgetPoolAge: 500s/);
|
||||
|
||||
now = 3000;
|
||||
const next = structuredClone(data);
|
||||
next.backend_pools[0].members[0].requests = 30;
|
||||
next.backend_pools[0].members[0].errors = 3;
|
||||
next.backend_pools[0].members[0].bytes_in = 3000;
|
||||
next.backend_pools[0].members[0].bytes_out = 3000;
|
||||
next.backend_pools[0].members[0].active = 5;
|
||||
next.backend_pools[0].members[0].latency_ms = 22;
|
||||
next.backend_pools[0].members[0].http_5xx_total = 4;
|
||||
next.backend_pools[0].members[0].timeouts_total = 5;
|
||||
next.backend_pools[0].members[0].connect_errors_total = 3;
|
||||
next.backend_pools[0].members[0].retry_attempts_total = 4;
|
||||
next.backend_pools[0].members[0].retry_successes_total = 2;
|
||||
next.backend_pools[0].members[0].queue_timeouts_total = 3;
|
||||
// New member starts with huge absolute counters: must NOT appear as a spike.
|
||||
next.backend_pools[0].members.push({ id: 'new', url:'https://new.invalid', protocol: 'http1', status:'up', requests: 100000, errors: 1000, bytes_in: 10000000, bytes_out: 99999999, active: 0, health_mode: 'off', last_probe_ms: 0 });
|
||||
module.render(next, []);
|
||||
assert.equal(text('errors'), '1005');
|
||||
assert.match(text('errors-detail'), /1\.0\/s/);
|
||||
assert.match(text('errors-detail'), /10\.0%/);
|
||||
assert.equal(text('traffic'), '1500 B/s');
|
||||
assert.equal(get('backend-dashboard-in-rate').textContent, '1000 B/s');
|
||||
assert.equal(get('backend-dashboard-out-rate').textContent, '500 B/s');
|
||||
assert.equal(text('activity'), '6');
|
||||
assert.equal(text('requests'), '10.0/s');
|
||||
assert.equal(text('capacity'), '100.0%');
|
||||
assert.equal(text('latency'), '22.0 ms');
|
||||
assert.match(text('http5xx-detail'), /1\.0\/s/);
|
||||
assert.match(text('timeouts-detail'), /2\.0\/s/);
|
||||
assert.match(text('network-detail'), /1\.0\/s/);
|
||||
assert.match(text('retries-detail'), /1\.0\/s.*backendWidgetRetriesSuccess: 2/);
|
||||
assert.match(text('queue-detail'), /1\.0\/s.*backendWidgetQueueRejected: 1/);
|
||||
|
||||
// Repaint on toggles/translation must not produce additional rate samples.
|
||||
now = 3500;
|
||||
module.render(next, [], false);
|
||||
assert.match(text('errors-detail'), /1\.0\/s/);
|
||||
// Reset of a member's counters should never be treated as an error burst.
|
||||
now = 5000;
|
||||
const restarted = structuredClone(next);
|
||||
restarted.backend_pools[0].members[0].requests = 2;
|
||||
restarted.backend_pools[0].members[0].errors = 0;
|
||||
restarted.backend_pools[0].members[0].bytes_in = 999999;
|
||||
module.render(restarted, []);
|
||||
assert.match(text('errors-detail'), /0\.0\/s/);
|
||||
assert.equal(text('traffic'), '0 B/s');
|
||||
assert.equal(text('requests'), '0.0/s');
|
||||
// Reconfiguration invalidates prior generations, first sample is a baseline.
|
||||
now = 8000;
|
||||
restarted.revision = 2;
|
||||
module.render(restarted, []);
|
||||
assert.match(text('errors-detail'), /0\.0\/s/);
|
||||
// Missing origin data must show neutral state and not previously cached values.
|
||||
module.render({ revision: 3, backend_pools: [] }, []);
|
||||
assert.equal(text('health'), '-');
|
||||
assert.equal(text('traffic'), '-');
|
||||
assert.equal(text('protocols-detail'), 'backendWidgetUnavailable');
|
||||
assert.equal(text('requests'), '-');
|
||||
assert.equal(text('capacity'), '-');
|
||||
for (const name of ['http5xx', 'timeouts', 'network', 'retries', 'queue', 'rotations'])
|
||||
assert.equal(text(name), '-', `neutral empty state for ${name}`);
|
||||
module.render(data, [...names]);
|
||||
assert(names.every(name => cards.get(name).hidden));
|
||||
|
||||
// Origin protocol editor must preserve legacy HTTPS fallback, expose strict
|
||||
// H2 as a separate new option, and distinguish cleartext h2c.
|
||||
const protocolHelp = fs.readFileSync('src/admin/web/vhosts/protocol.js', 'utf8');
|
||||
const element = () => ({ dataset: {}, textContent: '', hidden: false });
|
||||
const originCtx = { el: element, t: key => key };
|
||||
vm.createContext(originCtx);
|
||||
vm.runInContext(protocolHelp, originCtx);
|
||||
const mounted = [];
|
||||
const panel = { append(...items) { mounted.push(...items); } };
|
||||
const select = { value: 'http2', events: {}, addEventListener(type, handler) { this.events[type] = handler; } };
|
||||
const url = { value: 'https://origin.example', events: {}, addEventListener(type, handler) { this.events[type] = handler; } };
|
||||
originCtx.attachOriginProtocolDetails(panel, select, url);
|
||||
assert.equal(mounted.length, 2);
|
||||
assert.equal(mounted[0].dataset.i18n, 'vhProtocolLegacyHttp2HttpsDetails');
|
||||
assert.equal(mounted[1].hidden, false, 'Saved HTTPS H2 needs a compatibility warning');
|
||||
select.value = 'auto'; select.events.change();
|
||||
assert.equal(mounted[0].dataset.i18n, 'vhProtocolAutoHttpsDetails');
|
||||
assert.equal(mounted[1].hidden, true);
|
||||
url.value = 'http://origin.example'; url.events.input();
|
||||
assert.equal(mounted[0].dataset.i18n, 'vhProtocolAutoHttpDetails');
|
||||
select.value = 'http2'; select.events.change();
|
||||
assert.equal(mounted[0].dataset.i18n, 'vhProtocolLegacyHttp2HttpDetails');
|
||||
assert.equal(mounted[1].hidden, true);
|
||||
url.value = 'https://origin.example'; url.events.input();
|
||||
assert.equal(mounted[1].hidden, false);
|
||||
select.value = 'h2'; select.events.change();
|
||||
assert.equal(mounted[0].dataset.i18n, 'vhProtocolH2HttpsDetails');
|
||||
assert.equal(mounted[1].hidden, true);
|
||||
url.value = 'http://origin.example'; url.events.input();
|
||||
assert.equal(mounted[0].dataset.i18n, 'vhProtocolH2HttpDetails');
|
||||
// Strict H2 is tracked independently from the backwards-compatible value.
|
||||
const strict = structuredClone(data);
|
||||
strict.backend_pools[0].members[2].protocol = 'h2';
|
||||
module.render(strict, [], false);
|
||||
assert.match(text('protocols-detail'), /backendWidgetModeCompat 1.*backendWidgetModeStrict 1.*backendWidgetH2c: 2/);
|
||||
assert.equal(get('backend-mode-h2').style.flexGrow, '1');
|
||||
console.log('PASS: 14 opt-in HTTP backend cards, ALPN mode distinction, capacity, request rates, fallback UI');
|
||||
Reference in new issue
Block a user