mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Refactor(asn): drop prefix and relationship counts from the ASN info panel
The panel's IPv4/IPv6 prefix counts came from Radar's routes/stats, an endpoint that fails often and, as one of the profile's summary segments, kept complete ASN Profiles out of the edge cache. The ASN Profile already shows prefix counts (from the pfx2as list) and relationships (the CAIDA graph), so the panel drops both: no more routes/stats call, and the Radar asn view no longer reads the local CAIDA snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
846deb50a8
commit
8bcbb91624
14 files changed
+49
-149
No files matched your search
+4
-4
@@ -36,10 +36,10 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
|
||||
failed or late one is just absent. Always 200 `{ asn, status, incomplete,
|
||||
…sections }` (`ok` / `empty` / `error` / `disabled`; an upstream fault is
|
||||
`error`, never `empty`; `incomplete` = `ok` but partial); 502 if all fail.
|
||||
- **Relationship counts follow the graph:** `asn-connectivity` neighbours and
|
||||
the Radar `asn` view's counts come from local CAIDA
|
||||
(`common/as-relationships.js`), dropping the providers the graph walk
|
||||
distrusts.
|
||||
- **Relationship counts follow the graph:** `asn-connectivity` neighbour
|
||||
lists come from local CAIDA (`common/as-relationships.js`), dropping the
|
||||
providers the graph walk distrusts. The Radar `asn` view (ASN Info) carries
|
||||
no prefix or relationship counts — the ASN Profile has both.
|
||||
- **`/api/whois` `?q=`:** IPs (RDAP, whoiser fallback), domains (whoiser, RDAP
|
||||
fallback), `AS<n>` (RDAP autnum); all carry a `__raw` block.
|
||||
- **Offline datasets** (`common/caida-updater.js` rows): fetched when missing at
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// graph walk (buildTopology) drops the non-Tier-1 "providers" of an AS that
|
||||
// is adjacent to the Tier 1 clique past either bar below — CAIDA tends to
|
||||
// misinfer such links for hypergiants. These helpers apply that same rule
|
||||
// when counting, so ASN Info (the Radar `asn` view) and the ASN Profile
|
||||
// neighbour lists never show an upstream the graph leaves out. Nothing is
|
||||
// when counting, so the ASN Profile neighbour lists never show an upstream
|
||||
// the graph leaves out. Nothing is
|
||||
// reclassified: a distrusted provider is simply not counted.
|
||||
|
||||
import { providersOf, peersOf, customersOf, customerCountOf, isTier1, isAsRelLoaded } from './as-rel-db.js';
|
||||
import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from './as-rel-db.js';
|
||||
|
||||
// Peering-only bar: the hypergiant signature is clique peering (Google 12,
|
||||
// Cloudflare 7). Combined bar: multihomed hosters with a real non-T1
|
||||
@@ -38,12 +38,3 @@ export const countedRelationships = (origin, rel = asRelApi) => {
|
||||
const peers = unique(rel.peersOf(origin)).filter((asn) => !transit.has(asn));
|
||||
return { providers, peers, customers };
|
||||
};
|
||||
|
||||
// Counts for the Radar `asn` view (ASN Info): absent ({}) when the snapshot
|
||||
// isn't loaded or the AS has no relationships in it.
|
||||
export const relationshipCounts = (asn, { rel = asRelApi, loaded = isAsRelLoaded } = {}) => {
|
||||
if (!loaded()) return {};
|
||||
const { providers, peers, customers } = countedRelationships(Number(asn), rel);
|
||||
if (!providers.length && !peers.length && !customers.length) return {};
|
||||
return { upstreamCount: providers.length, downstreamCount: customers.length, peerCount: peers.length };
|
||||
};
|
||||
@@ -82,13 +82,11 @@ export const hasMeaningfulField = (body) => Object.values(body || {}).some((valu
|
||||
|
||||
// -- per-section classification of a source's answer ------------------------
|
||||
|
||||
// `answer` is loadAsnSummary's (common/cf-radar.js). Only the Radar-originated
|
||||
// fields decide ok vs empty — the local CAIDA relationship counts in
|
||||
// `summary` say nothing about Radar knowing the AS. With failed segments,
|
||||
// `answer` is loadAsnSummary's (common/cf-radar.js). With failed segments,
|
||||
// data is ok but incomplete; no data is an error, as the missing segments
|
||||
// may have held it.
|
||||
export const classifyRadar = ({ summary, radarFields, failedSegments = [] } = {}) => {
|
||||
if (hasMeaningfulField(radarFields)) {
|
||||
export const classifyRadar = ({ summary, failedSegments = [] } = {}) => {
|
||||
if (hasMeaningfulField(summary)) {
|
||||
return failedSegments.length > 0 ? { ...result('ok', summary), incomplete: true } : result('ok', summary);
|
||||
}
|
||||
if (failedSegments.length > 0) throw new Error(`Radar segments failed: ${failedSegments.join(', ')}`);
|
||||
|
||||
+9
-30
@@ -13,7 +13,6 @@
|
||||
|
||||
import { fetchUpstream } from './fetch-with-timeout.js';
|
||||
import { requireValidASN, requireValidCountry } from './guards.js';
|
||||
import { relationshipCounts } from './as-relationships.js';
|
||||
import { lookupCountryCode } from './maxmind-service.js';
|
||||
import { parseCidr, formatIPv4 } from './ip-math.js';
|
||||
import logger from './logger.js';
|
||||
@@ -52,16 +51,14 @@ export const isCompleteRadarAnswer = (body) => !partialAnswers.has(body);
|
||||
// -- view: asn — per-ASN entity info + 7d HTTP traffic profile --------------
|
||||
|
||||
// The Radar segments backing one response, keyed by the field name
|
||||
// cleanUpResponseData expects. Relationship counts are not Radar's: they come
|
||||
// from the local CAIDA snapshot (relationshipCounts), the same reading the
|
||||
// ASN Profile's topology and neighbour lists use.
|
||||
// cleanUpResponseData expects. Prefix and relationship counts live in the
|
||||
// ASN Profile (pfx2as list, CAIDA graph), not here.
|
||||
const SEGMENTS = {
|
||||
asnInfo: (asn) => `/radar/entities/asns/${asn}`,
|
||||
ipVersion: (asn) => `/radar/http/summary/ip_version?asn=${asn}&dateRange=7d`,
|
||||
httpProtocol: (asn) => `/radar/http/summary/http_protocol?asn=${asn}&dateRange=7d`,
|
||||
deviceType: (asn) => `/radar/http/summary/device_type?asn=${asn}&dateRange=7d`,
|
||||
botType: (asn) => `/radar/http/summary/bot_class?asn=${asn}&dateRange=7d`,
|
||||
routesStats: (asn) => `/radar/bgp/routes/stats?asn=${asn}`,
|
||||
quality: (asn) => `/radar/quality/speed/summary?asn=${asn}`,
|
||||
};
|
||||
|
||||
@@ -105,8 +102,6 @@ function cleanUpResponseData(data) {
|
||||
Mobile_Pct: data.deviceType?.result?.summary_0?.mobile,
|
||||
Bot_Pct: data.botType?.result?.summary_0?.bot,
|
||||
Human_Pct: data.botType?.result?.summary_0?.human,
|
||||
prefixesV4: data.routesStats?.result?.stats?.distinct_prefixes_ipv4,
|
||||
prefixesV6: data.routesStats?.result?.stats?.distinct_prefixes_ipv6,
|
||||
speedDownload: data.quality?.result?.summary_0?.bandwidthDownload,
|
||||
speedUpload: data.quality?.result?.summary_0?.bandwidthUpload,
|
||||
latency: data.quality?.result?.summary_0?.latencyIdle,
|
||||
@@ -116,17 +111,12 @@ function cleanUpResponseData(data) {
|
||||
|
||||
// Format output; a missing value parses to NaN, which filterData drops.
|
||||
function formatData(data) {
|
||||
const { asnName, asnCountryCode, asnOrgName, estimatedUsers, IPv4_Pct, IPv6_Pct, HTTP_Pct, HTTPS_Pct, Desktop_Pct, Mobile_Pct, Bot_Pct, Human_Pct, prefixesV4, prefixesV6, upstreamCount, downstreamCount, peerCount, speedDownload, speedUpload, latency, jitter } = data;
|
||||
const { asnName, asnCountryCode, asnOrgName, estimatedUsers, IPv4_Pct, IPv6_Pct, HTTP_Pct, HTTPS_Pct, Desktop_Pct, Mobile_Pct, Bot_Pct, Human_Pct, speedDownload, speedUpload, latency, jitter } = data;
|
||||
return {
|
||||
asnName,
|
||||
asnCountryCode,
|
||||
asnOrgName,
|
||||
estimatedUsers: parseFloat(estimatedUsers).toLocaleString(),
|
||||
prefixesV4: parseFloat(prefixesV4).toLocaleString(),
|
||||
prefixesV6: parseFloat(prefixesV6).toLocaleString(),
|
||||
upstreamCount: parseFloat(upstreamCount).toLocaleString(),
|
||||
downstreamCount: parseFloat(downstreamCount).toLocaleString(),
|
||||
peerCount: parseFloat(peerCount).toLocaleString(),
|
||||
speedDownload: `${parseFloat(speedDownload).toFixed(1)} Mbps`,
|
||||
speedUpload: `${parseFloat(speedUpload).toFixed(1)} Mbps`,
|
||||
latency: `${Math.round(parseFloat(latency))} ms`,
|
||||
@@ -153,21 +143,14 @@ function filterData(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Settled segment payloads (+ local relationship counts) → response body.
|
||||
// `relCounts` is injectable for tests. Exported for tests.
|
||||
export const shapeAsnProfile = (data, asn, relCounts = relationshipCounts) => {
|
||||
const cleaned = cleanUpResponseData(data);
|
||||
Object.assign(cleaned, relCounts(Number(asn)));
|
||||
return filterData(formatData(cleaned));
|
||||
};
|
||||
// Settled segment payloads → response body. Exported for tests.
|
||||
export const shapeAsnProfile = (data) => filterData(formatData(cleanUpResponseData(data)));
|
||||
|
||||
// One AS's summary, for the asn view and /api/asn-profile:
|
||||
// summary the view's payload (Radar fields + local relationship counts)
|
||||
// radarFields the Radar-originated fields alone, to judge whether Radar
|
||||
// knows the AS
|
||||
// summary the view's payload
|
||||
// failedSegments names of the segments that failed; [] = complete
|
||||
// Throws when every segment failed. `relCounts` is injectable for tests.
|
||||
export const loadAsnSummary = async (asn, relCounts = relationshipCounts) => {
|
||||
// Throws when every segment failed.
|
||||
export const loadAsnSummary = async (asn) => {
|
||||
const { data, failed } = await getAllASNData(asn);
|
||||
if (failed.length === Object.keys(SEGMENTS).length) {
|
||||
throw failed[0].reason instanceof Error
|
||||
@@ -177,11 +160,7 @@ export const loadAsnSummary = async (asn, relCounts = relationshipCounts) => {
|
||||
if (failed.length > 0) {
|
||||
logger.warn({ err: failed[0].reason, asn, segments: failed.map((f) => f.name) }, 'cf-radar: partial Radar segment failure');
|
||||
}
|
||||
return {
|
||||
summary: shapeAsnProfile(data, asn, relCounts),
|
||||
radarFields: shapeAsnProfile(data, asn, () => ({})),
|
||||
failedSegments: failed.map((f) => f.name),
|
||||
};
|
||||
return { summary: shapeAsnProfile(data), failedSegments: failed.map((f) => f.name) };
|
||||
};
|
||||
|
||||
// Partial summaries are served but not edge-cached.
|
||||
|
||||
@@ -86,7 +86,6 @@ const basicInfo = computed(() => {
|
||||
const info = {};
|
||||
const keys = [
|
||||
'asnName', 'asnCountryCode', 'asnOrgName', 'estimatedUsers',
|
||||
'prefixesV4', 'prefixesV6', 'upstreamCount', 'downstreamCount', 'peerCount',
|
||||
];
|
||||
for (const key of keys) {
|
||||
if (data[key]) info[key] = data[key];
|
||||
|
||||
@@ -1408,11 +1408,6 @@
|
||||
"asnOrgName": "AS Organization : ",
|
||||
"asnCountryCode": "AS Country : ",
|
||||
"estimatedUsers": "Estimated Users : ",
|
||||
"prefixesV4": "IPv4 Prefixes : ",
|
||||
"prefixesV6": "IPv6 Prefixes : ",
|
||||
"upstreamCount": "Upstream ASes : ",
|
||||
"downstreamCount": "Downstream ASes : ",
|
||||
"peerCount": "Peering ASes : ",
|
||||
"connectionQuality": "Average Connection Quality: ",
|
||||
"connectionQualityTooltip": "Aggregated from Cloudflare speed tests run by users on this ASN",
|
||||
"speedDownload": "Download : ",
|
||||
|
||||
@@ -1408,11 +1408,6 @@
|
||||
"asnOrgName": "Organisation de l'AS : ",
|
||||
"asnCountryCode": "Région de l'AS : ",
|
||||
"estimatedUsers": "Nombre d'utilisateurs estimé : ",
|
||||
"prefixesV4": "Préfixes IPv4 : ",
|
||||
"prefixesV6": "Préfixes IPv6 : ",
|
||||
"upstreamCount": "AS en amont : ",
|
||||
"downstreamCount": "AS en aval : ",
|
||||
"peerCount": "AS pairs : ",
|
||||
"connectionQuality": "Qualité de connexion moyenne : ",
|
||||
"connectionQualityTooltip": "Agrégé à partir des tests de débit Cloudflare effectués par les utilisateurs de cet ASN",
|
||||
"speedDownload": "Téléchargement : ",
|
||||
|
||||
@@ -1408,11 +1408,6 @@
|
||||
"asnOrgName": "Organização do AS: ",
|
||||
"asnCountryCode": "País do AS: ",
|
||||
"estimatedUsers": "Usuários estimados: ",
|
||||
"prefixesV4": "Prefixos IPv4: ",
|
||||
"prefixesV6": "Prefixos IPv6: ",
|
||||
"upstreamCount": "ASes upstream: ",
|
||||
"downstreamCount": "ASes downstream: ",
|
||||
"peerCount": "ASes de peering: ",
|
||||
"connectionQuality": "Qualidade média da conexão: ",
|
||||
"connectionQualityTooltip": "Agregado dos testes de velocidade da Cloudflare feitos por usuários deste ASN",
|
||||
"speedDownload": "Download: ",
|
||||
|
||||
@@ -1408,11 +1408,6 @@
|
||||
"asnOrgName": "Организация AS: ",
|
||||
"asnCountryCode": "Страна AS: ",
|
||||
"estimatedUsers": "Примерное число пользователей: ",
|
||||
"prefixesV4": "Префиксы IPv4: ",
|
||||
"prefixesV6": "Префиксы IPv6: ",
|
||||
"upstreamCount": "Вышестоящие AS: ",
|
||||
"downstreamCount": "Нижестоящие AS: ",
|
||||
"peerCount": "Пиринговые AS: ",
|
||||
"connectionQuality": "Среднее качество соединения: ",
|
||||
"connectionQualityTooltip": "Агрегировано по замерам скорости Cloudflare, выполненным пользователями этой AS",
|
||||
"speedDownload": "Загрузка: ",
|
||||
|
||||
@@ -1408,11 +1408,6 @@
|
||||
"asnOrgName": "AS 組織名稱:",
|
||||
"asnCountryCode": "AS 地區:",
|
||||
"estimatedUsers": "估計使用者數:",
|
||||
"prefixesV4": "IPv4 前綴數:",
|
||||
"prefixesV6": "IPv6 前綴數:",
|
||||
"upstreamCount": "上游 AS 數:",
|
||||
"downstreamCount": "下游 AS 數:",
|
||||
"peerCount": "對等 AS 數:",
|
||||
"connectionQuality": "平均連線品質:",
|
||||
"connectionQualityTooltip": "基於該 ASN 的使用者與 Cloudflare 測速的彙總結果",
|
||||
"speedDownload": "下載頻寬:",
|
||||
|
||||
@@ -1408,11 +1408,6 @@
|
||||
"asnOrgName": "AS 组织名称:",
|
||||
"asnCountryCode": "AS 地区:",
|
||||
"estimatedUsers": "估计用户数:",
|
||||
"prefixesV4": "IPv4 前缀数:",
|
||||
"prefixesV6": "IPv6 前缀数:",
|
||||
"upstreamCount": "上游 AS 数:",
|
||||
"downstreamCount": "下游 AS 数:",
|
||||
"peerCount": "对等 AS 数:",
|
||||
"connectionQuality": "平均连接质量:",
|
||||
"connectionQualityTooltip": "基于该 ASN 的用户与 Cloudflare 测速的聚合结果",
|
||||
"speedDownload": "下载带宽:",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Tests for common/as-relationships.js — relationship counts that follow
|
||||
// the /api/asn-connectivity graph's own misinference rule: a distrusted
|
||||
// non-Tier-1 provider is not counted (and not reclassified). The hypergiant
|
||||
// fixture checks tiles, lists and graph agree; the graph itself is
|
||||
// fixture checks lists and graph agree; the graph itself is
|
||||
// buildTopology, unchanged and covered in asn-connectivity.test.js.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, distrustsNonTier1Providers,
|
||||
countedRelationships, relationshipCounts,
|
||||
countedRelationships,
|
||||
} from '../common/as-relationships.js';
|
||||
import { buildTopology, buildNeighbours, fillNeighbourNames } from '../api/asn-connectivity.js';
|
||||
|
||||
@@ -84,28 +84,17 @@ describe('countedRelationships', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tiles, lists and graph agree', () => {
|
||||
describe('lists and graph agree', () => {
|
||||
it('hypergiant: no upstream in the counts, none drawn', () => {
|
||||
const neighbours = buildNeighbours(HYPER, rel, () => null);
|
||||
assert.deepEqual(neighbours.counts, { providers: 0, peers: 8, customers: 1 });
|
||||
const graph = buildTopology(HYPER, rel);
|
||||
assert.equal(graph.nodes.some((n) => n.asn === REGIONAL), false);
|
||||
assert.ok(graph.edges.every((e) => e.kind === 'peering'));
|
||||
assert.deepEqual(relationshipCounts(HYPER, { rel, loaded: () => true }),
|
||||
{ upstreamCount: 0, downstreamCount: 1, peerCount: 8 });
|
||||
});
|
||||
|
||||
it('regional: counts match the lists', () => {
|
||||
assert.deepEqual(buildNeighbours(REGIONAL, rel, () => null).counts, { providers: 1, peers: 0, customers: 3 });
|
||||
assert.deepEqual(relationshipCounts(REGIONAL, { rel, loaded: () => true }),
|
||||
{ upstreamCount: 1, downstreamCount: 3, peerCount: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('relationshipCounts', () => {
|
||||
it('is absent without a snapshot or for an AS with no relationships', () => {
|
||||
assert.deepEqual(relationshipCounts(HYPER, { rel, loaded: () => false }), {});
|
||||
assert.deepEqual(relationshipCounts(64512, { rel, loaded: () => true }), {});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,10 +14,8 @@ import { ASN_PROFILE_TIMEOUT_MS } from '../frontend/utils/asn-profile-view.js';
|
||||
|
||||
const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
||||
|
||||
// loadAsnSummary's shape: Radar fields, plus local CAIDA counts in `summary`.
|
||||
const radarAnswer = (radarFields, { local = {}, failed = [] } = {}) => ({
|
||||
summary: { ...radarFields, ...local }, radarFields, failedSegments: failed,
|
||||
});
|
||||
// loadAsnSummary's shape.
|
||||
const radarAnswer = (summary, { failed = [] } = {}) => ({ summary, failedSegments: failed });
|
||||
|
||||
describe('deadlines', () => {
|
||||
it('every deadline sits above the inner timeouts of the source it wraps', () => {
|
||||
@@ -48,14 +46,6 @@ describe('classification', () => {
|
||||
assert.deepEqual(classifyRadar(radarAnswer({ asnName: 'X' })), { status: 'ok', data: { asnName: 'X' } });
|
||||
});
|
||||
|
||||
it('radar: local CAIDA relationship counts alone are not Radar data', () => {
|
||||
const local = { upstreamCount: '3', downstreamCount: '12', peerCount: '40' };
|
||||
assert.deepEqual(classifyRadar(radarAnswer({ prefixesV4: '0' }, { local })), { status: 'empty', data: null });
|
||||
// With Radar data, the section carries the whole summary, counts included.
|
||||
assert.deepEqual(classifyRadar(radarAnswer({ asnName: 'X' }, { local })),
|
||||
{ status: 'ok', data: { asnName: 'X', ...local } });
|
||||
});
|
||||
|
||||
it('radar: failed segments make data incomplete, and no data an error', () => {
|
||||
assert.deepEqual(classifyRadar(radarAnswer({ asnName: 'X' }, { failed: ['quality'] })),
|
||||
{ status: 'ok', data: { asnName: 'X' }, incomplete: true });
|
||||
@@ -165,7 +155,7 @@ describe('composeAsnProfile', () => {
|
||||
|
||||
it('a partial Radar answer without data is an error, never a cacheable empty', async () => {
|
||||
const loaders = buildSectionLoaders(deps({
|
||||
fetchRadarAsn: async () => radarAnswer({}, { local: { peerCount: '4' }, failed: ['asnInfo'] }),
|
||||
fetchRadarAsn: async () => radarAnswer({}, { failed: ['asnInfo'] }),
|
||||
}));
|
||||
const body = await composeAsnProfile(13335, loaders);
|
||||
assert.equal(body.status.radar, 'error');
|
||||
|
||||
+24
-40
@@ -234,31 +234,24 @@ describe('buildTrafficMatrix', () => {
|
||||
});
|
||||
|
||||
describe('shapeAsnProfile', () => {
|
||||
const counts = () => ({ upstreamCount: 2, downstreamCount: 0, peerCount: 7 });
|
||||
|
||||
it('takes prefix counts from routes/stats and relationship counts from the local reading', () => {
|
||||
it('shapes the Radar segments and drops what Radar left out', () => {
|
||||
const body = shapeAsnProfile({
|
||||
routesStats: { result: { stats: { distinct_prefixes_ipv4: 2346, distinct_prefixes_ipv6: 2953, routes_valid: 5162 } } },
|
||||
}, '13335', counts);
|
||||
assert.equal(body.prefixesV4, (2346).toLocaleString());
|
||||
assert.equal(body.prefixesV6, (2953).toLocaleString());
|
||||
assert.equal(body.upstreamCount, '2');
|
||||
assert.equal(body.downstreamCount, '0');
|
||||
assert.equal(body.peerCount, '7');
|
||||
for (const key of ['rpkiValid', 'rpkiInvalid', 'rpkiUnknown', 'rpkiTotal']) {
|
||||
asnInfo: { result: { asn: { name: 'CLOUDFLARENET', estimatedUsers: { estimatedUsers: 1200 } } } },
|
||||
ipVersion: { result: { summary_0: { IPv4: '61.5', IPv6: '38.5' } } },
|
||||
});
|
||||
assert.equal(body.asnName, 'CLOUDFLARENET');
|
||||
assert.equal(body.estimatedUsers, (1200).toLocaleString());
|
||||
assert.equal(body.IPv4_Pct, '61.50%');
|
||||
for (const key of ['speedDownload', 'latency', 'Bot_Pct']) {
|
||||
assert.equal(Object.hasOwn(body, key), false, key);
|
||||
}
|
||||
});
|
||||
|
||||
it('passes the AS number to the relationship reading', () => {
|
||||
let asked;
|
||||
shapeAsnProfile({}, '64500', (asn) => { asked = asn; return {}; });
|
||||
assert.equal(asked, 64500);
|
||||
});
|
||||
|
||||
it('drops relationship fields when the local snapshot has nothing', () => {
|
||||
const body = shapeAsnProfile({ routesStats: { result: { stats: {} } } }, '13335', () => ({}));
|
||||
for (const key of ['upstreamCount', 'downstreamCount', 'peerCount', 'prefixesV4']) {
|
||||
it('carries no prefix or relationship counts (those live in the ASN Profile)', () => {
|
||||
const body = shapeAsnProfile({
|
||||
routesStats: { result: { stats: { distinct_prefixes_ipv4: 2346 } } },
|
||||
});
|
||||
for (const key of ['prefixesV4', 'prefixesV6', 'upstreamCount', 'downstreamCount', 'peerCount']) {
|
||||
assert.equal(Object.hasOwn(body, key), false, key);
|
||||
}
|
||||
});
|
||||
@@ -286,45 +279,36 @@ describe('loadAsnSummary / asn view', () => {
|
||||
};
|
||||
const KNOWN = {
|
||||
'/radar/entities/asns/13335': { result: { asn: { name: 'CLOUDFLARENET', country: 'US' } } },
|
||||
'/radar/bgp/routes/stats': { result: { stats: { distinct_prefixes_ipv4: 2346 } } },
|
||||
};
|
||||
const counts = () => ({ upstreamCount: 2, downstreamCount: 0, peerCount: 7 });
|
||||
|
||||
it('reports a complete answer, with the local counts kept out of radarFields', async () => {
|
||||
it('reports a complete answer, without asking routes/stats', async () => {
|
||||
const asked = [];
|
||||
stubRadar(KNOWN);
|
||||
const { summary, radarFields, failedSegments } = await loadAsnSummary('13335', counts);
|
||||
const stubbed = globalThis.fetch;
|
||||
globalThis.fetch = async (url) => { asked.push(new URL(String(url)).pathname); return stubbed(url); };
|
||||
const { summary, failedSegments } = await loadAsnSummary('13335');
|
||||
assert.deepEqual(failedSegments, []);
|
||||
assert.equal(summary.asnName, 'CLOUDFLARENET');
|
||||
assert.equal(summary.peerCount, '7');
|
||||
assert.equal(radarFields.asnName, 'CLOUDFLARENET');
|
||||
for (const key of ['upstreamCount', 'downstreamCount', 'peerCount']) {
|
||||
assert.equal(Object.hasOwn(radarFields, key), false, key);
|
||||
}
|
||||
});
|
||||
|
||||
it('an AS Radar does not know has only local counts, none in radarFields', async () => {
|
||||
stubRadar({ '/radar/bgp/routes/stats': { result: { stats: { distinct_prefixes_ipv4: 0 } } } });
|
||||
const { summary, radarFields } = await loadAsnSummary('64500', counts);
|
||||
assert.equal(summary.upstreamCount, '2');
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(radarFields)), { prefixesV4: '0' });
|
||||
assert.equal(asked.length, 6);
|
||||
assert.equal(asked.some((path) => path.includes('/bgp/routes/stats')), false);
|
||||
});
|
||||
|
||||
it('names the failed segments and still shapes the rest', async () => {
|
||||
stubRadar({ ...KNOWN, '/radar/http/summary/ip_version': 503, '/radar/quality/speed/summary': 500 });
|
||||
const { summary, failedSegments } = await loadAsnSummary('13335', counts);
|
||||
const { summary, failedSegments } = await loadAsnSummary('13335');
|
||||
assert.deepEqual(failedSegments.sort(), ['ipVersion', 'quality']);
|
||||
assert.equal(summary.asnName, 'CLOUDFLARENET');
|
||||
});
|
||||
|
||||
it("a 404 is Radar's \"no such AS\", not a failed segment", async () => {
|
||||
stubRadar({ '/radar/entities/asns/64511': 404, '/radar/bgp/routes/stats': { result: { stats: {} } } });
|
||||
const { failedSegments } = await loadAsnSummary('64511', counts);
|
||||
stubRadar({ '/radar/entities/asns/64511': 404 });
|
||||
const { failedSegments } = await loadAsnSummary('64511');
|
||||
assert.equal(failedSegments.includes('asnInfo'), false);
|
||||
});
|
||||
|
||||
it('throws when every segment failed', async () => {
|
||||
stubRadar(new Proxy({}, { get: () => 502 }));
|
||||
await assert.rejects(loadAsnSummary('13335', counts), /Radar responded 502/);
|
||||
await assert.rejects(loadAsnSummary('13335'), /Radar responded 502/);
|
||||
});
|
||||
|
||||
it('the asn view serves a partial answer unchanged but marks it uncacheable', async () => {
|
||||
|
||||
Reference in new issue
Block a user