181 lines
11 KiB
JavaScript
181 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import test from 'node:test';
|
||
|
||
import { readStyleSource } from './style-source.js';
|
||
|
||
const root = path.resolve(import.meta.dirname, '../..');
|
||
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
|
||
const feature = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
|
||
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/instructionBlocks.ts'), 'utf8');
|
||
const prometheus = fs.readFileSync(path.join(root, 'src/web/features/instructions/prometheus.ts'), 'utf8');
|
||
const styles = readStyleSource(root);
|
||
const dashboard = JSON.parse(fs.readFileSync(path.join(root, 'monitoring/grafana/harbor-gateway.json'), 'utf8'));
|
||
|
||
test('Gateway info drawer contains copyable Prometheus and Grafana instructions', () => {
|
||
assert.match(instructions, /\.\.\.\(isGateway \? \[\{/);
|
||
assert.match(instructions, /id: 'prometheus'/);
|
||
assert.match(instructions, /title: 'Prometheus и Grafana'/);
|
||
assert.match(instructions, /prometheusScrapeConfig\(controlHost\)/);
|
||
assert.match(instructions, /label: 'prometheus\.yml'/);
|
||
assert.match(instructions, /label: 'Grafana dashboard'/);
|
||
assert.match(prometheus, /monitoring\/grafana\/harbor-gateway\.json/);
|
||
assert.match(prometheus, /scrape_interval: 30s[\s\S]*metrics_path: \/metrics/);
|
||
assert.match(overview, /const controlHost = window\.location\.host/);
|
||
assert.match(feature, /<CopyButton[\s\S]*label="Скопировать"/);
|
||
assert.doesNotMatch(`${overview}\n${feature}`, /prometheus-toggle|Prometheus<\/span><\/button>/);
|
||
assert.match(styles, /\.client-instruction-copy-button \{[\s\S]*width: 104px;[\s\S]*min-width: 104px/);
|
||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-copy-feedback/);
|
||
});
|
||
|
||
test('Grafana dashboard uses a compact user-first layout and one device scope', () => {
|
||
const flattenPanels = (items) => items.flatMap((item) => [item, ...flattenPanels(item.panels || [])]);
|
||
const panels = flattenPanels(dashboard.panels);
|
||
const byId = new Map(panels.map((panel) => [panel.id, panel]));
|
||
const expressions = panels.flatMap((panel) => panel.targets || []).map(({ expr }) => expr).filter(Boolean);
|
||
const deviceVariable = dashboard.templating.list[0];
|
||
const freshness = byId.get(2);
|
||
const speed = byId.get(3);
|
||
const devices = byId.get(5);
|
||
const currentDownload = byId.get(6);
|
||
const currentUpload = byId.get(7);
|
||
const services = byId.get(8);
|
||
const domains = byId.get(9);
|
||
const totalTraffic = byId.get(1);
|
||
const sourceOutbounds = byId.get(4);
|
||
const singboxOutbounds = byId.get(15);
|
||
const directPaths = byId.get(16);
|
||
const appliedPolicy = byId.get(17);
|
||
const technical = byId.get(13);
|
||
|
||
assert.equal(dashboard.title, 'Harbor Gateway: трафик');
|
||
assert.equal(dashboard.refresh, '30s');
|
||
assert.equal(dashboard.version, 10);
|
||
assert.deepEqual(dashboard.panels.map(({ id }) => id), [10, 6, 7, 1, 15, 2, 17, 3, 5, 12, 8, 9, 13]);
|
||
assert.deepEqual(dashboard.panels.filter(({ type }) => type === 'row').map(({ title }) => title), [
|
||
'Обзор',
|
||
'Куда уходит трафик',
|
||
'Техническая детализация',
|
||
]);
|
||
assert.equal(technical.collapsed, true);
|
||
assert.deepEqual(technical.panels.map(({ id }) => id), [4, 16]);
|
||
assert.equal(byId.has(11), false);
|
||
assert.equal(byId.has(14), false);
|
||
|
||
assert.deepEqual(appliedPolicy.gridPos, { h: 4, w: 16, x: 0, y: 5 });
|
||
assert.deepEqual(speed.gridPos, { h: 12, w: 16, x: 0, y: 9 });
|
||
assert.deepEqual(devices.gridPos, { h: 16, w: 8, x: 16, y: 5 });
|
||
assert.deepEqual(services.gridPos, { h: 15, w: 9, x: 0, y: 22 });
|
||
assert.deepEqual(domains.gridPos, { h: 15, w: 15, x: 9, y: 22 });
|
||
assert.deepEqual(technical.gridPos, { h: 1, w: 24, x: 0, y: 37 });
|
||
assert.equal(devices.options.footer.enablePagination, false);
|
||
assert.equal(domains.options.footer.enablePagination, false);
|
||
|
||
assert.equal(currentDownload.type, 'stat');
|
||
assert.equal(currentUpload.type, 'stat');
|
||
assert.equal(currentDownload.targets[0].instant, true);
|
||
assert.equal(currentUpload.targets[0].instant, true);
|
||
assert.match(currentDownload.title, /в конце/);
|
||
assert.match(currentUpload.title, /в конце/);
|
||
assert.match(currentDownload.description, /адаптивным окном усреднения/);
|
||
assert.match(currentUpload.description, /адаптивным окном усреднения/);
|
||
assert.match(currentDownload.targets[0].expr, /device_id=~"\$device_id"[\s\S]*direction="download"[\s\S]*\[\$__rate_interval\]/);
|
||
assert.match(currentUpload.targets[0].expr, /device_id=~"\$device_id"[\s\S]*direction="upload"[\s\S]*\[\$__rate_interval\]/);
|
||
assert.equal(totalTraffic.targets[0].expr, 'sum(increase(harbor_device_traffic_bytes_total{device_id=~"$device_id"}[$__range]))');
|
||
|
||
assert.equal(speed.fieldConfig.defaults.custom.axisCenteredZero, true);
|
||
assert.deepEqual(speed.fieldConfig.defaults.custom.scaleDistribution, {
|
||
linearThreshold: 1048576,
|
||
log: 2,
|
||
type: 'symlog',
|
||
});
|
||
assert.equal(speed.title, 'Фактический VPN / Direct');
|
||
assert.deepEqual(speed.targets.map(({ refId }) => refId), ['A', 'B', 'C', 'D', 'E', 'F']);
|
||
assert.ok(speed.targets.every(({ expr }) => expr.includes('device_id=~"$device_id"') && expr.includes('[$__rate_interval]') && !expr.includes('[5m]')));
|
||
assert.ok(speed.targets.filter(({ refId }) => ['A', 'B', 'E', 'F'].includes(refId))
|
||
.every(({ expr }) => expr.includes('harbor_device_singbox_tracked_bytes_total')));
|
||
assert.ok(speed.targets.every(({ expr }) => !expr.includes('source=')));
|
||
assert.ok(speed.targets.filter(({ refId }) => ['C', 'D'].includes(refId)).every(({ expr }) => (
|
||
expr.includes('harbor_device_singbox_tracked_bytes_total')
|
||
&& expr.includes('harbor_device_direct_ipv4_packet_bytes_total')
|
||
&& expr.includes('or vector(0)')
|
||
)));
|
||
assert.deepEqual(speed.fieldConfig.overrides.filter((override) => (
|
||
override.matcher.id === 'byFrameRefID'
|
||
&& override.properties.some(({ id, value }) => id === 'custom.transform' && value === 'negative-Y')
|
||
)).map(({ matcher }) => matcher.options), ['B', 'D', 'F']);
|
||
assert.equal(speed.options.legend.placement, 'right');
|
||
assert.deepEqual(speed.options.legend.calcs, ['lastNotNull', 'mean', 'max']);
|
||
|
||
assert.equal(appliedPolicy.type, 'state-timeline');
|
||
assert.equal(appliedPolicy.title, 'Применённый режим');
|
||
assert.match(appliedPolicy.description, /Direct[\s\S]*VPN через явно настроенный Harbor Proxy/);
|
||
assert.match(appliedPolicy.targets[0].expr, /harbor_device_applied_policy\{device_id=~"\$device_id"\}/);
|
||
assert.match(appliedPolicy.targets[0].expr, /group_left \(name, ip\)[\s\S]*harbor_device_info/);
|
||
assert.deepEqual(appliedPolicy.fieldConfig.defaults.mappings[0].options, {
|
||
0: { color: 'orange', index: 0, text: 'Direct' },
|
||
1: { color: 'green', index: 1, text: 'VPN' },
|
||
});
|
||
assert.equal(appliedPolicy.fieldConfig.defaults.noValue, 'Нет данных');
|
||
|
||
assert.deepEqual(singboxOutbounds.targets.map(({ legendFormat }) => legendFormat), ['VPN', 'Direct', 'Не определено']);
|
||
assert.ok(singboxOutbounds.targets.every(({ expr }) => (
|
||
expr.includes('harbor_device_singbox_tracked_bytes_total')
|
||
&& expr.includes('device_id=~"$device_id"')
|
||
&& expr.includes('[$__range]')
|
||
)));
|
||
assert.match(singboxOutbounds.targets[0].expr, /outbound="vpn"/);
|
||
assert.doesNotMatch(singboxOutbounds.targets[0].expr, /source=/);
|
||
assert.match(singboxOutbounds.targets[1].expr, /harbor_device_singbox_tracked_bytes_total[\s\S]*outbound="direct"/);
|
||
assert.match(singboxOutbounds.targets[1].expr, /harbor_device_direct_ipv4_packet_bytes_total/);
|
||
assert.match(singboxOutbounds.targets[1].expr, /or vector\(0\)/);
|
||
assert.match(singboxOutbounds.targets[2].expr, /outbound="unknown"/);
|
||
assert.equal(singboxOutbounds.fieldConfig.defaults.noValue, 'Нет данных');
|
||
assert.equal(sourceOutbounds.title, 'Вход × выход sing-box');
|
||
assert.match(sourceOutbounds.targets[0].expr, /sum by \(source, outbound\)[\s\S]*harbor_device_singbox_tracked_bytes_total/);
|
||
assert.equal(sourceOutbounds.targets[0].legendFormat, '{{source}} · {{outbound}}');
|
||
assert.match(sourceOutbounds.description, /proxy · vpn/);
|
||
assert.deepEqual(directPaths.targets.map(({ legendFormat }) => legendFormat), ['Через sing-box', 'Linux мимо sing-box']);
|
||
assert.match(directPaths.targets[0].expr, /harbor_device_singbox_tracked_bytes_total[\s\S]*outbound="direct"/);
|
||
assert.match(directPaths.targets[1].expr, /harbor_device_direct_ipv4_packet_bytes_total/);
|
||
assert.match(directPaths.description, /приблизительной оценкой/);
|
||
|
||
assert.match(devices.targets[0].expr, /^\(sum by \(device_id\)[\s\S]*increase\(harbor_device_traffic_bytes_total\[\$__range\]\)[\s\S]*> 0[\s\S]*group_left \(name, ip\)/);
|
||
assert.doesNotMatch(devices.targets[0].expr, /topk/);
|
||
assert.equal(devices.options.sortBy[0].desc, true);
|
||
const deviceNameOverride = devices.fieldConfig.overrides.find(({ matcher }) => matcher.options === 'name');
|
||
const deviceLink = deviceNameOverride.properties.find(({ id }) => id === 'links').value[0];
|
||
assert.equal(deviceLink.url, '?' + '$' + '{__url_time_range}&var-device_id=' + '$' + '{__data.fields["device_id"]}');
|
||
assert.ok(devices.fieldConfig.overrides.some(({ properties }) => properties.some(({ id }) => id === 'custom.width')));
|
||
|
||
assert.deepEqual(freshness.fieldConfig.defaults.thresholds.steps.map(({ value }) => value), [null, 60, 120]);
|
||
assert.equal(freshness.fieldConfig.defaults.noValue, 'Нет данных');
|
||
assert.equal(freshness.title, 'Обновление данных');
|
||
assert.equal(freshness.targets.length, 1);
|
||
assert.match(freshness.targets[0].expr, /harbor_traffic_last_observed_timestamp_seconds/);
|
||
assert.match(freshness.targets[0].expr, /harbor_domain_traffic_last_observed_timestamp_seconds/);
|
||
assert.match(freshness.targets[0].expr, /harbor_direct_ipv4_packet_last_observed_timestamp_seconds/);
|
||
|
||
assert.equal(deviceVariable.name, 'device_id');
|
||
assert.equal(deviceVariable.multi, false);
|
||
assert.equal(deviceVariable.includeAll, true);
|
||
assert.equal(deviceVariable.allValue, '.*');
|
||
assert.equal(deviceVariable.current.value, '$__all');
|
||
assert.equal(deviceVariable.refresh, 2);
|
||
assert.equal(dashboard.templating.list.length, 1);
|
||
assert.match(deviceVariable.query.query, /query_result\(label_join\(/);
|
||
assert.match(deviceVariable.regex, /<text>/);
|
||
assert.match(deviceVariable.regex, /<value>/);
|
||
|
||
assert.match(services.targets[0].expr, /device_id=~"\$device_id"[\s\S]*> 0\)$/);
|
||
assert.deepEqual(domains.transformations[0].options.indexByName, { service: 0, domain: 1, Value: 2 });
|
||
assert.match(domains.targets[0].expr, /^topk\(15,[\s\S]*device_id=~"\$device_id"[\s\S]*> 0\)$/);
|
||
assert.ok(panels.every(({ title = '', description = '' }) => !/Накоплено|за всё время/.test([title, description].join(' '))));
|
||
assert.ok(panels
|
||
.filter(({ type }) => ['bargauge', 'table'].includes(type))
|
||
.flatMap(({ targets }) => targets || [])
|
||
.every(({ expr }) => expr.includes('> 0')));
|
||
assert.ok(expressions.every((expression) => !expression.includes('harbor_singbox_tracked_bytes_total')));
|
||
});
|