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

306 lines
20 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"
async function setup(page: Page, url: string, failed = 0, longNames = false, permission?: string, isAdmin = false) {
const control = { writes: [] as { method: string; path: string; body: unknown }[], 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 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 }))
if (permission) {
await page.addInitScript(token => localStorage.setItem("mmapp_token", token), uptimeToken(permission, isAdmin))
servers.push({ ...servers[0], id: 2, name: "Назначение", host: "192.0.2.2" })
}
if (longNames) {
servers[0].name = "Источник-с-длинным-названием-площадки-для-проверки-переноса"
probes.forEach(p => { p.name = "Проверка-доступности-длинного-имени-сетевого-узла"; p.target = "router-with-a-long-hostname.example.test"; p.filter = "Фильтр-с-длинным-названием" })
}
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.writes.push({ method: request.method(), path, body: request.postDataJSON() })
if (request.method() === "POST") control.posts.push({ path, body: request.postDataJSON() })
if (path === "/api/uptime/overview" && control.failed) return route.fulfill({ status: control.failed, json: { error: "Мониторинг недоступен" } })
const json = path === "/api/auth/config" ? { required: Boolean(permission) }
: 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: permission ? [{ id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "", dstInterface: "", protocol: "tcp", direction: "both", durationSec: "10", enabled: true }] : [] }
: path === "/api/uptime/speed-test/runs" ? { runs: [] }
: path === "/api/bgp/sessions" ? []
: path === "/api/filters/gre-tunnels" || path === "/api/vxlan" ? { tunnels: [] }
: path === "/api/wireguard" ? { interfaces: [], peers: [] }
: 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("очередь проверок: точный ID, чтение без ping, ручной ping только выбранной проверки", async ({ page }) => {
const control = await setup(page, "/dashboard")
const queue = page.getByRole("region", { name: "Требует внимания", exact: true })
const link = queue.locator('a[href="/uptime?probe=p%2F2"]')
await expect(link).toBeVisible()
await link.click()
await expect(page).toHaveURL(/\/uptime\?probe=p%2F2$/)
const context = page.getByRole("status", { name: "Проверка по ссылке" })
await expect(context).toContainText("Выбрана проверка: Проверка сети")
await expect(page.locator("[data-probe-id]")).toHaveCount(1)
await expect(page.locator('[data-probe-id="p/2"]')).toBeVisible()
expect(control.posts).toEqual([])
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(context).toContainText("Список ограничен")
expect(control.posts).toEqual([])
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect.poll(() => control.posts).toEqual([{ path: "/api/uptime/probes/collect-group", body: { probeIds: ["p/2"] } }])
await context.getByRole("link", { name: "К дашборду", exact: true }).click()
await expect(queue).toBeVisible()
})
test("проверка по прямой ссылке: сброс, возврат, перезагрузка и частичный сбой", async ({ page }) => {
const control = await setup(page, "/uptime?probe=p%2F2&keep=1")
const context = page.getByRole("status", { name: "Проверка по ссылке" })
await expect(context).toContainText("Выбрана проверка")
await context.getByRole("link", { name: "Все проверки", exact: true }).click()
await expect(page).toHaveURL(/\/uptime\?keep=1$/)
await expect(page.locator("[data-probe-id]")).toHaveCount(2)
await page.goBack()
await expect(page.locator("[data-probe-id]")).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(page.locator('[data-probe-id="p/2"]')).toBeVisible()
control.failed = 0
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await expect(context).toContainText("Список ограничен")
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/uptime-link-mobile.png", animations: "disabled" })
})
for (const failed of [0, 403, 500]) {
test(`недоступная проверка: ${failed || "не найден ID"}`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=missing", failed)
await expect(page.getByRole("status", { name: "Проверка по ссылке" })).toContainText(failed ? "Не удалось обновить данные проверки" : "Проверка не найдена или недоступна")
await expect(page.locator("[data-probe-id]")).toHaveCount(0)
expect(control.posts).toEqual([])
})
}
for (const width of [320, 390, 1440]) {
test(`Ping-пробы ${width}px: длинные подписи, доступные действия и раскрытие`, async ({ page }) => {
await page.setViewportSize({ width, height: 1000 })
const control = await setup(page, "/uptime?probe=p%2F2", 0, true)
const row = page.locator('[data-probe-id="p/2"]')
await expect(row).toBeVisible()
await expect(row.getByText("График RTT", { exact: true })).toBeVisible({ visible: width < 1440 })
if (width < 1440) expect(await row.getByText("Фильтр-с-длинным-названием", { exact: true }).evaluate(el => el.getBoundingClientRect().right)).toBeLessThanOrEqual(width)
for (const name of ["Редактировать пробу", "Удалить пробу", "Показать на дашборде"]) {
const button = row.getByRole("button", { name, exact: true })
await button.scrollIntoViewIfNeeded()
const bounds = await button.boundingBox()
expect(bounds!.x).toBeGreaterThanOrEqual(0)
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
}
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width)
const toggle = row.getByRole("switch", { name: /Включить пробу/ })
await expect(toggle).toBeChecked()
const chart = row.getByRole("button", { name: /Подробный график RTT/ })
await chart.focus()
await page.keyboard.press("Enter")
await expect(chart).toHaveAttribute("aria-expanded", "true")
await page.keyboard.press("Enter")
await expect(chart).toHaveAttribute("aria-expanded", "false")
await row.scrollIntoViewIfNeeded()
await page.screenshot({ path: `test-results/ping-row-${width}.png`, animations: "disabled" })
expect(control.writes).toEqual([])
await row.getByRole("button", { name: "Редактировать пробу", exact: true }).click()
await expect(page.getByRole("dialog")).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("dialog")).toBeHidden()
await row.getByRole("button", { name: "Показать на дашборде", exact: true }).click()
await expect.poll(() => control.writes.some(w => w.method === "PATCH" && w.path === "/api/uptime/probes/p%2F2" && (w.body as { showOnDashboard: boolean }).showOnDashboard === true)).toBe(true)
await toggle.click()
await expect.poll(() => control.writes.some(w => w.method === "PUT" && w.path === "/api/uptime/probes")).toBe(true)
})
}
// UI claim fixtures; JWT verification remains a separate integration check.
function uptimeToken(permission: string, isAdmin = false) {
return "fixture." + Buffer.from(JSON.stringify({ apps: ["mm"], permissions: ["mm:servers:read", permission], is_admin: isAdmin, exp: Math.floor(Date.now() / 1000) + 3600 })).toString("base64url") + ".fixture"
}
for (const [permission, isAdmin] of [["mm:uptime:read", false], ["mm:uptime:read", true], ["mm:servers:write", false]] as const) {
test(`мониторинг только чтение: ${permission}, is_admin=${isAdmin}`, async ({ page }) => {
const control = await setup(page, "/uptime", 0, false, permission, isAdmin)
await expect(page.getByText("Только просмотр", { exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeDisabled()
const row = page.locator('[data-probe-id="p1"]')
for (const name of ["Показать на дашборде", "Редактировать пробу", "Удалить пробу"]) {
await expect(row.getByRole("button", { name, exact: true })).toBeDisabled()
}
await expect(row.getByRole("switch")).toBeDisabled()
await expect(page.getByRole("button", { name: /Добавить интерфейс для/ })).toBeDisabled()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeDisabled()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await page.getByRole("button", { name: /Ресурсы/ }).click()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(page.getByRole("button", { name: "Новая speed-проба", exact: true })).toBeDisabled()
for (const name of ["Запустить", "Редактировать speed-пробу", "Удалить speed-пробу"]) {
await expect(page.getByRole("button", { name, exact: true })).toBeDisabled()
}
await expect(page.getByRole("switch")).toBeDisabled()
await page.getByRole("button", { name: "Обновить данные", exact: true }).click()
expect(control.writes).toEqual([])
})
}
for (const permission of ["mm:uptime:write", "mm:uptime:admin"]) {
test(`мониторинг: ${permission} разрешает ручной ping`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=p1", 0, false, permission)
await expect(page.getByRole("button", { name: "Новая проба", exact: true })).toBeEnabled()
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect.poll(() => control.posts.map(p => p.path)).toEqual(["/api/uptime/probes/collect-group"])
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(page.getByRole("button", { name: "Запустить", exact: true })).toBeEnabled()
})
}
for (const speed of [false, true]) {
test(`мониторинг: отзыв права перед сохранением ${speed ? "Speed" : "Ping"}`, async ({ page }) => {
const control = await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
await expect(page.getByRole("dialog")).toBeVisible()
await page.evaluate(token => localStorage.setItem("mmapp_token", token), uptimeToken("mm:uptime:read"))
await page.getByRole("dialog").getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(page.getByRole("dialog")).toBeVisible()
await expect(page.getByRole("dialog").getByRole("alert")).toContainText("Недостаточно прав")
expect(control.writes).toEqual([])
})
}
for (const speed of [false, true]) {
for (const status of [400, 403, 500]) {
test(`сохранение ${speed ? "Speed" : "Ping"}: ${status} сохраняет черновик, повтор подтверждён`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let fail = true
const bodies: { probes: Record<string, unknown>[] }[] = []
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", route => {
if (route.request().method() !== "PUT") return route.fallback()
bodies.push(route.request().postDataJSON())
return route.fulfill({ status: fail ? status : 200, json: fail ? { error: "Сохранение отклонено" } : { ok: true } })
})
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
const input = speed ? dialog.locator('input').filter({ visible: true }).last() : dialog.getByPlaceholder("youtube.com")
await input.fill(speed ? "17" : "Черновик проверки")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText(status === 403 ? "Недостаточно прав" : "Сохранение отклонено")
await expect(input).toHaveValue(speed ? "17" : "Черновик проверки")
fail = false
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toBeHidden()
expect(bodies).toHaveLength(2)
expect(bodies[1]).toEqual(bodies[0])
expect(bodies[1].probes[0][speed ? "durationSec" : "name"]).toBe(speed ? 17 : "Черновик проверки")
})
}
test(`сохранение ${speed ? "Speed" : "Ping"}: один PUT, Escape и отмена заблокированы до ответа`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
calls++
await pending
await route.fulfill({ json: { ok: true } })
})
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect(dialog.getByRole("button", { name: "Сохранение…", exact: true })).toBeDisabled()
await expect(dialog.getByRole("button", { name: "Отмена", exact: true })).toBeDisabled()
await expect(dialog.locator("fieldset input").last()).toBeDisabled()
await page.keyboard.press("Escape")
await expect(dialog).toBeVisible()
expect(calls).toBe(1)
release()
await expect(dialog).toBeHidden()
})
}
for (const speed of [false, true]) {
test(`сохранение ${speed ? "Speed" : "Ping"}: таймаут сохраняет форму с неизвестным результатом`, async ({ page }) => {
await page.addInitScript(() => {
const timeout = AbortSignal.timeout.bind(AbortSignal)
AbortSignal.timeout = ms => timeout(ms === 30_000 ? 100 : ms)
})
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
await pending
await route.fulfill({ json: { ok: true } }).catch(() => {})
})
await page.getByRole("button", { name: speed ? "Редактировать speed-пробу" : "Редактировать пробу", exact: true }).click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog.getByRole("alert")).toContainText("Результат операции неизвестен")
await expect(dialog.getByRole("button", { name: "Сохранить", exact: true })).toBeEnabled()
release()
await expect(dialog).toBeVisible()
})
}
for (const speed of [false, true]) {
test(`переключение ${speed ? "Speed" : "Ping"}: ожидание API, отказ, повтор без потери состояния`, async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
if (speed) await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let fail = true
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => {
if (route.request().method() !== "PUT") return route.fallback()
calls++
expect(route.request().postDataJSON().probes[0].enabled).toBe(false)
if (fail) await pending
await route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: "Forbidden" } : { ok: true } })
})
const toggle = page.getByRole("switch")
await expect(toggle).toBeChecked()
await toggle.click()
await expect(toggle).toBeDisabled()
await expect(toggle).toBeChecked()
await expect.poll(() => calls).toBe(1)
release()
await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toBeVisible()
await expect(toggle).toBeChecked()
await expect(toggle).toBeEnabled()
fail = false
await toggle.click()
await expect(toggle).not.toBeChecked()
await expect(toggle).toBeEnabled()
await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toHaveCount(0)
expect(calls).toBe(2)
})
}