From 5d181e7cb0fcf01f5169a9b72e10bce72df38951 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 11 Nov 2026 23:46:51 +0700 Subject: [PATCH] =?UTF-8?q?fix(netflow):=20=D0=BF=D1=80=D0=BE=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D1=83=20=D1=84=D0=BB=D0=B0=D0=B3=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B8=D0=B7=D0=B2=D0=B5=D1=81=D1=82=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/flag.tsx | 16 +++++++++++----- tests/e2e/traffic-flow.spec.ts | 19 +++++++++++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/components/flag.tsx b/components/flag.tsx index cf68b998..33348004 100644 --- a/components/flag.tsx +++ b/components/flag.tsx @@ -59,10 +59,17 @@ function nearestCdnSize(px: number): number { return CDN_SIZES.find(s => s >= px) ?? CDN_SIZES[CDN_SIZES.length - 1] } +function flagCode(code: string): string | null { + if (!code) return null + const lower = code.toLowerCase() + // The server catalogue uses UN when the country is unknown. + return lower !== "un" && /^[a-z]{2}$/.test(lower) ? lower : null +} + /** CDN URL for SVG `` (flagcdn widths only). */ export function flagCdnUrl(code: string, size = 40): string | null { - const lower = code.toLowerCase() - if (!/^[a-z]{2}$/.test(lower)) return null + const lower = flagCode(code) + if (!lower) return null return `https://flagcdn.com/w${nearestCdnSize(size)}/${lower}.png` } @@ -71,9 +78,8 @@ export function flagCdnUrl(code: string, size = 40): string | null { * Source: https://flagcdn.com — free CDN, no API key needed. */ export function Flag({ code, size = 20, className }: FlagProps) { - if (!code) return null - const lower = code.toLowerCase() - if (!/^[a-z]{2}$/.test(lower)) return null + const lower = flagCode(code) + if (!lower) return null const name = countryName(code.toUpperCase()) const cdnSrc = nearestCdnSize(size) const cdnSrc2x = nearestCdnSize(size * 2) diff --git a/tests/e2e/traffic-flow.spec.ts b/tests/e2e/traffic-flow.spec.ts index d55ab219..56535f2b 100644 --- a/tests/e2e/traffic-flow.spec.ts +++ b/tests/e2e/traffic-flow.spec.ts @@ -1,7 +1,7 @@ import { installOpenLiveStream, resumeLiveStream, sendLiveHeartbeat } from "./live-stream-fixture" import { test, expect, type Page } from '@playwright/test' -async function setup(page: Page, liveRefusals = 0) { +async function setup(page: Page, liveRefusals = 0, unknownServer = false) { const control = { emptyExporters: false, failCatalog: false, hold: '', holdIface: '', fail: false, failList: false, invalidSample: undefined as unknown, mergeSample: false, metrics: {} as Record, release: () => {}, liveCalls: 0 } const gate = new Promise(resolve => { control.release = resolve }) const card = { id: '1', name: 'Экспортёр тест', subtitle: '', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3], bytes: 200 } @@ -27,13 +27,18 @@ async function setup(page: Page, liveRefusals = 0) { : path === '/api/traffic/flow/exporters' ? { exporters: control.emptyExporters ? [] : [card, { ...card, id: '2', name: 'Экспортёр второй' }] } : path === '/api/traffic/flow/clients' ? { clients: [{ ...card, id: 'client1', name: 'Клиент тест' }] } : path === '/api/traffic/flows' ? { exportersOnline: 1, bytesPerMin: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', talkers: [], listenerBound: true } - : path === '/api/traffic/servers' ? { servers: [] } + : path === '/api/traffic/servers' ? { servers: unknownServer ? [{ ...card, name: 'Маршрутизатор без страны', country: '', rxPeak: null, txPeak: null, rxTotal: null, txTotal: null }] : [] } : path.endsWith('/interfaces') || path.endsWith('/bound-interfaces') ? { interfaces: [] } : path === '/api/traffic/users' ? { users: [] } : path === '/api/servers' ? [] : {} return route.fulfill({ json }) }) await page.goto('/traffic') + if (unknownServer) { + const server = page.getByRole('button').filter({ hasText: 'Маршрутизатор без страны' }) + await expect(server).toBeVisible() + await expect(server.locator('img[src$="/un.png"]')).toHaveCount(0) + } await page.getByRole('button', { name: 'Потоки', exact: true }).click() await expect(page.getByLabel('Скорость потоков')).toBeVisible() return control @@ -51,6 +56,16 @@ test('Flow: начальный HTTP 429 сменяется фактически expect(control.liveCalls).toBe(2) }) +test('Flow: переход с сервера без страны в потоки не загружает флаг UN', async ({ page }) => { + let placeholderRequests = 0 + await page.route('https://flagcdn.com/**', async route => { + if (new URL(route.request().url()).pathname.endsWith('/un.png')) placeholderRequests++ + await route.abort() + }) + await setup(page, 0, true) + expect(placeholderRequests).toBe(0) +}) + test('Flow: поздний ответ периода не заменяет текущую аналитику', async ({ page }) => { const control = await setup(page) control.hold = '24h'