mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(military): narrow ICAO hex ranges to stop civilian false positives (#627)
* fix(military): narrow hex ranges and callsign regex to stop civilian false positives (#462) MILITARY_HEX_RANGES used entire country ICAO allocations instead of military-specific sub-ranges (sourced from tar1090-db/ranges.json). This flagged ALL commercial aircraft from Italy, Spain, Japan, India, South Korea, etc. as military activity. Key changes: - Remove A00000-A3FFFF (US civilian N-numbers) — military starts at ADF7C8 - Italy 300000-33FFFF → 33FF00-33FFFF (top 256 codes only) - Spain 340000-37FFFF → 350000-37FFFF (upper 3/4 confirmed military) - Japan 840000-87FFFF removed (no confirmed JASDF sub-range) - France narrowed to 3AA000-3AFFFF + 3B7000-3BFFFF - Germany narrowed to 3EA000-3EBFFF + 3F4000-3FBFFF - India 800000-83FFFF → 800200-8002FF (256 codes) - Canada C00000-C0FFFF → C20000-C3FFFF (upper half) - Remove unconfirmed: South Korea, Sweden, Singapore, Pakistan - Add confirmed: Austria, Belgium, Switzerland, Brazil - Drop overly broad /^[A-Z]{4,}\d{1,3}$/ callsign regex from server * fix(military): constrain short prefixes + add classification tests Move ambiguous 2-letter prefixes (AE, RF, TF, PAT, SAM, OPS, CTF, IRG, TAF) to SHORT_MILITARY_PREFIXES — these now only match when followed by a digit (e.g. AE1234=military, AEE123=Aegean Airlines). Add 97-case test suite covering: - Military callsign detection (19 known patterns) - Short prefix digit gating (6 cases) - Civilian airline non-detection (26 airlines) - Short prefix letter rejection (6 cases) - Military hex range boundaries (7 confirmed ranges) - Civilian hex non-detection (19 codes) - Boundary precision (ADF7C8 start, 33FF00 start, etc.) - No-full-allocation guard (10 countries) * fix: use charAt() instead of bracket indexing for strict TS
This commit is contained in:
@@ -28,17 +28,20 @@ export const MILITARY_PREFIXES = [
|
||||
'COBRA', 'PYTHON', 'RAPTOR', 'EAGLE', 'HAWK', 'TALON',
|
||||
'BOXER', 'OMNI', 'TOPCAT', 'SKULL', 'REAPER', 'HUNTER',
|
||||
'ARMY', 'NAVY', 'USAF', 'USMC', 'USCG',
|
||||
'AE', 'CNV', 'PAT', 'SAM', 'EXEC',
|
||||
'OPS', 'CTF', 'TF',
|
||||
'CNV', 'EXEC',
|
||||
'NATO', 'GAF', 'RRF', 'RAF', 'FAF', 'IAF', 'RNLAF', 'BAF', 'DAF', 'HAF', 'PAF',
|
||||
'SWORD', 'LANCE', 'ARROW', 'SPARTAN',
|
||||
'RSAF', 'EMIRI', 'UAEAF', 'KAF', 'QAF', 'BAHAF', 'OMAAF',
|
||||
'IRIAF', 'IRG', 'IRGC',
|
||||
'TAF', 'TUAF',
|
||||
'RSD', 'RF', 'RFF', 'VKS',
|
||||
'IRIAF', 'IRGC',
|
||||
'TUAF',
|
||||
'RSD', 'RFF', 'VKS',
|
||||
'CHN', 'PLAAF', 'PLA',
|
||||
];
|
||||
|
||||
// Short prefixes that only match when followed by digits (not letters)
|
||||
// e.g. AE1234 = military, AEE123 = Aegean Airlines
|
||||
const SHORT_MILITARY_PREFIXES = ['AE', 'RF', 'TF', 'PAT', 'SAM', 'OPS', 'CTF', 'IRG', 'TAF'];
|
||||
|
||||
export const AIRLINE_CODES = new Set([
|
||||
'SVA', 'QTR', 'THY', 'UAE', 'ETD', 'GFA', 'MEA', 'RJA', 'KAC', 'ELY',
|
||||
'IAW', 'IRA', 'MSR', 'SYR', 'PGT', 'AXB', 'FDB', 'KNE', 'FAD', 'ADY', 'OMA',
|
||||
@@ -58,7 +61,9 @@ export function isMilitaryCallsign(callsign: string | null | undefined): boolean
|
||||
for (const prefix of MILITARY_PREFIXES) {
|
||||
if (cs.startsWith(prefix)) return true;
|
||||
}
|
||||
if (/^[A-Z]{4,}\d{1,3}$/.test(cs)) return true;
|
||||
for (const prefix of SHORT_MILITARY_PREFIXES) {
|
||||
if (cs.startsWith(prefix) && cs.length > prefix.length && /\d/.test(cs.charAt(prefix.length))) return true;
|
||||
}
|
||||
if (/^[A-Z]{3}\d{1,2}$/.test(cs)) {
|
||||
const prefix = cs.slice(0, 3);
|
||||
if (!AIRLINE_CODES.has(prefix)) return true;
|
||||
|
||||
@@ -281,41 +281,42 @@ export const MILITARY_AIRCRAFT_TYPES: Record<string, { type: MilitaryAircraftTyp
|
||||
* Reference: https://www.ads-b.nl/icao.php
|
||||
*/
|
||||
export const MILITARY_HEX_RANGES: { start: string; end: string; operator: MilitaryOperator; country: string }[] = [
|
||||
// United States Military (largest block)
|
||||
{ start: 'ADF7C7', end: 'ADF7CF', operator: 'usaf', country: 'USA' }, // Known USAF tankers
|
||||
{ start: 'AE0000', end: 'AFFFFF', operator: 'usaf', country: 'USA' }, // Main USAF block
|
||||
{ start: 'A00000', end: 'A3FFFF', operator: 'usaf', country: 'USA' }, // Additional US military
|
||||
// United States DoD — civil N-numbers end at ADF7C7; everything above is military
|
||||
{ start: 'ADF7C8', end: 'AFFFFF', operator: 'usaf', country: 'USA' },
|
||||
|
||||
// UK Military
|
||||
// UK Military (small block at start + main RAF block)
|
||||
{ start: '400000', end: '40003F', operator: 'raf', country: 'UK' },
|
||||
{ start: '43C000', end: '43CFFF', operator: 'raf', country: 'UK' },
|
||||
|
||||
// France Military
|
||||
{ start: '3A0000', end: '3AFFFF', operator: 'faf', country: 'France' },
|
||||
{ start: '3B0000', end: '3BFFFF', operator: 'faf', country: 'France' },
|
||||
// France Military (two sub-blocks within 380000-3BFFFF)
|
||||
{ start: '3AA000', end: '3AFFFF', operator: 'faf', country: 'France' },
|
||||
{ start: '3B7000', end: '3BFFFF', operator: 'faf', country: 'France' },
|
||||
|
||||
// Germany Military
|
||||
{ start: '3F0000', end: '3FFFFF', operator: 'gaf', country: 'Germany' },
|
||||
// Germany Military (two sub-blocks within 3C0000-3FFFFF)
|
||||
{ start: '3EA000', end: '3EBFFF', operator: 'gaf', country: 'Germany' },
|
||||
{ start: '3F4000', end: '3FBFFF', operator: 'gaf', country: 'Germany' },
|
||||
|
||||
// Israel Military (critical for Middle East)
|
||||
{ start: '738000', end: '73FFFF', operator: 'iaf', country: 'Israel' },
|
||||
// Israel Military (confirmed IAF sub-range within 738000-73FFFF)
|
||||
{ start: '738A00', end: '738BFF', operator: 'iaf', country: 'Israel' },
|
||||
|
||||
// NATO AWACS (Luxembourg registration but NATO operated)
|
||||
{ start: '4D0000', end: '4D03FF', operator: 'nato', country: 'NATO' },
|
||||
|
||||
// Italy Military
|
||||
{ start: '300000', end: '33FFFF', operator: 'other', country: 'Italy' },
|
||||
// Italy Military (top of 300000-33FFFF block)
|
||||
{ start: '33FF00', end: '33FFFF', operator: 'other', country: 'Italy' },
|
||||
|
||||
// Spain Military
|
||||
{ start: '340000', end: '37FFFF', operator: 'other', country: 'Spain' },
|
||||
// Spain Military (upper 3/4 of 340000-37FFFF; civilian in 340000-34FFFF)
|
||||
{ start: '350000', end: '37FFFF', operator: 'other', country: 'Spain' },
|
||||
|
||||
// Netherlands Military
|
||||
{ start: '480000', end: '480FFF', operator: 'other', country: 'Netherlands' },
|
||||
|
||||
// Turkey Military (important for Middle East)
|
||||
{ start: '4BA000', end: '4BCFFF', operator: 'other', country: 'Turkey' },
|
||||
// Turkey Military (confirmed sub-range within 4B8000-4BFFFF)
|
||||
{ start: '4B8200', end: '4B82FF', operator: 'other', country: 'Turkey' },
|
||||
|
||||
// Saudi Arabia Military
|
||||
{ start: '710000', end: '717FFF', operator: 'other', country: 'Saudi Arabia' },
|
||||
// Saudi Arabia Military (two small confirmed sub-blocks)
|
||||
{ start: '710258', end: '71028F', operator: 'other', country: 'Saudi Arabia' },
|
||||
{ start: '710380', end: '71039F', operator: 'other', country: 'Saudi Arabia' },
|
||||
|
||||
// UAE Military
|
||||
{ start: '896000', end: '896FFF', operator: 'other', country: 'UAE' },
|
||||
@@ -326,41 +327,38 @@ export const MILITARY_HEX_RANGES: { start: string; end: string; operator: Milita
|
||||
// Kuwait Military
|
||||
{ start: '706000', end: '706FFF', operator: 'other', country: 'Kuwait' },
|
||||
|
||||
// Japan Self-Defense Forces
|
||||
{ start: '840000', end: '87FFFF', operator: 'other', country: 'Japan' },
|
||||
// Australia Military (confirmed RAAF sub-range)
|
||||
{ start: '7CF800', end: '7CFAFF', operator: 'other', country: 'Australia' },
|
||||
|
||||
// South Korea Military
|
||||
{ start: '718000', end: '71FFFF', operator: 'other', country: 'South Korea' },
|
||||
// Canada Military (upper half of C00000-C3FFFF)
|
||||
{ start: 'C20000', end: 'C3FFFF', operator: 'other', country: 'Canada' },
|
||||
|
||||
// Australia Military
|
||||
{ start: '7CF800', end: '7CFFFF', operator: 'other', country: 'Australia' },
|
||||
// India Military (confirmed IAF sub-range within 800000-83FFFF)
|
||||
{ start: '800200', end: '8002FF', operator: 'other', country: 'India' },
|
||||
|
||||
// Canada Military
|
||||
{ start: 'C00000', end: 'C0FFFF', operator: 'other', country: 'Canada' },
|
||||
// Egypt Military (confirmed sub-range)
|
||||
{ start: '010070', end: '01008F', operator: 'other', country: 'Egypt' },
|
||||
|
||||
// India Military
|
||||
{ start: '800000', end: '83FFFF', operator: 'other', country: 'India' },
|
||||
// Poland Military (confirmed sub-range within 488000-48FFFF)
|
||||
{ start: '48D800', end: '48D87F', operator: 'other', country: 'Poland' },
|
||||
|
||||
// Pakistan Military
|
||||
{ start: '760000', end: '767FFF', operator: 'other', country: 'Pakistan' },
|
||||
// Greece Military (confirmed sub-range at start of 468000-46FFFF)
|
||||
{ start: '468000', end: '4683FF', operator: 'other', country: 'Greece' },
|
||||
|
||||
// Egypt Military
|
||||
{ start: '500000', end: '5003FF', operator: 'other', country: 'Egypt' },
|
||||
// Norway Military (confirmed sub-range within 478000-47FFFF)
|
||||
{ start: '478100', end: '4781FF', operator: 'other', country: 'Norway' },
|
||||
|
||||
// Poland Military
|
||||
{ start: '488000', end: '48FFFF', operator: 'other', country: 'Poland' },
|
||||
// Austria Military
|
||||
{ start: '444000', end: '446FFF', operator: 'other', country: 'Austria' },
|
||||
|
||||
// Greece Military
|
||||
{ start: '468000', end: '46FFFF', operator: 'other', country: 'Greece' },
|
||||
// Belgium Military
|
||||
{ start: '44F000', end: '44FFFF', operator: 'other', country: 'Belgium' },
|
||||
|
||||
// Sweden Military
|
||||
{ start: '4A8000', end: '4AFFFF', operator: 'other', country: 'Sweden' },
|
||||
// Switzerland Military
|
||||
{ start: '4B7000', end: '4B7FFF', operator: 'other', country: 'Switzerland' },
|
||||
|
||||
// Norway Military
|
||||
{ start: '478000', end: '47FFFF', operator: 'other', country: 'Norway' },
|
||||
|
||||
// Singapore Military
|
||||
{ start: '768000', end: '76FFFF', operator: 'other', country: 'Singapore' },
|
||||
// Brazil Military
|
||||
{ start: 'E40000', end: 'E41FFF', operator: 'other', country: 'Brazil' },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
249
tests/military-classification.test.mjs
Normal file
249
tests/military-classification.test.mjs
Normal file
@@ -0,0 +1,249 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract server-side classification data from _shared.ts source
|
||||
// ---------------------------------------------------------------------------
|
||||
const sharedSrc = readFileSync(
|
||||
join(root, 'server/worldmonitor/military/v1/_shared.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
function extractArray(src, name) {
|
||||
// Match both `const X = [...]` and `const X = new Set([...])`
|
||||
const re = new RegExp(`(?:export )?const ${name}\\s*=\\s*(?:new Set\\()?\\[([\\s\\S]*?)\\]`);
|
||||
const m = src.match(re);
|
||||
if (!m) return [];
|
||||
return [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]);
|
||||
}
|
||||
|
||||
const MILITARY_PREFIXES = extractArray(sharedSrc, 'MILITARY_PREFIXES');
|
||||
const SHORT_MILITARY_PREFIXES = extractArray(sharedSrc, 'SHORT_MILITARY_PREFIXES');
|
||||
const AIRLINE_CODES = new Set(extractArray(sharedSrc, 'AIRLINE_CODES'));
|
||||
|
||||
function isMilitaryCallsign(callsign) {
|
||||
if (!callsign) return false;
|
||||
const cs = callsign.toUpperCase().trim();
|
||||
for (const prefix of MILITARY_PREFIXES) {
|
||||
if (cs.startsWith(prefix)) return true;
|
||||
}
|
||||
for (const prefix of SHORT_MILITARY_PREFIXES) {
|
||||
if (cs.startsWith(prefix) && cs.length > prefix.length && /\d/.test(cs[prefix.length]))
|
||||
return true;
|
||||
}
|
||||
if (/^[A-Z]{3}\d{1,2}$/.test(cs)) {
|
||||
const prefix = cs.slice(0, 3);
|
||||
if (!AIRLINE_CODES.has(prefix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract client-side hex ranges from military.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
const clientSrc = readFileSync(join(root, 'src/config/military.ts'), 'utf-8');
|
||||
|
||||
function extractHexRanges(src) {
|
||||
const ranges = [];
|
||||
const re = /start:\s*'([0-9A-Fa-f]+)',\s*end:\s*'([0-9A-Fa-f]+)'/g;
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) {
|
||||
ranges.push({ start: m[1].toUpperCase(), end: m[2].toUpperCase() });
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const HEX_RANGES = extractHexRanges(clientSrc);
|
||||
|
||||
function isKnownMilitaryHex(hexCode) {
|
||||
const hex = hexCode.toUpperCase();
|
||||
for (const range of HEX_RANGES) {
|
||||
if (hex >= range.start && hex <= range.end) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Tests
|
||||
// ===========================================================================
|
||||
|
||||
describe('Military callsign classifier (server-side)', () => {
|
||||
describe('correctly identifies military callsigns', () => {
|
||||
const military = [
|
||||
'RCH1234', 'REACH01', 'MOOSE55', 'NAVY1', 'ARMY22',
|
||||
'COBRA11', 'DUKE01', 'SHELL22', 'RAPTOR1', 'REAPER01',
|
||||
'NATO01', 'GAF123', 'RAF01', 'FAF55', 'IAF01',
|
||||
'RSAF01', 'IRGC1', 'VKS01', 'PLAAF1',
|
||||
];
|
||||
for (const cs of military) {
|
||||
it(`marks ${cs} as military`, () => {
|
||||
assert.ok(isMilitaryCallsign(cs), `${cs} should be military`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('correctly identifies short-prefix military callsigns', () => {
|
||||
const shortMilitary = [
|
||||
'AE1234', 'RF01', 'TF122', 'PAT01', 'SAM1', 'OPS22',
|
||||
];
|
||||
for (const cs of shortMilitary) {
|
||||
it(`marks ${cs} as military (short prefix + digit)`, () => {
|
||||
assert.ok(isMilitaryCallsign(cs), `${cs} should be military`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('does NOT flag commercial airline callsigns', () => {
|
||||
const civilian = [
|
||||
'AEE123', // Aegean Airlines
|
||||
'AEA456', // Air Europa
|
||||
'THY1234', // Turkish Airlines
|
||||
'SVA123', // Saudia
|
||||
'QTR456', // Qatar Airways
|
||||
'UAE789', // Emirates
|
||||
'BAW123', // British Airways
|
||||
'AFR456', // Air France
|
||||
'DLH789', // Lufthansa
|
||||
'KLM12', // KLM
|
||||
'AAL1234', // American Airlines
|
||||
'DAL5678', // Delta
|
||||
'UAL901', // United
|
||||
'SWA1234', // Southwest
|
||||
'JAL123', // Japan Airlines
|
||||
'ANA456', // All Nippon Airways
|
||||
'KAL789', // Korean Air
|
||||
'CCA123', // Air China
|
||||
'AIC456', // Air India
|
||||
'SIA789', // Singapore Airlines
|
||||
'ELY123', // El Al
|
||||
'RYR456', // Ryanair
|
||||
'EZY789', // easyJet
|
||||
'WZZ123', // Wizz Air
|
||||
'FDX456', // FedEx
|
||||
'UPS789', // UPS
|
||||
];
|
||||
for (const cs of civilian) {
|
||||
it(`does NOT mark ${cs} as military`, () => {
|
||||
assert.ok(!isMilitaryCallsign(cs), `${cs} should NOT be military`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('short prefixes do NOT match when followed by letters', () => {
|
||||
const civilianShort = [
|
||||
'AEE123', // Aegean — starts with AE but next char is E (letter)
|
||||
'AERO1', // Generic — starts with AE but not short-prefix match
|
||||
'RFAIR', // hypothetical — RF followed by letter
|
||||
'TFLIGHT', // hypothetical — TF followed by letter
|
||||
'PATROL1', // starts with PAT but next char is R (letter)
|
||||
'SAMPLE', // starts with SAM but next char is P (letter)
|
||||
];
|
||||
for (const cs of civilianShort) {
|
||||
it(`does NOT mark ${cs} as military via short prefix`, () => {
|
||||
assert.ok(!isMilitaryCallsign(cs), `${cs} should NOT be military`);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Military hex range classifier (client-side)', () => {
|
||||
describe('correctly identifies military hex codes', () => {
|
||||
const military = [
|
||||
'AE0000', // US DoD start
|
||||
'AF0000', // US DoD mid
|
||||
'AFFFFF', // US DoD end
|
||||
'43C000', // RAF start
|
||||
'43CFFF', // RAF end
|
||||
'3AA000', // French military start
|
||||
'3F4000', // German military start
|
||||
];
|
||||
for (const hex of military) {
|
||||
it(`marks ${hex} as military`, () => {
|
||||
assert.ok(isKnownMilitaryHex(hex), `${hex} should be military`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('does NOT flag civilian ICAO hex codes', () => {
|
||||
const civilian = [
|
||||
'A00001', // US civilian N-number (N1)
|
||||
'A0B0C0', // US civilian mid-range
|
||||
'A3FFFF', // US civilian — was incorrectly flagged before fix
|
||||
'ADF7C7', // Last US civilian N-number (N99999)
|
||||
'300000', // Italian civilian (Alitalia range start)
|
||||
'330000', // Italian civilian
|
||||
'33FE00', // Italian civilian (just below military)
|
||||
'340000', // Spanish civilian
|
||||
'34FFFF', // Spanish civilian (just below military at 350000)
|
||||
'840000', // Japanese civilian (JAL/ANA) — entire block removed
|
||||
'870000', // Japanese civilian
|
||||
'800000', // Indian civilian (Air India)
|
||||
'800100', // Indian civilian
|
||||
'718000', // South Korean civilian — no confirmed military range
|
||||
'3C0000', // German civilian (Lufthansa range)
|
||||
'380000', // French civilian (Air France range)
|
||||
'C00000', // Canadian civilian (Air Canada)
|
||||
'C10000', // Canadian civilian
|
||||
'7C0000', // Australian civilian (Qantas)
|
||||
];
|
||||
for (const hex of civilian) {
|
||||
it(`does NOT mark ${hex} as military`, () => {
|
||||
assert.ok(!isKnownMilitaryHex(hex), `${hex} should NOT be military`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('validates range boundaries are tight', () => {
|
||||
it('US military starts at ADF7C8, not A00000', () => {
|
||||
assert.ok(!isKnownMilitaryHex('ADF7C7'), 'ADF7C7 (last N-number) should be civilian');
|
||||
assert.ok(isKnownMilitaryHex('ADF7C8'), 'ADF7C8 should be military');
|
||||
});
|
||||
|
||||
it('Italy military is only top 256 codes (33FF00-33FFFF)', () => {
|
||||
assert.ok(!isKnownMilitaryHex('33FEFF'), '33FEFF should be civilian');
|
||||
assert.ok(isKnownMilitaryHex('33FF00'), '33FF00 should be military');
|
||||
});
|
||||
|
||||
it('Spain military starts at 350000 (civilian below)', () => {
|
||||
assert.ok(!isKnownMilitaryHex('34FFFF'), '34FFFF should be civilian');
|
||||
assert.ok(isKnownMilitaryHex('350000'), '350000 should be military');
|
||||
});
|
||||
|
||||
it('Canada military starts at C20000 (civilian below)', () => {
|
||||
assert.ok(!isKnownMilitaryHex('C1FFFF'), 'C1FFFF should be civilian');
|
||||
assert.ok(isKnownMilitaryHex('C20000'), 'C20000 should be military');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no range spans an entire country ICAO allocation', () => {
|
||||
const countryAllocations = [
|
||||
{ country: 'USA', start: 'A00000', end: 'AFFFFF' },
|
||||
{ country: 'Italy', start: '300000', end: '33FFFF' },
|
||||
{ country: 'Spain', start: '340000', end: '37FFFF' },
|
||||
{ country: 'Japan', start: '840000', end: '87FFFF' },
|
||||
{ country: 'India', start: '800000', end: '83FFFF' },
|
||||
{ country: 'France', start: '380000', end: '3BFFFF' },
|
||||
{ country: 'Germany', start: '3C0000', end: '3FFFFF' },
|
||||
{ country: 'UK', start: '400000', end: '43FFFF' },
|
||||
{ country: 'Canada', start: 'C00000', end: 'C3FFFF' },
|
||||
{ country: 'Australia', start: '7C0000', end: '7FFFFF' },
|
||||
];
|
||||
for (const alloc of countryAllocations) {
|
||||
it(`no single range covers all of ${alloc.country} (${alloc.start}-${alloc.end})`, () => {
|
||||
const fullRange = HEX_RANGES.find(
|
||||
(r) => r.start <= alloc.start && r.end >= alloc.end,
|
||||
);
|
||||
assert.ok(
|
||||
!fullRange,
|
||||
`Range ${fullRange?.start}-${fullRange?.end} spans entire ${alloc.country} allocation`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user