Files
MyIP/tests/asn-connectivity.test.js
jason5ng32andClaude Fable 5.1 0e6e411beb Feat(asn): backend data for the ASN Profile page
- cfradar `view=asn` keeps the RPKI route counts from routes/stats.
- New cfradar `view=bgp-prefixes`: the AS's announced prefixes (pfx2as)
  plus IPv4 country shares resolved against the local MaxMind database.
- New /api/asrank: CAIDA AS Rank lookup (rank, customer cone, degree).
  Misses answer 200 with null fields and stay out of the edge cache via
  the new `cacheIf` option on `cacheable`.
- /api/asn-connectivity adds `neighbours`: provider / peer / customer
  counts and name lists from the local AS-relationship snapshot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 19:40:52 +08:00

205 lines
8.1 KiB
JavaScript

// Unit tests for the pure topology walk and neighbour lists in
// api/asn-connectivity.js. Both take an injected adjacency API, so fixtures
// need no snapshot on disk.
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { buildTopology, buildNeighbours } from '../api/asn-connectivity.js';
// Build a rel API from plain objects; ASNs 101+ are the Tier 1 clique.
const makeRel = ({ providers = {}, peers = {}, customers = {}, tier1s = [], customerCounts = {} }) => {
const t1 = new Set(tier1s);
return {
providersOf: (asn) => providers[asn] || [],
peersOf: (asn) => peers[asn] || [],
customersOf: (asn) => customers[asn] || [],
isTier1: (asn) => t1.has(asn),
customerCountOf: (asn) => customerCounts[asn] || 0,
};
};
const edgeKey = (e) => `${e.from}->${e.to}:${e.kind}`;
const sortedEdges = (graph) => graph.edges.map(edgeKey).sort();
const nodeType = (graph, asn) => graph.nodes.find(n => n.asn === asn)?.type;
describe('buildTopology', () => {
it('walks transit chains up to Tier 1s and types nodes', () => {
// 10 buys from 20; 20 buys from 101 (T1) and 30; 30 buys from 101.
const rel = makeRel({
providers: { 10: [20], 20: [101, 30], 30: [101] },
tier1s: [101],
});
const graph = buildTopology(10, rel);
assert.deepEqual(sortedEdges(graph), [
'10->20:transit',
'20->101:transit',
'20->30:transit',
'30->101:transit',
]);
assert.equal(nodeType(graph, 10), 'origin');
assert.equal(nodeType(graph, 20), 'intermediate');
assert.equal(nodeType(graph, 101), 'tier1');
});
it('records Tier-1 peers as terminal peering edges', () => {
// 10's provider 20 reaches the backbone by peering, not transit.
const rel = makeRel({
providers: { 10: [20] },
peers: { 20: [101, 102, 40] }, // 40 is a non-T1 peer — ignored
tier1s: [101, 102],
});
const graph = buildTopology(10, rel);
assert.deepEqual(sortedEdges(graph), [
'10->20:transit',
'20->101:peering',
'20->102:peering',
]);
assert.equal(graph.nodes.some(n => n.asn === 40), false);
});
it('drops non-Tier-1 providers of a node peering with the clique', () => {
// 5 T1 peers hit the peering bar → misinferred "provider" 60 vanishes.
const rel = makeRel({
providers: { 99: [101, 60], 60: [103] },
peers: { 99: [102, 103, 104, 105, 106] },
tier1s: [101, 102, 103, 104, 105, 106],
});
const graph = buildTopology(99, rel);
assert.deepEqual(sortedEdges(graph), [
'99->101:transit',
'99->102:peering',
'99->103:peering',
'99->104:peering',
'99->105:peering',
'99->106:peering',
]);
assert.equal(graph.nodes.some(n => n.asn === 60), false);
});
it('keeps a multihomed hoster below both trusted bars expanding', () => {
// 5 T1 transits + 2 T1 peers is under both bars.
const rel = makeRel({
providers: { 10: [101, 102, 103, 104, 105, 20], 20: [106] },
peers: { 10: [105, 106] },
tier1s: [101, 102, 103, 104, 105, 106],
});
const graph = buildTopology(10, rel);
assert.ok(graph.edges.some(e => edgeKey(e) === '10->20:transit'));
assert.ok(graph.edges.some(e => edgeKey(e) === '20->106:transit'));
});
it('drops non-Tier-1 providers on high combined Tier-1 adjacency', () => {
// 8 T1 transits, no T1 peers: only the combined bar catches it.
const rel = makeRel({
providers: { 99: [101, 102, 103, 104, 105, 106, 107, 108, 60], 60: [101] },
tier1s: [101, 102, 103, 104, 105, 106, 107, 108],
});
const graph = buildTopology(99, rel);
assert.equal(graph.nodes.some(n => n.asn === 60), false);
assert.equal(graph.edges.length, 8);
assert.ok(graph.edges.every(e => e.kind === 'transit'));
});
it('gives a Tier-1 origin its peering edges into the clique', () => {
const rel = makeRel({
peers: { 101: [102, 103] },
tier1s: [101, 102, 103],
});
const graph = buildTopology(101, rel);
assert.equal(nodeType(graph, 101), 'origin-tier1');
assert.deepEqual(sortedEdges(graph), [
'101->102:peering',
'101->103:peering',
]);
});
it('caps non-Tier-1 providers at the top 3 by customer count', () => {
const rel = makeRel({
providers: { 10: [21, 22, 23, 24, 25] },
customerCounts: { 21: 500, 22: 400, 23: 300, 24: 200, 25: 100 },
// Providers reach a T1 so pruning doesn't hide the cap result.
peers: { 21: [101], 22: [101], 23: [101], 24: [101], 25: [101] },
tier1s: [101],
});
const graph = buildTopology(10, rel);
const kept = graph.edges.filter(e => e.from === 10).map(e => e.to).sort();
assert.deepEqual(kept, [21, 22, 23]);
});
it('prunes intermediate chains that never reach a Tier 1', () => {
// 10 → 20 → 30, nobody touches the clique → single-node graph.
const rel = makeRel({
providers: { 10: [20], 20: [30] },
tier1s: [101],
});
const graph = buildTopology(10, rel);
assert.deepEqual(graph.nodes.map(n => n.asn), [10]);
assert.deepEqual(graph.edges, []);
});
it('keeps one edge per pair, transit winning over peering', () => {
const rel = makeRel({
providers: { 10: [101] },
peers: { 10: [101] },
tier1s: [101],
});
const graph = buildTopology(10, rel);
assert.deepEqual(sortedEdges(graph), ['10->101:transit']);
});
});
describe('buildNeighbours', () => {
const names = { 20: 'Upstream One', 21: 'Upstream Two', 40: 'Peer Net' };
const lookupName = (asn) => names[asn] || null;
it('lists providers, peers and customers with names and counts', () => {
const rel = makeRel({
providers: { 10: [20, 21] },
peers: { 10: [40] },
customers: { 10: [50] },
});
assert.deepEqual(buildNeighbours(10, rel, lookupName), {
counts: { providers: 2, peers: 1, customers: 1 },
providers: [{ asn: 20, name: 'Upstream One' }, { asn: 21, name: 'Upstream Two' }],
peers: [{ asn: 40, name: 'Peer Net' }],
customers: [{ asn: 50, name: null }],
});
});
it('ranks by customer count, ASN breaking ties', () => {
const rel = makeRel({
providers: { 10: [23, 21, 22, 24] },
customerCounts: { 21: 5, 22: 900, 23: 5 },
});
assert.deepEqual(buildNeighbours(10, rel, lookupName).providers.map((n) => n.asn), [22, 21, 23, 24]);
});
it('caps each list at 50 while counts stay complete', () => {
const many = Array.from({ length: 120 }, (_, i) => 1000 + i);
const rel = makeRel({ customers: { 10: many }, peers: { 10: many.map((asn) => asn + 1000) } });
const result = buildNeighbours(10, rel, lookupName);
assert.equal(result.counts.customers, 120);
assert.equal(result.customers.length, 50);
assert.equal(result.counts.peers, 120);
assert.equal(result.peers.length, 50);
});
it('counts a pair seen as both transit and peering as transit only', () => {
const rel = makeRel({
providers: { 10: [20] },
customers: { 10: [50] },
peers: { 10: [20, 50, 40, 40] },
});
const result = buildNeighbours(10, rel, lookupName);
assert.deepEqual(result.counts, { providers: 1, peers: 1, customers: 1 });
assert.deepEqual(result.peers, [{ asn: 40, name: 'Peer Net' }]);
});
it('returns empty lists for an ASN unknown to the snapshot', () => {
assert.deepEqual(buildNeighbours(10, makeRel({}), lookupName), {
counts: { providers: 0, peers: 0, customers: 0 },
providers: [], peers: [], customers: [],
});
});
});