fix(ui): защищать оптимизацию OSPF и подтверждение результата

This commit is contained in:
Denozordec committed 2026-09-30 17:46:56 +07:00
1 parent 4254904c3a
commit baa43b7ad4
8 files changed
+193 -42

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
import { test, expect } from "@playwright/test"
const headers = { "access-control-allow-origin": "*", "access-control-allow-methods": "GET,POST,OPTIONS", "access-control-allow-headers": "Content-Type,Authorization" }
const device = { id: 1, name: "Роутер 1", host: "192.0.2.1", enabled: true, country: "RU", status: null }
const iface = { id: "*1", serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU", instance: "default", area: "backbone", areaId: "0.0.0.0", interface: "ether1", cost: 20, type: "broadcast", disabled: false, inactive: false, priority: 1, helloInterval: 10, deadInterval: 40, useBfd: false }
const catalog = { neighbors: [], interfaces: [iface], instances: [], bfdSessions: [], routes: [], failures: [] }
const result = { serverId: 1, serverName: "Роутер 1", pingWeight: 60, optimizedCount: 1, applied: [{ interface: "ether1", from: 20, to: 10 }], interfaces: [{ id: "*1", interface: "ether1", currentCost: 20, optimalCost: 10, score: 1, pingMs: 10, dlMbps: 100, ulMbps: 100 }] }
for (const change of ["backend", "mode"]) for (const operation of ["read", "write"]) for (const fails of [false, true]) {
test('OSPF: поздний ' + operation + ' после смены ' + change + '/' + fails, async ({ page }) => {
let newReads = 0
await page.route("**/health", r => r.fulfill({ headers, json: { ok: true } }))
await page.route("**/api/**", r => {
const url = new URL(r.request().url()), path = url.pathname
if (path === "/api/ospf/catalog" && url.port === "8101") newReads++
return r.fulfill({ headers, json: path === "/api/ospf/catalog" ? { ...catalog, interfaces: [{ ...iface, interface: url.port === "8101" ? "Новый интерфейс" : "Старый интерфейс" }] } : path === "/api/servers" ? [device] : path === "/api/auth/config" ? { required: false } : {} })
})
await page.goto("/settings")
await page.getByRole("link", { name: /^OSPF/ }).click()
await expect(page.getByText("Старый интерфейс", { exact: true })).toBeVisible()
await page.getByRole("option").filter({ hasText: "Роутер 1" }).click()
let release: (() => void) | undefined, started = false
const gate = new Promise<void>(resolve => { release = resolve })
const endpoint = operation === "read" ? "/api/ospf/catalog" : "/api/servers/1/ospf/optimize"
await page.route("**" + endpoint, async r => {
if (new URL(r.request().url()).port !== "8000") return r.fallback()
started = true
await gate
return r.fulfill(fails ? { headers, status: 503, json: { error: "Старая ошибка OSPF" } } : { headers, json: operation === "read" ? { ...catalog, interfaces: [] } : result })
})
await page.getByRole("button", { name: operation === "read" ? "Обновить" : "Оптимизировать OSPF", exact: true }).click()
await expect.poll(() => started).toBe(true)
await page.goBack()
await expect(page).toHaveURL(/settings/)
if (change === "backend") {
const input = page.getByPlaceholder("http://localhost:8000")
await input.fill("http://localhost:8101")
await input.press("Enter")
} else await page.getByRole("button", { name: "Моковые", exact: true }).click()
await page.getByRole("link", { name: /^OSPF/ }).click()
if (change === "backend") await expect(page.getByText("Новый интерфейс", { exact: true })).toBeVisible()
else await expect(page.getByText("Моковые данные", { exact: true })).toBeVisible()
const before = newReads
const response = page.waitForResponse(r => r.url().includes(":8000" + endpoint))
release!()
await (await response).finished()
await page.evaluate(() => new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))))
if (change === "backend") await expect(page.getByText("Новый интерфейс", { exact: true })).toBeVisible()
await expect(page.getByText("Старая ошибка OSPF", { exact: false })).toHaveCount(0)
await expect(page.getByText("API подтвердил оптимизацию OSPF", { exact: false })).toHaveCount(0)
expect(newReads).toBe(before)
})
}
+68
View File
@@ -0,0 +1,68 @@
import { test, expect } from "@playwright/test"
const device = { id: 1, name: "Роутер 1", host: "192.0.2.1", enabled: true, country: "RU", status: null }
const iface = { id: "*1", serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU", instance: "default", area: "backbone", areaId: "0.0.0.0", interface: "ether1", cost: 20, type: "broadcast", disabled: false, inactive: false, priority: 1, helloInterval: 10, deadInterval: 40, useBfd: false }
const catalog = { neighbors: [], interfaces: [iface], instances: [], bfdSessions: [], routes: [], failures: [] }
const result = { serverId: 1, serverName: "Роутер 1", pingWeight: 60, optimizedCount: 1, applied: [{ interface: "ether1", from: 20, to: 10 }], interfaces: [{ id: "*1", interface: "ether1", currentCost: 20, optimalCost: 10, score: 1, pingMs: 10, dlMbps: 100, ulMbps: 100 }] }
for (const outcome of ["success", "http", "foreign", "malformed", "poll-failure"]) {
test("OSPF: оптимизация, общий lock и результат " + outcome, async ({ page }) => {
let writes = 0, polled = false, reads = 0
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", async r => {
const path = new URL(r.request().url()).pathname
if (path === "/api/servers/1/ospf/optimize") {
writes++
expect(r.request().method()).toBe("POST")
expect(r.request().postDataJSON().pingWeight).toBeGreaterThanOrEqual(0)
await gate
polled = true
return r.fulfill(outcome === "http" ? { status: 503, json: { error: "Применение не завершено" } } : { json: outcome === "foreign" ? { ...result, serverId: 2 } : outcome === "malformed" ? { ...result, optimizedCount: 99 } : result })
}
if (path === "/api/ospf/catalog") {
reads++
if (polled && outcome === "poll-failure") return r.fulfill({ status: 503, json: { error: "Опрос после записи недоступен" } })
return r.fulfill({ json: catalog })
}
return r.fulfill({ json: path === "/api/servers" ? [device] : path === "/api/auth/config" ? { required: false } : {} })
})
await page.goto("/ospf")
await page.getByRole("option").filter({ hasText: "Роутер 1" }).click()
const optimize = page.getByRole("button", { name: "Оптимизировать OSPF", exact: true })
await expect(optimize).toBeEnabled()
await expect(page.getByText("opt 10", { exact: true })).toHaveCount(0)
await optimize.evaluate(el => { (el as HTMLButtonElement).click(); (el as HTMLButtonElement).click() })
await expect.poll(() => writes).toBe(1)
await expect(page.getByRole("button", { name: "Оптимизация OSPF…", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeDisabled()
await page.getByRole("button", { name: "Обновить", exact: true }).evaluate(el => (el as HTMLButtonElement).click())
expect(reads).toBe(1)
release!()
await expect.poll(() => reads).toBe(2)
if (["http", "foreign", "malformed"].includes(outcome)) {
await expect(page.getByRole("alert").filter({ hasText: "Оптимизация OSPF не подтверждена" })).toBeVisible()
await expect(page.getByText("API подтвердил оптимизацию OSPF", { exact: false })).toHaveCount(0)
} else await expect(page.getByText("API подтвердил оптимизацию OSPF: 1 интерфейсов", { exact: true })).toBeVisible()
if (outcome === "poll-failure") {
await expect(page.getByRole("alert").filter({ hasText: "Опрос после записи недоступен" })).toBeVisible()
await expect(optimize).toBeDisabled()
} else await expect(optimize).toBeEnabled()
expect(writes).toBe(1)
})
}
test("OSPF: read-права не разрешают оптимизацию устройства", async ({ page }) => {
const payload = { sub: "test", email: "test@example.test", name: "Reader", apps: ["mm"], permissions: ["mm:network:read", "mm:servers:read"], exp: 4102444800 }
await page.addInitScript(value => localStorage.setItem("mmapp_token", "test." + btoa(JSON.stringify(value)) + ".test"), payload)
let writes = 0
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
if (r.request().method() === "POST") writes++
return r.fulfill({ json: path === "/api/auth/config" ? { required: true } : path === "/api/servers" ? [device] : path === "/api/ospf/catalog" ? catalog : {} })
})
await page.goto("/ospf")
await page.getByRole("option").filter({ hasText: "Роутер 1" }).click()
await expect(page.getByRole("button", { name: "Оптимизировать OSPF", exact: true })).toBeDisabled()
expect(writes).toBe(0)
})