fix(ui): сохранить неизвестные данные и ошибки BGP на дашборде
This commit is contained in:
1 parent
c343ce62c8
commit
f0eaa94e02
11 files changed
+78
-24
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import { test, expect, type Page } from "@playwright/test"
|
||||
import { serverReadSchema } from "../../packages/contracts/src/servers"
|
||||
|
||||
async function setup(page: Page, initiallyFailed = false, serverCount = 1) {
|
||||
const control = { failed: initiallyFailed, partial: false, prefixes: 321, hold: false, holdPath: "/api/bgp/sessions", fallback: false, failFallback: false, pending: 0, release: () => {} }
|
||||
import {bgpSession} from "./fixtures/bgp"
|
||||
|
||||
async function setup(page: Page, initiallyFailed = false, serverCount = 1, initialBgpPayload?:unknown) {
|
||||
const control = { failed: initiallyFailed, partial: false, prefixes: 321 as number|null, bgpPayload: initialBgpPayload as unknown, hold: false, holdPath: "/api/bgp/catalog", fallback: false, failFallback: false, pending: 0, release: () => {} }
|
||||
let release!: () => void
|
||||
const gate = new Promise<void>(resolve => { release = resolve })
|
||||
control.release = release
|
||||
@@ -16,7 +18,7 @@ async function setup(page: Page, initiallyFailed = false, serverCount = 1) {
|
||||
const path = new URL(route.request().url()).pathname
|
||||
if (path === "/api/auth/config") return route.fulfill({ json: { required: false } })
|
||||
if (path === "/api/evobgp/settings") return route.fulfill({ json: { enabled: false, baseUrl: "", secretConfigured: false } })
|
||||
const fail = control.failed || (control.failFallback && path === "/api/uptime/speed-probes") || (control.partial && ["/api/bgp/sessions", "/api/filters/gre-tunnels", "/api/events", "/api/internet-path/latest", "/api/traffic/servers"].includes(path))
|
||||
const fail = control.failed || (control.failFallback && path === "/api/uptime/speed-probes") || (control.partial && ["/api/bgp/catalog", "/api/filters/gre-tunnels", "/api/events", "/api/internet-path/latest", "/api/traffic/servers"].includes(path))
|
||||
const prefixes = control.prefixes
|
||||
if (control.hold && path === control.holdPath) {
|
||||
control.hold = false
|
||||
@@ -26,7 +28,7 @@ async function setup(page: Page, initiallyFailed = false, serverCount = 1) {
|
||||
if (fail) return route.fulfill({ status: 503, json: { error: "Источник временно недоступен" } })
|
||||
const json = path === "/api/servers" ? servers
|
||||
: path === "/api/uptime/overview" ? { probes: [] }
|
||||
: path === "/api/bgp/sessions" ? [{ state: "Established", prefixesRx: prefixes }]
|
||||
: path === "/api/bgp/catalog" ? control.bgpPayload !== undefined ? control.bgpPayload : {sessions:[bgpSession(1,prefixes)],failures:[]}
|
||||
: path === "/api/filters/rules" ? { rulesets: [] }
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: [{ id: "g1", name: "GRE сохранён", status: "down", serverId: "1" }] }
|
||||
: path === "/api/wireguard" ? { interfaces: [], peers: [] }
|
||||
@@ -126,7 +128,7 @@ test("Дашборд: поздний ответ не заменяет свежи
|
||||
control.prefixes = 999
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("mm:uptime-probes-changed")))
|
||||
await expect(bgp(page)).toContainText("999")
|
||||
const oldResponse = page.waitForResponse(response => response.url().includes("/api/bgp/sessions"))
|
||||
const oldResponse = page.waitForResponse(response => response.url().includes("/api/bgp/catalog"))
|
||||
control.release()
|
||||
await (await oldResponse).finished()
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
||||
@@ -203,3 +205,14 @@ test('Сквозная приёмка: проблема → опрос серв
|
||||
await expect(poll).toBeEnabled()
|
||||
expect(writes).toEqual(['/api/servers/2/poll', '/api/servers/2/poll'])
|
||||
})
|
||||
|
||||
for(const broken of ["missing","partial","duplicate","counter","conflict"])test("Dashboard strict BGP unknown and last good/"+broken,async({page})=>{
|
||||
const sessions=[bgpSession()],failure={serverId:2,serverName:"Узел без ответа",error:"timeout"};const payload=broken==="missing"?{sessions:[]}:broken==="partial"?{sessions,failures:[failure]}:broken==="duplicate"?{sessions:[...sessions,...sessions],failures:[]}:broken==="counter"?{sessions:[bgpSession(1,-1)],failures:[]}:{sessions,failures:[{...failure,serverId:1}]};
|
||||
const control=await setup(page,false,1,payload);const source=page.locator('[data-source="bgp"]');await expect(refresh(page)).toBeEnabled();await expect(source).toContainText("ошибка");await expect(bgp(page)).toContainText("—");
|
||||
control.bgpPayload=undefined;await refresh(page).click();await expect(bgp(page)).toContainText("321");const receivedAt=await source.locator("time").getAttribute("datetime");control.bgpPayload=payload;await refresh(page).click();await expect(source).toContainText("ошибка обновления");await expect(source.locator("time")).toHaveAttribute("datetime",receivedAt!);await expect(bgp(page)).toContainText("321");await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка");
|
||||
control.bgpPayload=undefined;control.prefixes=999;await refresh(page).click();await expect(bgp(page)).toContainText("999");await expect(source).not.toContainText("ошибка");
|
||||
})
|
||||
for(const unknown of ["prefix","state"])test("Dashboard BGP accepts observed unknown without inventing zero/"+unknown,async({page})=>{
|
||||
const control=await setup(page);await expect(bgp(page)).toContainText("321");await expect(refresh(page)).toBeEnabled();control.bgpPayload={sessions:[unknown==="prefix"?bgpSession(1,null):{...bgpSession(),state:"Unknown"}],failures:[]};await refresh(page).click();await expect(bgp(page)).toContainText("—");await expect(bgp(page)).toContainText("префиксы неизвестны");await expect(bgp(page)).toContainText(unknown==="prefix"?"Сессий установлено: 1":"Сессий установлено: неизвестно");await expect(page.locator('[data-source="bgp"]')).not.toContainText("ошибка");
|
||||
control.bgpPayload={sessions:[],failures:[]};await refresh(page).click();await expect(bgp(page)).toContainText("Сессий установлено: 0");await expect(bgp(page)).not.toContainText("—");
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import {test,expect} from "@playwright/test"
|
||||
import {bgpSession} from "./fixtures/bgp"
|
||||
for(const bgp of [false,true])test("Dashboard limited JWT requests only authorized data sources/"+bgp,async({page})=>{
|
||||
const permissions=["mm:dashboard:read",...(bgp?["mm:bgp:read"]:[])],token="header."+Buffer.from(JSON.stringify({sub:"fixture",apps:["mm"],permissions,exp:Math.floor(Date.now()/1000)+3600})).toString("base64url")+".fixture"
|
||||
await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);const calls:string[]=[]
|
||||
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/auth/config")return r.fulfill({json:{required:true}});calls.push(path);return r.fulfill({json:path==="/api/vxlan"?{tunnels:[]}:path==="/api/events"?{events:[]}:path==="/api/bgp/sessions"?[{state:"established",prefixesRx:321}]:{}})});await page.goto("/dashboard");const refresh=page.getByRole("button",{name:"Обновить данные",exact:true});await expect(refresh).toBeEnabled();await expect(page.locator('[data-source="servers"]')).toContainText("Нет доступа");await refresh.click();await expect(refresh).toBeEnabled();for(const path of ["/api/servers","/api/uptime/overview","/api/filters/rules","/api/filters/gre-tunnels","/api/internet-path/latest","/api/wireguard","/api/traffic/servers"])expect(calls).not.toContain(path);expect(calls.includes("/api/bgp/sessions")).toBe(bgp);const tile=page.getByRole("link").filter({hasText:"Префиксы BGP"});await expect(tile).toContainText(bgp?"321":"—");expect(calls).toContain("/api/vxlan")
|
||||
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/auth/config")return r.fulfill({json:{required:true}});calls.push(path);return r.fulfill({json:path==="/api/vxlan"?{tunnels:[]}:path==="/api/events"?{events:[]}:path==="/api/bgp/catalog"?{sessions:[bgpSession()],failures:[]}:{}})});await page.goto("/dashboard");const refresh=page.getByRole("button",{name:"Обновить данные",exact:true});await expect(refresh).toBeEnabled();await expect(page.locator('[data-source="servers"]')).toContainText("Нет доступа");await refresh.click();await expect(refresh).toBeEnabled();for(const path of ["/api/servers","/api/uptime/overview","/api/filters/rules","/api/filters/gre-tunnels","/api/internet-path/latest","/api/wireguard","/api/traffic/servers"])expect(calls).not.toContain(path);expect(calls.includes("/api/bgp/catalog")).toBe(bgp);const tile=page.getByRole("link").filter({hasText:"Префиксы BGP"});await expect(tile).toContainText(bgp?"321":"—");expect(calls).toContain("/api/vxlan")
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
import type {BackendBgpSession} from "@/lib/bgp/response"
|
||||
export function bgpSession(serverId=1,prefixesRx:number|null=321):BackendBgpSession{return {id:"*1",serverId,serverName:"Роутер "+serverId,serverSite:"TEST",serverCountry:"RU",name:"Сессия "+serverId,peerIp:"192.0.2."+serverId,remoteAs:64513,localAs:64512,localId:"192.0.2.100",remoteId:"192.0.2."+serverId,state:"Established",type:"eBGP",uptime:"1h",holdTime:90,keepalive:30,prefixesRx,prefixesTx:2,inputMessages:20,outputMessages:10,capabilities:[],lastError:null}}
|
||||
Reference in new issue
Block a user