Files
MikrotikManager/tests/e2e/wireguard-links.spec.ts
T

530 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, type Page } from "@playwright/test"
import { wgListResponseSchema } from "../../packages/contracts/src/wireguard"
async function setup(page: Page, url: string, failed = 0) {
const control = { partial: false, failed, posts: [] as { path: string; body: unknown }[] }
const servers = [{ id: 1, name: "Источник", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "64512", status: "online", latency: 1, os: "7", model: "CHR", wanUplinks: [] }]
const interfaces = wgListResponseSchema.parse({ interfaces: ["1", "2"].flatMap(serverId => ["*/2", "other"].map(id => ({ id, rosId: id, serverId, serverName: serverId === "1" ? "Источник" : "Второй сервер", name: "wg-same", address: "192.0.2." + (serverId === "1" ? "11" : "22"), listenPort: 13231, mtu: 1420, comment: "", enabled: true, status: "down", peers: [{ id: "peer", rosId: "*P", publicKey: "fixture-key", allowedIps: [serverId === "1" ? "10.0.1.1/32" : "10.0.2.1/32"] }] }))) }).interfaces
servers.push({ ...servers[0], id: 2, name: "Второй сервер", host: "192.0.2.2" });
const probes = ["p1", "p/2"].map(id => ({ id, srcServerId: "1", name: "Проверка сети", target: "192.0.2.10", filter: "main", rtt: null, loss: 100, status: "down", series: [0], enabled: true, showOnDashboard: false }))
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", route => {
const request = route.request()
const path = new URL(request.url()).pathname
if (!["GET", "HEAD"].includes(request.method())) control.posts.push({ path, body: request.postDataJSON() })
if (path === "/api/wireguard" && control.failed) return route.fulfill({ status: control.failed, json: { error: "Мониторинг недоступен" } })
const json = path === "/api/auth/config" ? { required: false }
: path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false }
: path === "/api/servers" ? servers
: path === "/api/uptime/overview" ? { probes, resources: [] }
: path === "/api/uptime/speed-probes" ? { probes: [] }
: path === "/api/uptime/speed-test/runs" ? { runs: [] }
: path === "/api/bgp/sessions" ? []
: path === "/api/wireguard" ? { interfaces: control.partial ? interfaces.filter(t => t.serverId !== "2") : interfaces, failures: control.partial ? [{ serverId: "2", error: "Unavailable" }] : [] }
: path === "/api/filters/gre-tunnels" ? { tunnels: [] }
: path === "/api/vxlan" ? { tunnels: [] }
: path === "/api/traffic/servers" ? { servers: [] }
: path === "/api/events" ? { events: [] }
: path === "/api/internet-path/latest" ? { snapshot: { sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} } }
: {}
return route.fulfill({ json })
})
await page.goto(url)
return control
}
test("WireGuard: переход из очереди выбирает сервер и точный ID без записи", async ({ page }) => {
const control = await setup(page, "/dashboard")
const link = page.getByRole("region", { name: "Требует внимания", exact: true }).locator('a[href="/wireguard?server=2&interface=*%2F2"]')
await expect(link).toBeVisible()
await link.click()
const context = page.getByRole("status", { name: "WireGuard по ссылке" })
await expect(context).toContainText("Второй сервер")
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("192.0.2.22")
await page.getByRole("tab", { name: /Пиры/ }).click()
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
await expect(page.locator("tr[data-row-id]")).toContainText("10.0.2.1/32")
expect(control.posts).toEqual([])
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
await expect(link).toBeVisible()
})
test("WireGuard: прямая ссылка, сброс, назад, reload и ошибка обновления", async ({ page }) => {
const control = await setup(page, "/wireguard?server=2&interface=*%2F2&keep=1")
const context = page.getByRole("status", { name: "WireGuard по ссылке" })
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(1)
await context.getByRole("link", { name: "Все интерфейсы сервера", exact: true }).click()
await expect(page).toHaveURL(/server=2&keep=1$/)
await expect(rows).toHaveCount(2)
await page.goBack()
await expect(rows).toHaveCount(1)
await page.reload()
await expect(context).toContainText("Второй сервер")
control.failed = 503
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(context).toContainText("сохранённые данные могут быть устаревшими")
await expect(rows).toHaveCount(1)
await page.locator("tr[data-row-id]").getByRole("button", { name: "Действия: wg-same" }).click()
await expect(page.getByRole("menuitem", { name: /Удалить/ })).toHaveCount(0)
await page.keyboard.press("Escape")
control.failed = 0
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(context).toContainText("Список, пиры и сводка ограничены")
control.partial = true
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(context).toContainText("Не удалось получить данные выбранного сервера")
await expect(rows).toHaveCount(0)
expect(control.posts).toEqual([])
await page.setViewportSize({ width: 390, height: 844 })
await context.scrollIntoViewIfNeeded()
const box = await context.boundingBox()
expect(box!.x + box!.width).toBeLessThanOrEqual(390)
await page.screenshot({ path: "test-results/wireguard-link-mobile.png", animations: "disabled" })
})
test("WireGuard: ручной выбор сервера снимает выбор туннеля и сохраняет query", async ({ page }) => {
const control = await setup(page, "/wireguard?server=2&interface=*%2F2&keep=1")
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
await page.getByRole("option").filter({ hasText: "Источник" }).click()
await expect(page).toHaveURL(/server=1&keep=1$/)
await expect(page.locator("tr[data-row-id]")).toHaveCount(2)
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toHaveCount(0)
await page.goBack()
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("192.0.2.22")
expect(control.posts).toEqual([])
})
test("WireGuard: одинаковые ID сохраняют отдельные строки интерфейсов и пиров", async ({ page }) => {
await setup(page, "/wireguard")
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(4)
expect(new Set(await rows.evaluateAll(elements => elements.map(el => el.getAttribute("data-row-id")))).size).toBe(4)
await rows.nth(2).click()
await expect(page.getByText("10.0.2.1/32", { exact: true })).toBeVisible()
await expect(page.getByText("10.0.1.1/32", { exact: true })).toHaveCount(0)
await page.getByRole("tab", { name: /Пиры/ }).click()
await expect(rows).toHaveCount(4)
expect(new Set(await rows.evaluateAll(elements => elements.map(el => el.getAttribute("data-row-id")))).size).toBe(4)
})
for (const query of ["server=missing&interface=*%2F2", "server=2&interface=missing", "interface=*%2F2"]) {
test("WireGuard: нет подмены объекта " + query, async ({ page }) => {
const control = await setup(page, "/wireguard?" + query)
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("Интерфейс не найден или недоступен")
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
expect(control.posts).toEqual([])
})
}
for (const status of [403, 500]) {
test("WireGuard: ошибка API " + status, async ({ page }) => {
const control = await setup(page, "/wireguard?server=2&interface=*%2F2", status)
await expect(page.getByRole("status", { name: "WireGuard по ссылке" })).toContainText("Не удалось получить данные выбранного сервера")
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
expect(control.posts).toEqual([])
})
}
for (const kind of ["iface", "peer"] as const) {
test("WireGuard: подтверждение удаления и повтор после ошибки " + kind, async ({ page }) => {
await setup(page, "/wireguard?server=1&interface=*%2F2")
let attempts = 0
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/wireguard/" + (kind === "iface" ? "interfaces" : "peers") + "/**", async route => {
expect(route.request().method()).toBe("DELETE")
expect(decodeURIComponent(new URL(route.request().url()).pathname)).toBe(
kind === "iface" ? "/api/wireguard/interfaces/1/*/2" : "/api/wireguard/peers/1/*P",
)
attempts++
if (attempts === 1) {
await gate
await route.fulfill({ status: 403, json: { error: "Нет доступа к удалению" } })
} else if (attempts === 2) {
await route.fulfill({ json: { ok: false } })
} else {
await route.fulfill({ json: { ok: true } })
}
})
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
if (kind === "iface") {
await page.getByRole("button", { name: "Действия: wg-same" }).click()
await page.getByRole("menuitem", { name: /Удалить/ }).click()
} else {
await page.getByRole("tab", { name: /Пиры/ }).click()
await page.getByRole("button", { name: "Удалить пира", exact: true }).click()
}
const dialog = page.getByRole("alertdialog")
await expect(dialog).toContainText("Источник")
await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
await expect(dialog.getByRole("button", { name: "Удаление…" })).toBeDisabled()
await expect(dialog.getByRole("button", { name: "Отмена" })).toBeDisabled()
await page.keyboard.press("Escape")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("status")).toContainText("Ожидание подтверждения")
expect(attempts).toBe(1)
release!()
await expect(dialog.getByRole("alert")).toContainText("Нет доступа")
await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText("Сервер не подтвердил удаление")
await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
await expect(dialog).toHaveCount(0)
expect(attempts).toBe(3)
})
}
test("WireGuard: импорт сохраняет форму без подтверждения применения", async ({ page }) => {
await setup(page, "/wireguard?server=1")
let attempts = 0
const preview = { format: "rsc", interface: { name: "wg-import-test" }, peers: [] }
const content = "/interface wireguard add name=wg-import-test listen-port=13231"
await page.route("**/api/wireguard/import", async route => {
expect(route.request().postDataJSON()).toEqual({ serverId: "1", content, format: "auto", dryRun: false })
attempts++
if (attempts === 1) return route.fulfill({ status: 403, json: { error: "Нет доступа к импорту" } })
if (attempts === 2) return route.fulfill({ json: { dryRun: true, preview } })
if (attempts === 3) return route.fulfill({ json: { dryRun: false, preview } })
if (attempts === 4) return route.fulfill({ json: {} })
return route.fulfill({ json: { dryRun: false, preview, applied: { interfaceName: "wg-import-test", peersCreated: 0 } } })
})
await page.getByRole("button", { name: "Импорт", exact: true }).click()
const dialog = page.getByRole("dialog", { name: "Импорт конфига WireGuard" })
await dialog.locator("textarea").fill(content)
for (const message of ["Нет доступа к импорту", "Сервер не подтвердил применение конфигурации", "Сервер не подтвердил применение конфигурации", "Некорректный ответ импорта WireGuard"]) {
const before = attempts
await dialog.getByRole("button", { name: "Применить на роутер" }).click()
await expect.poll(() => attempts).toBe(before + 1)
await expect(page.getByText(message, { exact: true }).first()).toBeVisible()
await expect(dialog.locator("textarea")).toHaveValue(content)
await expect(dialog.locator("select").first()).toHaveValue("1")
await expect(dialog.getByRole("button", { name: "Применить на роутер" })).toBeEnabled()
}
await dialog.getByRole("button", { name: "Применить на роутер" }).click()
await expect(dialog).toHaveCount(0)
await expect(page.getByText("Импортировано: wg-import-test (+0 пиров)", { exact: true })).toBeVisible()
expect(attempts).toBe(5)
})
test("WireGuard: импорт удерживает форму до ответа и разрешает повтор", async ({ page }) => {
await setup(page, "/wireguard?server=1")
let attempts = 0
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
const content = "/interface wireguard add name=wg-wait"
await page.route("**/api/wireguard/import", async route => {
expect(route.request().postDataJSON()).toEqual({ serverId: "1", content, format: "auto", dryRun: false })
attempts++
if (attempts === 1) {
await gate
return route.fulfill({ status: 500, json: { error: "Ошибка импорта тест" } })
}
return route.fulfill({ json: { dryRun: false, preview: { format: "rsc", interface: { name: "wg-wait" }, peers: [] }, applied: { interfaceName: "wg-wait", peersCreated: 0 } } })
})
await page.getByRole("button", { name: "Импорт", exact: true }).click()
const dialog = page.getByRole("dialog", { name: "Импорт конфига WireGuard" })
await dialog.getByRole("textbox", { name: "Конфигурация WireGuard" }).fill(content)
const submit = dialog.getByRole("button", { name: "Применить на роутер" })
await submit.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect.poll(() => attempts).toBe(1)
await expect(dialog.getByRole("status")).toContainText("Ожидание результата")
await expect(dialog.getByRole("textbox")).toBeDisabled()
await expect(dialog.getByRole("combobox", { name: "Сервер" })).toBeDisabled()
await expect(dialog.getByRole("combobox", { name: "Формат" })).toBeDisabled()
await expect(dialog.getByRole("button", { name: "Отмена" })).toBeDisabled()
await expect(dialog.getByRole("button", { name: "Закрыть", exact: true })).toHaveCount(0)
await page.keyboard.press("Escape")
await page.mouse.click(10, 300)
await expect(dialog).toBeVisible()
expect(attempts).toBe(1)
release!()
await expect(dialog.getByRole("alert")).toHaveText("Ошибка импорта тест")
await expect(dialog.getByRole("textbox")).toHaveValue(content)
await expect(submit).toBeEnabled()
await submit.click()
await expect(dialog).toHaveCount(0)
expect(attempts).toBe(2)
await page.getByRole("button", { name: "Импорт", exact: true }).click()
await expect(dialog.getByRole("alert")).toHaveCount(0)
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
})
for (const kind of ["interface", "interface-dto", "peer"] as const) {
test("WireGuard: создание сохраняет форму и ждёт подтверждения " + kind, async ({ page }) => {
await setup(page, "/wireguard?server=1&interface=*%2F2")
const peer = kind === "peer"
let attempts = 0
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/wireguard/" + (peer ? "peers" : "interfaces"), async route => {
const body = route.request().postDataJSON()
expect(body.serverId).toBe("1")
expect(body[peer ? "interfaceName" : "name"]).toBe(peer ? "wg-same" : "wg-new")
if (peer) {
expect(body.publicKey).toBe("fixture-public-key")
expect(body.allowedAddresses).toEqual(["10.0.3.1/32"])
}
attempts++
if (attempts === 1) {
await gate
return route.fulfill({ status: 403, json: { error: "Создание запрещено тест" } })
}
if (attempts === 2) return route.fulfill({ json: {} })
const dto = wgListResponseSchema.parse({ interfaces: [{ id: "new", rosId: "*N", serverId: "1", serverName: "Источник", name: "wg-new", listenPort: 13231, mtu: 1420, comment: "", enabled: true, status: "down", peers: [] }] }).interfaces[0]
return route.fulfill({ status: 201, json: kind === "interface-dto" ? dto : peer ? { ok: true } : { ok: true, name: "wg-new" } })
})
const open = async () => {
if (peer) {
await page.getByRole("button", { name: "Действия: wg-same" }).click()
await page.getByRole("menuitem", { name: "Добавить пира" }).click()
} else await page.getByRole("button", { name: "Новый интерфейс", exact: true }).click()
}
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
await open()
const dialog = page.getByRole("dialog", { name: peer ? "Добавить пира" : "Быстрый туннель WireGuard" })
const input = dialog.getByRole("textbox", { name: peer ? "Public key" : "Имя интерфейса", exact: true })
await input.fill(peer ? "fixture-public-key" : "wg-new")
if (peer) await dialog.getByRole("textbox", { name: "Allowed IPs", exact: true }).fill("10.0.3.1/32")
const submit = dialog.getByRole("button", { name: peer ? "Добавить" : "Создать туннель", exact: true })
await submit.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect.poll(() => attempts).toBe(1)
await expect(input).toBeDisabled()
if (!peer) await expect(dialog.getByRole("switch", { name: "Интерфейс включён" })).toBeDisabled()
await expect(dialog.getByRole("status")).toContainText("Ожидание подтверждения")
await expect(dialog.getByRole("button", { name: "Отмена" })).toBeDisabled()
await expect(dialog.getByRole("button", { name: "Закрыть", exact: true })).toHaveCount(0)
await page.keyboard.press("Escape")
await page.mouse.click(10, 300)
await expect(dialog).toBeVisible()
release!()
await expect(dialog.getByRole("alert")).toHaveText("Создание запрещено тест")
await expect(input).toHaveValue(peer ? "fixture-public-key" : "wg-new")
await submit.click()
await expect(dialog.getByRole("alert")).toContainText("Сервер не подтвердил создание")
await submit.click()
await expect(dialog).toHaveCount(0)
expect(attempts).toBe(3)
await open()
await expect(dialog.getByRole("alert")).toHaveCount(0)
await expect(input).toHaveValue("")
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
})
}
test("WireGuard: переключение ждёт подтверждения и защищено от повтора", async ({ page }) => {
await setup(page, "/wireguard?server=1&interface=*%2F2")
let attempts = 0
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/wireguard/interfaces/**", async route => {
expect(route.request().method()).toBe("PATCH")
expect(decodeURIComponent(new URL(route.request().url()).pathname)).toBe("/api/wireguard/interfaces/1/*/2")
expect(route.request().postDataJSON()).toEqual({ disabled: true })
attempts++
if (attempts === 1) {
await gate
return route.fulfill({ status: 403, json: { error: "Нет доступа к переключению" } })
}
return route.fulfill({ json: { ok: attempts > 2 } })
})
const menu = page.getByRole("button", { name: "Действия: wg-same" })
await menu.click()
await page.getByRole("menuitem", { name: "Отключить", exact: true }).click()
await expect.poll(() => attempts).toBe(1)
await expect(page.getByRole("status").filter({ hasText: "Изменение интерфейса wg-same · Источник" })).toBeVisible()
await menu.click()
await expect(page.getByRole("menuitem", { name: "Отключить", exact: true })).toBeDisabled()
await page.keyboard.press("Escape")
expect(attempts).toBe(1)
release!()
await expect(page.getByRole("alert").filter({ hasText: "wg-same · Источник: Нет доступа к переключению" })).toBeVisible()
await menu.click()
await page.getByRole("menuitem", { name: "Отключить", exact: true }).click()
await expect(page.getByRole("alert").filter({ hasText: "Сервер не подтвердил изменение интерфейса" })).toBeVisible()
await menu.click()
await page.getByRole("menuitem", { name: "Отключить", exact: true }).click()
await expect(page.getByText("Отключено", { exact: true })).toBeVisible()
await expect(page.getByRole("alert").filter({ hasText: "Сервер не подтвердил" })).toHaveCount(0)
expect(attempts).toBe(3)
})
for (const oldFails of [false, true]) {
test("WireGuard: поздний экспорт не меняет новую панель " + oldFails, async ({ page }) => {
await setup(page, "/wireguard")
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
let requests = 0
await page.route("**/api/wireguard/export", async route => {
requests++
const body = route.request().postDataJSON()
expect(body.format).toBe("rsc")
if (body.serverId === "1") {
await gate
return oldFails ? route.fulfill({ status: 500, json: { error: "Старая ошибка экспорта" } }) : route.fulfill({ json: { format: "rsc", filename: "old.rsc", content: "OLD_EXPORT" } })
}
expect(body.serverId).toBe("2")
return route.fulfill({ json: { format: "rsc", filename: "new.rsc", content: "NEW_EXPORT" } })
})
const open = async (index: number) => {
await page.getByRole("button", { name: "Действия: wg-same" }).nth(index).click()
await page.getByRole("menuitem", { name: "Экспорт", exact: true }).click()
}
await expect(page.locator("tr[data-row-id]")).toHaveCount(4)
await open(0)
const dialog = page.getByRole("dialog", { name: "Экспорт WireGuard" })
await dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect.poll(() => requests).toBe(1)
await expect(dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key", exact: true })).toBeDisabled()
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
await open(2)
await expect(dialog).toContainText("Второй сервер")
await dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key", exact: true }).click()
await expect(dialog.locator("pre")).toContainText("NEW_EXPORT")
const oldResponse = page.waitForResponse(response => response.url().endsWith("/api/wireguard/export") && response.request().postDataJSON().serverId === "1")
release!()
await (await oldResponse).finished()
await page.waitForTimeout(150)
await expect(dialog.locator("pre")).toContainText("NEW_EXPORT")
await expect(page.getByText("Старая ошибка экспорта", { exact: true })).toHaveCount(0)
expect(requests).toBe(2)
})
}
test("WireGuard: экспорт отклоняет повреждённый ответ и другой формат", async ({ page }) => {
await setup(page, "/wireguard?server=1&interface=*%2F2")
let attempts = 0
await page.route("**/api/wireguard/export", route => {
attempts++
return route.fulfill({ json: attempts === 1 ? {} : attempts === 2 ? { format: "conf", filename: "wrong.conf", content: "WRONG_FORMAT" } : { format: "rsc", filename: "valid.rsc", content: "VALID_EXPORT" } })
})
await page.getByRole("button", { name: "Действия: wg-same" }).click()
await page.getByRole("menuitem", { name: "Экспорт", exact: true }).click()
const dialog = page.getByRole("dialog", { name: "Экспорт WireGuard" })
for (let i = 0; i < 2; i++) {
await dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key", exact: true }).click()
await expect.poll(() => attempts).toBe(i + 1)
await expect(page.getByText("Некорректный ответ экспорта WireGuard", { exact: true }).first()).toBeVisible()
await expect(dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key", exact: true })).toBeEnabled()
await expect(dialog.locator("pre")).not.toContainText("WRONG_FORMAT")
}
await dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key", exact: true }).click()
await expect(dialog.locator("pre")).toContainText("VALID_EXPORT")
})
test("WireGuard: экспорт второго пира использует его RouterOS ID", async ({ page }) => {
await setup(page, "/wireguard?server=1")
const fixture = wgListResponseSchema.parse({ interfaces: [{ id: "iface", rosId: "*I", serverId: "1", serverName: "Источник", name: "wg-peers", publicKey: "fixture-server-key", listenPort: 13231, mtu: 1420, enabled: true, status: "up", comment: "", peers: [
{ id: "first", rosId: "*P1", publicKey: "fixture-peer-one", allowedIps: ["10.0.1.1/32"] },
{ id: "second", rosId: "*P2", publicKey: "fixture-peer-two", allowedIps: ["10.0.2.1/32"] },
] }] })
await page.route("**/api/wireguard", route => route.fulfill({ json: fixture }))
let body: Record<string, unknown> | undefined
await page.route("**/api/wireguard/export", route => {
body = route.request().postDataJSON()
return route.fulfill({ json: { format: "peer-conf", filename: "peer.conf", content: "SECOND_PEER_EXPORT" } })
})
await page.reload()
await page.getByRole("tab", { name: /Пиры/ }).click()
await expect(page.locator("tr[data-row-id]")).toHaveCount(2)
await page.locator("tr[data-row-id]").filter({ hasText: "10.0.2.1/32" }).getByRole("button", { name: "Экспорт peer .conf" }).click()
const dialog = page.getByRole("dialog", { name: "Экспорт WireGuard" })
await expect(dialog.locator("pre")).toContainText("10.0.2.1/32")
await expect(dialog.locator("pre")).not.toContainText("10.0.1.1/32")
await dialog.getByRole("button", { name: "Подтянуть конфиг с роутера с private-key" }).click()
await expect(dialog.locator("pre")).toContainText("SECOND_PEER_EXPORT")
expect(body).toMatchObject({ serverId: "1", interfaceName: "wg-peers", peerId: "*P2", format: "peer-conf", includePrivateKey: false })
})
test("WireGuard: восстановление требует подтверждения API", async ({ page }) => {
await setup(page, "/wireguard?server=1")
await page.route("**/api/wireguard/revisions?**", route => route.fulfill({ json: { revisions: [{ id: "rev/1", serverId: "1", section: "wireguard", source: "apply", fingerprint: "fixture", createdAt: "2026-09-29T00:00:00Z", note: null, itemCount: 1 }] } }))
let attempts = 0
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/wireguard/revisions/*/restore", async route => {
expect(decodeURIComponent(new URL(route.request().url()).pathname)).toBe("/api/wireguard/revisions/rev/1/restore")
expect(route.request().postDataJSON()).toEqual({ serverId: "1" })
attempts++
if (attempts === 1) await gate
return route.fulfill({ json: { ok: attempts > 1 } })
})
await page.getByRole("button", { name: "История", exact: true }).click()
const sheet = page.getByRole("dialog", { name: "История WireGuard" })
await sheet.getByRole("button", { name: "Откатить", exact: true }).click()
const confirm = page.getByRole("alertdialog")
await confirm.getByRole("button", { name: "Откатить", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect.poll(() => attempts).toBe(1)
await expect(confirm.getByRole("button", { name: "Откатить", exact: true })).toBeDisabled()
release!()
await expect(confirm.getByRole("alert").filter({ hasText: "Сервер не подтвердил восстановление" })).toBeVisible()
await expect(page.getByText("Версия применена на роутер", { exact: true })).toHaveCount(0)
await expect(confirm).toBeVisible()
await confirm.getByRole("button", { name: "Откатить", exact: true }).click()
await expect(page.getByText("Версия применена на роутер", { exact: true })).toBeVisible()
expect(attempts).toBe(2)
})
test("WireGuard: ошибка истории не становится пустым списком", async ({ page }) => {
await setup(page, "/wireguard?server=1")
let attempts = 0
await page.route("**/api/wireguard/revisions?**", route => {
attempts++
if (attempts === 1) return route.fulfill({ status: 403, json: { error: "История запрещена тест" } })
if (attempts === 2) return route.fulfill({ json: {} })
if (attempts === 3) return route.fulfill({ json: { revisions: [{ id: "wrong", serverId: "2", section: "wireguard", source: "apply", fingerprint: "fixture", createdAt: "2026-09-29T00:00:00Z", note: null, itemCount: 1 }] } })
return route.fulfill({ json: { revisions: [] } })
})
await page.getByRole("button", { name: "История", exact: true }).click()
const sheet = page.getByRole("dialog", { name: "История WireGuard" })
for (const message of ["История запрещена тест", "Некорректный ответ истории WireGuard", "Некорректный ответ истории WireGuard"]) {
await expect(sheet.getByRole("alert")).toContainText(message)
await expect(sheet.getByText("Истории пока нет", { exact: true })).toHaveCount(0)
await expect(sheet.getByRole("button", { name: "Откатить", exact: true })).toHaveCount(0)
await sheet.getByRole("button", { name: "Повторить загрузку" }).click()
}
await expect(sheet.getByText("Истории пока нет", { exact: true })).toBeVisible()
await expect(sheet.getByRole("alert")).toHaveCount(0)
expect(attempts).toBe(4)
})
for (const oldFails of [false, true]) {
test("WireGuard: закрытая история не меняет новый запрос " + oldFails, async ({ page }) => {
await setup(page, "/wireguard?server=1")
let releaseOld: (() => void) | undefined
let releaseNew: (() => void) | undefined
const oldGate = new Promise<void>(resolve => { releaseOld = resolve })
const newGate = new Promise<void>(resolve => { releaseNew = resolve })
let attempts = 0
await page.route("**/api/wireguard/revisions?**", async route => {
const current = ++attempts
await (current === 1 ? oldGate : newGate)
if (current === 1 && oldFails) return route.fulfill({ status: 500, json: { error: "Старая ошибка истории" } })
return route.fulfill({ json: { revisions: current === 1 ? [] : [{ id: "new", serverId: "1", section: "wireguard", source: "apply", fingerprint: "fixture", createdAt: "2026-09-29T00:00:00Z", note: null, itemCount: 9 }] } })
})
const open = page.getByRole("button", { name: "История", exact: true })
const sheet = page.getByRole("dialog", { name: "История WireGuard" })
await open.click()
await expect.poll(() => attempts).toBe(1)
await expect(sheet.getByRole("status")).toContainText("Загрузка истории")
await page.keyboard.press("Escape")
await expect(sheet).toHaveCount(0)
await open.click()
await expect.poll(() => attempts).toBe(2)
const response = page.waitForResponse(r => r.url().includes("/api/wireguard/revisions?"))
releaseOld!()
await (await response).finished()
await page.waitForTimeout(150)
await expect(sheet.getByRole("status")).toContainText("Загрузка истории")
await expect(sheet.getByRole("alert")).toHaveCount(0)
releaseNew!()
await expect(sheet).toContainText("9 интерфейсов")
await expect(sheet.getByRole("status")).toHaveCount(0)
await expect(sheet.getByRole("button", { name: "Откатить", exact: true })).toBeVisible()
})
}