fix(ui): изолировать операции восстановления Filters
This commit is contained in:
1 parent
7554f1804b
commit
1fba16e1f2
5 files changed
+145
-15
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
for (const failure of ["http", "wrong-server", "stale"] as const) {
|
||||
test(`Filters: замок восстановления и отказ чтения ${failure}`, async ({ page }) => {
|
||||
const rule = { id: "rule-1", community: "65001:101", gateway: "", gatewayTunnelId: "", action: "blackhole", description: "Правило" }
|
||||
let restores = 0, revisionReads = 0, readsAfterRestore = 0
|
||||
let releaseRead: (() => void) | undefined, releaseHistory: (() => void) | undefined
|
||||
const readGate = new Promise<void>(resolve => { releaseRead = resolve })
|
||||
const historyGate = new Promise<void>(resolve => { releaseHistory = resolve })
|
||||
let historyWaiting = false
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", async route => {
|
||||
const path = new URL(route.request().url()).pathname
|
||||
if (path.endsWith("/restore")) {
|
||||
restores++
|
||||
expect(route.request().postDataJSON()).toEqual({ serverId: "1" })
|
||||
if (restores === 1) return route.fulfill({ status: 403, json: { error: "Откат запрещён тест" } })
|
||||
return route.fulfill({ json: { ok: true } })
|
||||
}
|
||||
if (path === "/api/filters/revisions") {
|
||||
revisionReads++
|
||||
if (restores === 3) { historyWaiting = true; await historyGate }
|
||||
return route.fulfill({ json: { revisions: [{ id: "rev/1", serverId: "1", section: "filters", source: "apply", fingerprint: "fixture", createdAt: "2026-09-29T00:00:00Z", note: null, itemCount: 1 }] } })
|
||||
}
|
||||
if (path === "/api/filters/rules" && restores >= 2) {
|
||||
readsAfterRestore++
|
||||
if (restores === 2) {
|
||||
if (failure === "http") return route.fulfill({ status: 503, json: { error: "Чтение недоступно тест" } })
|
||||
return route.fulfill({ json: { rulesets: [{ serverId: failure === "wrong-server" ? "2" : "1", rules: [] }], greTunnels: [], stale: failure === "stale" } })
|
||||
}
|
||||
await readGate
|
||||
}
|
||||
return route.fulfill({ json: path === "/api/filters/rules" ? { rulesets: [{ serverId: "1", rules: [rule] }], greTunnels: [], live: true }
|
||||
: path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "", status: "online", latency: 1 }]
|
||||
: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: [] }
|
||||
: path === "/api/recursive-routes" ? { routes: [] } : {} })
|
||||
})
|
||||
await page.goto("/filters")
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(1)
|
||||
await page.getByRole("button", { name: "История", exact: true }).click()
|
||||
const sheet = page.getByRole("dialog")
|
||||
await sheet.getByRole("button", { name: "Откатить", exact: true }).click()
|
||||
const confirm = page.getByRole("alertdialog")
|
||||
const restore = confirm.getByRole("button", { name: "Откатить", exact: true })
|
||||
await restore.click()
|
||||
await expect(confirm.getByRole("alert").filter({ hasText: "Откат запрещён тест" })).toBeVisible()
|
||||
await restore.click()
|
||||
await expect(confirm.getByRole("alert").filter({ hasText: "Версия применена, но актуальные правила не прочитаны" })).toBeVisible()
|
||||
await expect(rows).toHaveCount(1)
|
||||
await expect(page.getByText("Версия применена на роутер", { exact: true })).toHaveCount(0)
|
||||
await expect(restore).toBeEnabled()
|
||||
await restore.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
|
||||
await expect.poll(() => restores).toBe(3)
|
||||
await expect.poll(() => readsAfterRestore).toBe(2)
|
||||
await expect(restore).toBeDisabled()
|
||||
await expect(confirm.getByRole("button", { name: "Отмена" })).toBeDisabled()
|
||||
for (const name of ["Обновить", "История", "Новое правило", "Копировать", "Редактировать правило", "Удалить правило"]) await expect(page.getByRole("button", { name, exact: true, includeHidden: true })).toBeDisabled()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(confirm).toBeVisible()
|
||||
releaseRead!()
|
||||
await expect.poll(() => historyWaiting).toBe(true)
|
||||
await expect(restore).toBeDisabled()
|
||||
await expect(page.getByRole("button", { name: "Обновить", exact: true, includeHidden: true })).toBeDisabled()
|
||||
await expect(page.getByText("Версия применена на роутер", { exact: true })).toHaveCount(0)
|
||||
releaseHistory!()
|
||||
await expect(confirm).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "Обновить", exact: true, includeHidden: true })).toBeEnabled()
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true, includeHidden: true })).toBeEnabled()
|
||||
expect(restores).toBe(3)
|
||||
expect(revisionReads).toBe(2)
|
||||
})
|
||||
}
|
||||
|
||||
test("Filters: двойное обновление удерживает общий замок и освобождает его после ошибки", async ({ page }) => {
|
||||
let deviceReads = 0
|
||||
let release: (() => void) | undefined
|
||||
const gate = new Promise<void>(resolve => { release = resolve })
|
||||
const rule = { id: "rule-1", community: "65001:101", gateway: "", gatewayTunnelId: "", action: "blackhole", description: "Правило" }
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", async route => {
|
||||
const url = new URL(route.request().url()), path = url.pathname
|
||||
if (path === "/api/filters/rules" && url.searchParams.has("serverId")) {
|
||||
deviceReads++
|
||||
if (deviceReads === 2) { await gate; return route.fulfill({ status: 503, json: { error: "Сбой обновления тест" } }) }
|
||||
}
|
||||
return route.fulfill({ json: path === "/api/filters/rules" ? { rulesets: [{ serverId: "1", rules: [rule] }], greTunnels: [], live: true }
|
||||
: path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "", status: "online", latency: 1 }]
|
||||
: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: [] }
|
||||
: path === "/api/recursive-routes" ? { routes: [] } : {} })
|
||||
})
|
||||
await page.goto("/filters")
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await expect.poll(() => deviceReads).toBe(1)
|
||||
const refresh = page.getByRole("button", { name: "Обновить", exact: true, includeHidden: true })
|
||||
await refresh.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
|
||||
await expect.poll(() => deviceReads).toBe(2)
|
||||
for (const name of ["Обновить", "История", "Новое правило", "Копировать", "Редактировать правило", "Удалить правило"]) await expect(page.getByRole("button", { name, exact: true, includeHidden: true })).toBeDisabled()
|
||||
release!()
|
||||
await expect(refresh).toBeEnabled()
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true, includeHidden: true })).toBeDisabled()
|
||||
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
|
||||
await refresh.click()
|
||||
await expect.poll(() => deviceReads).toBe(3)
|
||||
await expect(page.getByRole("button", { name: "Новое правило", exact: true, includeHidden: true })).toBeEnabled()
|
||||
})
|
||||
Reference in new issue
Block a user