Add network identity diagnostics
This commit is contained in:
@@ -4,6 +4,7 @@ import net from 'node:net';
|
|||||||
import {
|
import {
|
||||||
assessConnectivity,
|
assessConnectivity,
|
||||||
CONNECTIVITY_IP_SOURCES,
|
CONNECTIVITY_IP_SOURCES,
|
||||||
|
CONNECTIVITY_NETWORK_SOURCE,
|
||||||
CONNECTIVITY_SITES,
|
CONNECTIVITY_SITES,
|
||||||
MAX_CUSTOM_DIAGNOSTIC_SERVICES,
|
MAX_CUSTOM_DIAGNOSTIC_SERVICES,
|
||||||
} from '../../shared/connectivityDiagnostics.js';
|
} from '../../shared/connectivityDiagnostics.js';
|
||||||
@@ -66,6 +67,16 @@ interface IpProbeResult {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NetworkProbeResult {
|
||||||
|
address: string | null;
|
||||||
|
asn: string | null;
|
||||||
|
provider: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
attempts: number;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface SiteProbeResult {
|
interface SiteProbeResult {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -80,6 +91,7 @@ interface SiteProbeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiagnosticTarget =
|
type DiagnosticTarget =
|
||||||
|
| { kind: 'network' }
|
||||||
| { kind: 'ip'; probe: IpProbe }
|
| { kind: 'ip'; probe: IpProbe }
|
||||||
| { kind: 'site'; probe: SiteProbe };
|
| { kind: 'site'; probe: SiteProbe };
|
||||||
|
|
||||||
@@ -261,6 +273,55 @@ async function publicIps(path: PathKind, proxyPort: number, execute: CurlExecuto
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function text(value: unknown) {
|
||||||
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNetwork(body: string): Omit<NetworkProbeResult, 'attempts' | 'error'> | null {
|
||||||
|
try {
|
||||||
|
const value = record(JSON.parse(body));
|
||||||
|
const connection = record(value.connection);
|
||||||
|
const address = text(value.ip);
|
||||||
|
if (value.success === false || !address || net.isIP(address) !== 4) return null;
|
||||||
|
const number = Number(connection.asn);
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
asn: Number.isSafeInteger(number) && number > 0 ? `AS${number}` : null,
|
||||||
|
provider: text(connection.isp) || text(connection.org),
|
||||||
|
city: text(value.city),
|
||||||
|
country: text(value.country_code) || text(value.country),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function networkProbe(
|
||||||
|
path: PathKind,
|
||||||
|
proxyPort: number,
|
||||||
|
execute: CurlExecutor,
|
||||||
|
sampleCount = 1,
|
||||||
|
): Promise<NetworkProbeResult> {
|
||||||
|
const samples: Array<RequestResult & { network: ReturnType<typeof parseNetwork> }> = [];
|
||||||
|
for (let attempt = 0; attempt < sampleCount; attempt += 1) {
|
||||||
|
const result = await request(CONNECTIVITY_NETWORK_SOURCE, path, proxyPort, execute, { body: true, ipv4: true });
|
||||||
|
samples.push({ ...result, network: result.ok ? parseNetwork(result.body) : null });
|
||||||
|
}
|
||||||
|
const selected = mostCommon(samples
|
||||||
|
.map(({ network }) => network && JSON.stringify(network))
|
||||||
|
.filter((value): value is string => Boolean(value)));
|
||||||
|
const network = selected ? JSON.parse(selected) as ReturnType<typeof parseNetwork> : null;
|
||||||
|
return {
|
||||||
|
address: network?.address || null,
|
||||||
|
asn: network?.asn || null,
|
||||||
|
provider: network?.provider || null,
|
||||||
|
city: network?.city || null,
|
||||||
|
country: network?.country || null,
|
||||||
|
attempts: samples.length,
|
||||||
|
error: network ? null : samples.at(-1)?.error || 'invalid network response',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isPublicAddress(address: string, family: number) {
|
function isPublicAddress(address: string, family: number) {
|
||||||
const type = family === 4 ? 'ipv4' : family === 6 ? 'ipv6' : '';
|
const type = family === 4 ? 'ipv4' : family === 6 ? 'ipv6' : '';
|
||||||
const blocked = family === 4 ? BLOCKED_IPV4_ADDRESSES : BLOCKED_IPV6_ADDRESSES;
|
const blocked = family === 4 ? BLOCKED_IPV4_ADDRESSES : BLOCKED_IPV6_ADDRESSES;
|
||||||
@@ -361,7 +422,8 @@ async function probePath(
|
|||||||
execute: CurlExecutor,
|
execute: CurlExecutor,
|
||||||
sites: SiteProbe[],
|
sites: SiteProbe[],
|
||||||
): Promise<ConnectivityPathResult> {
|
): Promise<ConnectivityPathResult> {
|
||||||
const [ip, siteResults] = await Promise.all([
|
const [network, ip, siteResults] = await Promise.all([
|
||||||
|
networkProbe(path, proxyPort, execute),
|
||||||
publicIps(path, proxyPort, execute),
|
publicIps(path, proxyPort, execute),
|
||||||
Promise.all(sites.map((probe) => siteProbe(probe, path, proxyPort, execute))),
|
Promise.all(sites.map((probe) => siteProbe(probe, path, proxyPort, execute))),
|
||||||
]);
|
]);
|
||||||
@@ -370,6 +432,7 @@ async function probePath(
|
|||||||
internetAvailable: Boolean(
|
internetAvailable: Boolean(
|
||||||
ip.ipv4.addresses.length || ip.ipv6 || siteResults.some((site) => site.status !== 'unavailable'),
|
ip.ipv4.addresses.length || ip.ipv6 || siteResults.some((site) => site.status !== 'unavailable'),
|
||||||
),
|
),
|
||||||
|
network,
|
||||||
...ip,
|
...ip,
|
||||||
sites: siteResults,
|
sites: siteResults,
|
||||||
};
|
};
|
||||||
@@ -389,6 +452,7 @@ function unavailablePath(): ConnectivityPathResult {
|
|||||||
|
|
||||||
function resolveTarget(targetId: unknown, sites: SiteProbe[]): DiagnosticTarget | null {
|
function resolveTarget(targetId: unknown, sites: SiteProbe[]): DiagnosticTarget | null {
|
||||||
if (typeof targetId !== 'string') return null;
|
if (typeof targetId !== 'string') return null;
|
||||||
|
if (targetId === CONNECTIVITY_NETWORK_SOURCE.id) return { kind: 'network' };
|
||||||
if (targetId.startsWith('ip:')) {
|
if (targetId.startsWith('ip:')) {
|
||||||
const probe = IP_PROBES.find(({ id }) => id === targetId.slice(3));
|
const probe = IP_PROBES.find(({ id }) => id === targetId.slice(3));
|
||||||
return probe ? { kind: 'ip', probe } : null;
|
return probe ? { kind: 'ip', probe } : null;
|
||||||
@@ -406,6 +470,9 @@ async function probeTarget(
|
|||||||
proxyPort: number,
|
proxyPort: number,
|
||||||
execute: CurlExecutor,
|
execute: CurlExecutor,
|
||||||
): Promise<ConnectivityPathResult> {
|
): Promise<ConnectivityPathResult> {
|
||||||
|
const network = target.kind === 'network'
|
||||||
|
? await networkProbe(path, proxyPort, execute, TARGET_SAMPLE_COUNT)
|
||||||
|
: null;
|
||||||
const ip = target.kind === 'ip'
|
const ip = target.kind === 'ip'
|
||||||
? await ipProbe(target.probe, path, proxyPort, execute, TARGET_SAMPLE_COUNT)
|
? await ipProbe(target.probe, path, proxyPort, execute, TARGET_SAMPLE_COUNT)
|
||||||
: null;
|
: null;
|
||||||
@@ -417,7 +484,8 @@ async function probeTarget(
|
|||||||
const sites = site ? [site] : [];
|
const sites = site ? [site] : [];
|
||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
internetAvailable: Boolean(ip?.address || (site && site.status !== 'unavailable')),
|
internetAvailable: Boolean(network?.address || ip?.address || (site && site.status !== 'unavailable')),
|
||||||
|
...(network ? { network } : {}),
|
||||||
ipv4: {
|
ipv4: {
|
||||||
addresses: ipv4Sources.map(({ address }) => address).filter((value): value is string => Boolean(value)),
|
addresses: ipv4Sources.map(({ address }) => address).filter((value): value is string => Boolean(value)),
|
||||||
sources: ipv4Sources,
|
sources: ipv4Sources,
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ export const CONNECTIVITY_IP_SOURCES = Object.freeze([
|
|||||||
{ id: 'ipify-v6', label: 'ipify IPv6', family: 6, url: 'https://api6.ipify.org' },
|
{ id: 'ipify-v6', label: 'ipify IPv6', family: 6, url: 'https://api6.ipify.org' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const CONNECTIVITY_NETWORK_SOURCE = Object.freeze({
|
||||||
|
id: 'network',
|
||||||
|
label: 'Сеть',
|
||||||
|
url: 'https://ipwho.is/',
|
||||||
|
});
|
||||||
|
|
||||||
export const CONNECTIVITY_SITES = Object.freeze([
|
export const CONNECTIVITY_SITES = Object.freeze([
|
||||||
{ id: 'google', label: 'Google', url: 'https://www.google.com/generate_204' },
|
{ id: 'google', label: 'Google', url: 'https://www.google.com/generate_204' },
|
||||||
{ id: 'youtube', label: 'YouTube', url: 'https://www.youtube.com/generate_204' },
|
{ id: 'youtube', label: 'YouTube', url: 'https://www.youtube.com/generate_204' },
|
||||||
@@ -32,6 +38,7 @@ export interface ConnectivityPathResult {
|
|||||||
internetAvailable: boolean;
|
internetAvailable: boolean;
|
||||||
ipv4: { addresses: string[]; [key: string]: unknown };
|
ipv4: { addresses: string[]; [key: string]: unknown };
|
||||||
ipv6: string | null;
|
ipv6: string | null;
|
||||||
|
network?: unknown;
|
||||||
sites: ConnectivitySiteResult[];
|
sites: ConnectivitySiteResult[];
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.24.2',
|
macClient: '0.25.0',
|
||||||
gatewayClient: '0.25.2',
|
gatewayClient: '0.26.0',
|
||||||
gatewayBackend: '0.25.1',
|
gatewayBackend: '0.26.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ParsedVersion {
|
export interface ParsedVersion {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Drawer } from '../../ui/Drawer.js';
|
|||||||
import { Tooltip } from '../../ui/Tooltip.js';
|
import { Tooltip } from '../../ui/Tooltip.js';
|
||||||
import {
|
import {
|
||||||
CONNECTIVITY_IP_SOURCES,
|
CONNECTIVITY_IP_SOURCES,
|
||||||
|
CONNECTIVITY_NETWORK_SOURCE,
|
||||||
CONNECTIVITY_SITES,
|
CONNECTIVITY_SITES,
|
||||||
MAX_CUSTOM_DIAGNOSTIC_SERVICES,
|
MAX_CUSTOM_DIAGNOSTIC_SERVICES,
|
||||||
} from '../../../shared/connectivityDiagnostics.js';
|
} from '../../../shared/connectivityDiagnostics.js';
|
||||||
@@ -134,6 +135,32 @@ function IpCell({
|
|||||||
return <code aria-label={`${route}: ${value.address}`}>{value.address}</code>;
|
return <code aria-label={`${route}: ${value.address}`}>{value.address}</code>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NetworkCell({
|
||||||
|
path,
|
||||||
|
pending,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
path: DiagnosticPath | undefined;
|
||||||
|
pending: boolean;
|
||||||
|
route: string;
|
||||||
|
}) {
|
||||||
|
let status: StatusValue | null = null;
|
||||||
|
if (path?.available === false) status = ['is-muted', '—'];
|
||||||
|
else if (pending) status = ['is-running', 'Тестируем'];
|
||||||
|
else if (!path?.available) status = ['is-muted', '—'];
|
||||||
|
const identity = [path?.network?.asn, path?.network?.provider].filter(Boolean).join(' · ');
|
||||||
|
const location = [path?.network?.city, path?.network?.country].filter(Boolean).join(', ');
|
||||||
|
if (!status && !identity && !location) status = ['is-error', 'Нет ответа'];
|
||||||
|
if (status) return <>
|
||||||
|
<Status value={status} route={route} /><br />
|
||||||
|
<span className="client-diagnostics-status is-muted" aria-hidden="true"> </span>
|
||||||
|
</>;
|
||||||
|
return <span aria-label={`${route}: ${[identity, location].filter(Boolean).join(', ')}`}>
|
||||||
|
<span className="client-diagnostics-status">{identity || location}</span><br />
|
||||||
|
<span className="client-diagnostics-status is-muted">{identity && location ? location : <> </>}</span>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
function mergeItems<T>(previous: T[] = [], incoming: T[] = [], key: (item: T) => string) {
|
function mergeItems<T>(previous: T[] = [], incoming: T[] = [], key: (item: T) => string) {
|
||||||
const merged = [...previous];
|
const merged = [...previous];
|
||||||
for (const item of incoming) {
|
for (const item of incoming) {
|
||||||
@@ -161,6 +188,7 @@ function mergePath(previous: DiagnosticPath | undefined, incoming: DiagnosticPat
|
|||||||
ipv4: { addresses, sources },
|
ipv4: { addresses, sources },
|
||||||
ipv6,
|
ipv6,
|
||||||
ipv6Source,
|
ipv6Source,
|
||||||
|
network: incoming.available === false ? null : incoming.network ?? previous?.network ?? null,
|
||||||
sites,
|
sites,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -242,6 +270,7 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
try {
|
try {
|
||||||
let next = result;
|
let next = result;
|
||||||
const targets = [
|
const targets = [
|
||||||
|
CONNECTIVITY_NETWORK_SOURCE.id,
|
||||||
...CONNECTIVITY_IP_SOURCES.map(({ id }) => `ip:${id}`),
|
...CONNECTIVITY_IP_SOURCES.map(({ id }) => `ip:${id}`),
|
||||||
...sites.map(({ id }) => `site:${id}`),
|
...sites.map(({ id }) => `site:${id}`),
|
||||||
];
|
];
|
||||||
@@ -378,7 +407,24 @@ export function ConnectivityDiagnosticsPanel({
|
|||||||
<th>Напрямую</th>
|
<th>Напрямую</th>
|
||||||
<th>VPN{result?.vpn?.server?.label ? ` · ${result.vpn.server.label}` : ''}</th>
|
<th>VPN{result?.vpn?.server?.label ? ` · ${result.vpn.server.label}` : ''}</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>{CONNECTIVITY_IP_SOURCES.map((source) => {
|
<tbody>
|
||||||
|
<tr
|
||||||
|
data-diagnostic-target={CONNECTIVITY_NETWORK_SOURCE.id}
|
||||||
|
className={activeTarget === CONNECTIVITY_NETWORK_SOURCE.id ? 'is-running' : undefined}
|
||||||
|
>
|
||||||
|
<th scope="row">{CONNECTIVITY_NETWORK_SOURCE.label}</th>
|
||||||
|
<td><NetworkCell
|
||||||
|
path={result?.direct}
|
||||||
|
pending={activeTarget === CONNECTIVITY_NETWORK_SOURCE.id}
|
||||||
|
route="Напрямую, сеть"
|
||||||
|
/></td>
|
||||||
|
<td><NetworkCell
|
||||||
|
path={result?.vpn}
|
||||||
|
pending={activeTarget === CONNECTIVITY_NETWORK_SOURCE.id}
|
||||||
|
route="VPN, сеть"
|
||||||
|
/></td>
|
||||||
|
</tr>
|
||||||
|
{CONNECTIVITY_IP_SOURCES.map((source) => {
|
||||||
const target = `ip:${source.id}`;
|
const target = `ip:${source.id}`;
|
||||||
const running = activeTarget === target;
|
const running = activeTarget === target;
|
||||||
return <tr key={source.id} data-diagnostic-target={target} className={running ? 'is-running' : undefined}>
|
return <tr key={source.id} data-diagnostic-target={target} className={running ? 'is-running' : undefined}>
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ export interface DiagnosticSiteResult extends Record<string, unknown> {
|
|||||||
latencyMs: number | null;
|
latencyMs: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiagnosticNetworkResult extends Record<string, unknown> {
|
||||||
|
address: string | null;
|
||||||
|
asn: string | null;
|
||||||
|
provider: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiagnosticServer extends Record<string, unknown> {
|
export interface DiagnosticServer extends Record<string, unknown> {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -26,6 +34,7 @@ export interface DiagnosticPath extends Record<string, unknown> {
|
|||||||
};
|
};
|
||||||
ipv6: string | null;
|
ipv6: string | null;
|
||||||
ipv6Source: DiagnosticIpResult | null;
|
ipv6Source: DiagnosticIpResult | null;
|
||||||
|
network?: DiagnosticNetworkResult | null;
|
||||||
sites: DiagnosticSiteResult[];
|
sites: DiagnosticSiteResult[];
|
||||||
server?: DiagnosticServer | null;
|
server?: DiagnosticServer | null;
|
||||||
}
|
}
|
||||||
@@ -59,6 +68,19 @@ function validSiteResult(value: unknown): value is DiagnosticSiteResult {
|
|||||||
&& nullableNonnegativeNumber(value.latencyMs);
|
&& nullableNonnegativeNumber(value.latencyMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nullableText(value: unknown): value is string | null {
|
||||||
|
return value === null || (typeof value === 'string' && value.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validNetworkResult(value: unknown): value is DiagnosticNetworkResult {
|
||||||
|
return record(value)
|
||||||
|
&& nullableText(value.address)
|
||||||
|
&& nullableText(value.asn)
|
||||||
|
&& nullableText(value.provider)
|
||||||
|
&& nullableText(value.city)
|
||||||
|
&& nullableText(value.country);
|
||||||
|
}
|
||||||
|
|
||||||
function validServer(value: unknown): value is DiagnosticServer | null {
|
function validServer(value: unknown): value is DiagnosticServer | null {
|
||||||
return value === null || (record(value)
|
return value === null || (record(value)
|
||||||
&& typeof value.id === 'string'
|
&& typeof value.id === 'string'
|
||||||
@@ -76,6 +98,7 @@ function validPath(value: unknown): value is DiagnosticPath {
|
|||||||
&& value.ipv4.sources.every(validIpResult)
|
&& value.ipv4.sources.every(validIpResult)
|
||||||
&& (value.ipv6 === null || (typeof value.ipv6 === 'string' && value.ipv6.length > 0))
|
&& (value.ipv6 === null || (typeof value.ipv6 === 'string' && value.ipv6.length > 0))
|
||||||
&& (value.ipv6Source === null || validIpResult(value.ipv6Source))
|
&& (value.ipv6Source === null || validIpResult(value.ipv6Source))
|
||||||
|
&& (!Object.hasOwn(value, 'network') || value.network === null || validNetworkResult(value.network))
|
||||||
&& Array.isArray(value.sites)
|
&& Array.isArray(value.sites)
|
||||||
&& value.sites.every(validSiteResult);
|
&& value.sites.every(validSiteResult);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ test('connectivity diagnostics force separate direct and VPN paths', async () =>
|
|||||||
assert.deepEqual(result.vpn.ipv4.addresses, ['203.0.113.20']);
|
assert.deepEqual(result.vpn.ipv4.addresses, ['203.0.113.20']);
|
||||||
assert.equal(result.direct.ipv6, '2001:db8::10');
|
assert.equal(result.direct.ipv6, '2001:db8::10');
|
||||||
assert.equal(result.vpn.ipv6, '2001:db8::20');
|
assert.equal(result.vpn.ipv6, '2001:db8::20');
|
||||||
|
assert.equal(result.direct.network.error, 'invalid network response');
|
||||||
assert.equal(result.assessment.summary, 'available');
|
assert.equal(result.assessment.summary, 'available');
|
||||||
assert.ok(calls.some((args) => args.includes('--noproxy') && args.includes('*')));
|
assert.ok(calls.some((args) => args.includes('--noproxy') && args.includes('*')));
|
||||||
assert.ok(calls.some((args) => args.includes('--proxy') && args.includes('http://127.0.0.1:18080')));
|
assert.ok(calls.some((args) => args.includes('--proxy') && args.includes('http://127.0.0.1:18080')));
|
||||||
@@ -242,6 +243,48 @@ test('a targeted IP row uses three samples and keeps the majority address', asyn
|
|||||||
assert.equal(result.vpn.ipv4.sources[0].latencyMs, 250);
|
assert.equal(result.vpn.ipv4.sources[0].latencyMs, 250);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a targeted network row reports provider, ASN and location through both forced paths', async () => {
|
||||||
|
const attempts = { direct: 0, vpn: 0 };
|
||||||
|
const calls = [];
|
||||||
|
const execute = async (args) => {
|
||||||
|
calls.push(args);
|
||||||
|
const route = args.includes('--proxy') ? 'vpn' : 'direct';
|
||||||
|
attempts[route] += 1;
|
||||||
|
if (route === 'direct' && attempts.direct === 1) return response('{invalid');
|
||||||
|
return response(JSON.stringify(route === 'vpn' ? {
|
||||||
|
success: true,
|
||||||
|
ip: '203.0.113.20',
|
||||||
|
city: 'Amsterdam',
|
||||||
|
country_code: 'NL',
|
||||||
|
connection: { asn: 9009, isp: 'M247 Europe' },
|
||||||
|
} : {
|
||||||
|
success: true,
|
||||||
|
ip: '198.51.100.10',
|
||||||
|
city: 'Moscow',
|
||||||
|
country_code: 'RU',
|
||||||
|
connection: { asn: 12389, isp: 'Rostelecom' },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const result = await createConnectivityDiagnosticsService({ proxyPort: 18080, execute })
|
||||||
|
.run({ vpnAvailable: true, target: 'network' });
|
||||||
|
|
||||||
|
assert.deepEqual(attempts, { direct: 3, vpn: 3 });
|
||||||
|
assert.deepEqual(result.direct.network, {
|
||||||
|
address: '198.51.100.10',
|
||||||
|
asn: 'AS12389',
|
||||||
|
provider: 'Rostelecom',
|
||||||
|
city: 'Moscow',
|
||||||
|
country: 'RU',
|
||||||
|
attempts: 3,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
assert.equal(result.vpn.network.asn, 'AS9009');
|
||||||
|
assert.equal(result.vpn.network.provider, 'M247 Europe');
|
||||||
|
assert.ok(calls.every((args) => args.at(-1) === 'https://ipwho.is/' && args.includes('--ipv4')));
|
||||||
|
assert.ok(calls.some((args) => args.includes('--noproxy') && args.includes('*')));
|
||||||
|
assert.ok(calls.some((args) => args.includes('--proxy') && args.includes('http://127.0.0.1:18080')));
|
||||||
|
});
|
||||||
|
|
||||||
test('a targeted service row averages three measurements and ignores one transient failure', async () => {
|
test('a targeted service row averages three measurements and ignores one transient failure', async () => {
|
||||||
const attempts = { direct: 0, vpn: 0 };
|
const attempts = { direct: 0, vpn: 0 };
|
||||||
const execute = async (args) => {
|
const execute = async (args) => {
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ const pathResult = {
|
|||||||
ipv4: { addresses: ['198.51.100.10'], sources: [ip] },
|
ipv4: { addresses: ['198.51.100.10'], sources: [ip] },
|
||||||
ipv6: null,
|
ipv6: null,
|
||||||
ipv6Source: null,
|
ipv6Source: null,
|
||||||
|
network: {
|
||||||
|
address: '198.51.100.10',
|
||||||
|
asn: 'AS12389',
|
||||||
|
provider: 'Rostelecom',
|
||||||
|
city: 'Moscow',
|
||||||
|
country: 'RU',
|
||||||
|
},
|
||||||
sites: [site],
|
sites: [site],
|
||||||
};
|
};
|
||||||
const valid = {
|
const valid = {
|
||||||
@@ -62,6 +69,7 @@ test('unknown target and legacy-full results pass one identity-preserving parser
|
|||||||
{ ...valid, direct: { ...valid.direct, available: 'yes' } },
|
{ ...valid, direct: { ...valid.direct, available: 'yes' } },
|
||||||
{ ...valid, direct: { ...valid.direct, ipv4: { addresses: [], sources: [{}] } } },
|
{ ...valid, direct: { ...valid.direct, ipv4: { addresses: [], sources: [{}] } } },
|
||||||
{ ...valid, direct: { ...valid.direct, ipv6: 6 } },
|
{ ...valid, direct: { ...valid.direct, ipv6: 6 } },
|
||||||
|
{ ...valid, direct: { ...valid.direct, network: { ...valid.direct.network, asn: 12389 } } },
|
||||||
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, id: '' }] } },
|
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, id: '' }] } },
|
||||||
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, status: 'blocked' }] } },
|
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, status: 'blocked' }] } },
|
||||||
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, latencyMs: -1 }] } },
|
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, latencyMs: -1 }] } },
|
||||||
@@ -75,7 +83,7 @@ test('unknown target and legacy-full results pass one identity-preserving parser
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('serial probes, storage and editor behavior stay panel-owned', () => {
|
test('serial probes, storage and editor behavior stay panel-owned', () => {
|
||||||
assert.match(panel, /const targets = \[[\s\S]*CONNECTIVITY_IP_SOURCES\.map[\s\S]*sites\.map/);
|
assert.match(panel, /const targets = \[[\s\S]*CONNECTIVITY_NETWORK_SOURCE\.id[\s\S]*CONNECTIVITY_IP_SOURCES\.map[\s\S]*sites\.map/);
|
||||||
assert.match(panel, /for \(const target of targets\) \{[\s\S]*setActiveTarget\(target\)[\s\S]*await runConnectivityDiagnostics\(customServices, target\)[\s\S]*if \(legacyFullResult\) break/);
|
assert.match(panel, /for \(const target of targets\) \{[\s\S]*setActiveTarget\(target\)[\s\S]*await runConnectivityDiagnostics\(customServices, target\)[\s\S]*if \(legacyFullResult\) break/);
|
||||||
assert.match(panel, /CUSTOM_SERVICES_KEY = 'harbor-diagnostic-services'/);
|
assert.match(panel, /CUSTOM_SERVICES_KEY = 'harbor-diagnostic-services'/);
|
||||||
assert.match(panel, /HIDDEN_SERVICES_KEY = 'harbor-hidden-diagnostic-services'/);
|
assert.match(panel, /HIDDEN_SERVICES_KEY = 'harbor-hidden-diagnostic-services'/);
|
||||||
@@ -85,3 +93,10 @@ test('serial probes, storage and editor behavior stay panel-owned', () => {
|
|||||||
assert.match(panel, /retryable: Boolean\(Reflect\.get\(value, 'retryable'\)\)/);
|
assert.match(panel, /retryable: Boolean\(Reflect\.get\(value, 'retryable'\)\)/);
|
||||||
assert.match(panel, /requestDetails\(error\)[\s\S]*requestError\.retryable/);
|
assert.match(panel, /requestDetails\(error\)[\s\S]*requestError\.retryable/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('network identity is one stable compact row for Direct and VPN', () => {
|
||||||
|
assert.match(panel, /data-diagnostic-target=\{CONNECTIVITY_NETWORK_SOURCE\.id\}[\s\S]*<NetworkCell[\s\S]*route="Напрямую, сеть"[\s\S]*<NetworkCell[\s\S]*route="VPN, сеть"/);
|
||||||
|
assert.match(panel, /path\?\.network\?\.asn[\s\S]*path\?\.network\?\.provider[\s\S]*path\?\.network\?\.city[\s\S]*path\?\.network\?\.country/);
|
||||||
|
assert.match(panel, /<Status value=\{status\} route=\{route\} \/><br \/>[\s\S]*client-diagnostics-status is-muted[\s\S]* /);
|
||||||
|
assert.doesNotMatch(panel, /traceroute|tracepath/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const acceptedLedger = {
|
|||||||
ruleDeclarationSequences: '9dacb5d7407717ae041cabf6e8e75af7cfccfb7b5e4c387880468db409e93fbd',
|
ruleDeclarationSequences: '9dacb5d7407717ae041cabf6e8e75af7cfccfb7b5e4c387880468db409e93fbd',
|
||||||
selectors: '8456f64c608e0411bbc56c0c955d25383391f019fa5af0648a72d40564cfee1a',
|
selectors: '8456f64c608e0411bbc56c0c955d25383391f019fa5af0648a72d40564cfee1a',
|
||||||
variableReferences: '42661286e6150115a367068908f79c79951e24fb3fcf8775c2b083bb2d520e38',
|
variableReferences: '42661286e6150115a367068908f79c79951e24fb3fcf8775c2b083bb2d520e38',
|
||||||
witnesses: 'd992876195d2c68c00d687c5a174e5657e3480ad8496251afe52d20c9571e82f',
|
witnesses: '7f44af307fd67fc88e274c1dcc6a21a5e064a8be3895d80fd640715e1af42419',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner',
|
|||||||
|
|
||||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||||
const witnesses = readStyleWitnesses(root);
|
const witnesses = readStyleWitnesses(root);
|
||||||
assert.equal(witnesses.length, 737);
|
assert.equal(witnesses.length, 757);
|
||||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||||
|
|||||||
Reference in New Issue
Block a user