Files
MikrotikManager/tests/e2e/data-collection-settings.spec.ts

145 lines
16 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test"
import { geoip } from "./fixtures/geoip"
const common = { enabled: true, intervalSec: 300, retentionDays: 14, lastCollectedAt: null, lastDurationMs: 0, lastError: null }
const values: Record<string, object> = {
"/api/traffic/settings": { ...common, intervalSec: 45 },
"/api/servers-api-ping/settings": { ...common, enabled: false, intervalSec: 120 },
"/api/uptime/settings": { ...common, resourcesEnabled: true, pingEnabled: false, speedEnabled: false, probeIntervalSec: 15, speedIntervalSec: 60 },
"/api/internet-path/settings": common,
"/api/certificates/renew-settings": { enabled: false, intervalSec: 21600, renewBeforeDays: 30 },
}
async function setup(page: Page) {
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
if (path === "/api/auth/config") return r.fulfill({ json: { required: false } })
if (path === "/api/geoip") return r.fulfill({ json: geoip })
if (path === "/api/scheduler/status") return r.fulfill({ json: { jobs: [] } })
if (path === "/api/scheduler/runs") return r.fulfill({ json: { runs: [] } })
if (values[path]) return r.fulfill({ json: values[path] })
return r.fulfill({ status: 503, json: { error: "outside settings fixture" } })
})
}
test("Настройки: неполный DTO не включает defaults и не разрешает запись", async ({ page }) => {
await setup(page); await page.route("**/api/traffic/settings", r => r.fulfill({ json: {} }))
await page.goto("/data-collection")
await expect(page.getByTestId("settings-traffic")).toContainText("Настройки сборщика не подтверждены")
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toHaveValue("")
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Сохранить настройки планировщика" })).toBeDisabled()
await expect(page.getByTestId("settings-rest")).toContainText("настройки получены")
})
test("Настройки: медленный uptime не задерживает трафик и REST", async ({ page }) => {
await setup(page); let started = false, release!: () => void; const gate = new Promise<void>(resolve => release = resolve)
await page.route("**/api/uptime/settings", async r => { started = true; await gate; try { await r.fulfill({ json: values["/api/uptime/settings"] }) } catch {} })
try {
await page.goto("/data-collection"); await expect.poll(() => started).toBe(true)
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toHaveValue("45")
await expect(page.getByTestId("settings-rest")).toContainText("настройки получены")
await expect(page.getByTestId("settings-uptime")).toContainText("настройки не получены")
} finally { release() }
})
test("Настройки: фоновая ошибка сохраняет данные, время и черновик", async ({ page }) => {
await setup(page); let fails = false
await page.route("**/api/traffic/settings", r => r.fulfill(fails ? { status: 503, json: { error: "traffic unavailable" } } : { json: values["/api/traffic/settings"] }))
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })
await expect(field).toHaveValue("45"); await field.fill("99")
const received = await page.getByTestId("settings-traffic").getByRole("status").textContent(); fails = true
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled(); await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByTestId("settings-traffic")).toContainText("traffic unavailable")
await expect(field).toHaveValue("99"); await expect(page.getByTestId("settings-traffic").getByRole("status")).toHaveText(received!)
})
test("Настройки: успешное обновление не затирает черновик", async ({ page }) => {
await setup(page); await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })
await expect(field).toHaveValue("45"); await field.fill("99")
await page.getByRole("textbox", { name: "Хранение трафика", exact: true }).fill("21")
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled(); await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled(); await expect(field).toHaveValue("99")
await expect(page.getByRole("textbox", { name: "Хранение трафика", exact: true })).toHaveValue("21")
})
test("Настройки: сохраняется только изменённое поле и сбрасывается dirty после receipt", async ({ page }) => {
await setup(page); const writes: { path: string; body: object }[] = []
await page.route("**/api/traffic/settings", r => { if (r.request().method() !== "PUT") return r.fallback(); const body = r.request().postDataJSON(); writes.push({ path: new URL(r.request().url()).pathname, body }); return r.fulfill({ json: { ok: true, settings: { ...values["/api/traffic/settings"], ...body } } }) })
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99")
const save = page.getByRole("button", { name: "Сохранить настройки планировщика" }); await save.click()
await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toBeVisible(); await expect(save).toBeDisabled()
expect(writes).toEqual([{ path: "/api/traffic/settings", body: { intervalSec: 99 } }])
})
for (const raw of [{}, { ok: false }, { ok: true, settings: { ...values["/api/traffic/settings"], intervalSec: 100 } }]) test("Настройки: неверный receipt сохраняет dirty/" + JSON.stringify(raw), async ({ page }) => {
await setup(page); let writes = 0
await page.route("**/api/traffic/settings", r => { if (r.request().method() !== "PUT") return r.fallback(); writes++; return r.fulfill({ json: raw }) })
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99"); await page.getByRole("button", { name: "Сохранить настройки планировщика" }).click()
await expect(page.getByText("Операция не завершена", { exact: true })).toBeVisible(); await expect(field).toHaveValue("99")
await expect(page.getByRole("button", { name: "Сохранить настройки планировщика" })).toBeEnabled(); expect(writes).toBe(1)
await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toHaveCount(0)
})
test("Настройки: дробный ввод не вызывает PUT", async ({ page }) => {
await setup(page); let writes = 0; page.on("request", r => { if (r.method() === "PUT") writes++ })
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("30.5")
await page.getByRole("button", { name: "Сохранить настройки планировщика" }).click(); await expect(page.getByText("Трафик: укажите целое значение от 5", { exact: true })).toBeVisible(); expect(writes).toBe(0)
})
test("Настройки: переключатель отправляет только флаг, сохраняя dirty интервал", async ({ page }) => {
await setup(page); const writes: object[] = []
await page.route("**/api/traffic/settings", r => { if (r.request().method() !== "PUT") return r.fallback(); const body = r.request().postDataJSON(); writes.push(body); return r.fulfill({ json: { ok: true, settings: { ...values["/api/traffic/settings"], ...body } } }) })
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99")
await page.getByRole("switch", { name: "Включить: Трафик", exact: true }).click()
await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toBeVisible(); expect(writes).toEqual([{ enabled: false }]); await expect(field).toHaveValue("99")
await expect(page.getByRole("button", { name: "Сохранить настройки планировщика" })).toBeEnabled()
})
test("Настройки: частичный сбой очищает только подтверждённую часть", async ({ page }) => {
await setup(page); const writes: string[] = []
await page.route("**/api/traffic/settings", r => { if (r.request().method() !== "PUT") return r.fallback(); writes.push("traffic"); return r.fulfill({ json: { ok: true, settings: { ...values["/api/traffic/settings"], ...r.request().postDataJSON() } } }) })
await page.route("**/api/uptime/settings", r => { if (r.request().method() !== "PUT") return r.fallback(); writes.push("uptime"); return r.fulfill({ status: 503, json: { error: "uptime write unavailable" } }) })
await page.goto("/data-collection"); const traffic = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(traffic).toHaveValue("45"); await traffic.fill("99")
await page.getByRole("textbox", { name: "Хранение uptime", exact: true }).fill("21"); const save = page.getByRole("button", { name: "Сохранить настройки планировщика" }); await save.click()
await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toBeVisible(); await expect(page.getByText(/uptime write unavailable/)).toBeVisible(); await expect(save).toBeEnabled(); await save.click(); await expect.poll(() => writes.length).toBe(3); expect(writes).toEqual(["traffic", "uptime", "uptime"])
})
test("Настройки: lock блокирует второй запуск сохранения", async ({ page }) => {
await setup(page); let writes = 0, release!: () => void; const gate = new Promise<void>(resolve => release = resolve)
await page.route("**/api/traffic/settings", async r => { if (r.request().method() !== "PUT") return r.fallback(); writes++; await gate; await r.fulfill({ json: { ok: true, settings: { ...values["/api/traffic/settings"], ...r.request().postDataJSON() } } }) })
try {
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99")
const save = page.getByRole("button", { name: "Сохранить настройки планировщика" }); await save.click(); await expect.poll(() => writes).toBe(1); await expect(save).toBeDisabled(); await expect(field).toBeDisabled()
release(); await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toBeVisible(); expect(writes).toBe(1)
} finally { release() }
})
test("Настройки: поздний GET не отменяет подтверждённый переключатель", async ({ page }) => {
await setup(page); let delay = false, started = false, release!: () => void; const gate = new Promise<void>(resolve => release = resolve)
await page.route("**/api/traffic/settings", async r => {
if (r.request().method() === "PUT") return r.fulfill({ json: { ok: true, settings: { ...values["/api/traffic/settings"], ...r.request().postDataJSON() } } })
if (delay) { started = true; await gate }
try { await r.fulfill({ json: values["/api/traffic/settings"] }) } catch {}
})
try {
await page.goto("/data-collection"); const toggle = page.getByRole("switch", { name: "Включить: Трафик", exact: true }); await expect(toggle).toBeChecked(); delay = true
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled(); await page.getByRole("button", { name: "Обновить", exact: true }).click(); await expect.poll(() => started).toBe(true)
await toggle.click(); await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toBeVisible(); release(); await expect(toggle).not.toBeChecked()
} finally { release() }
})
test("Настройки: API 403 сохраняет черновик", async ({ page }) => {
await setup(page); await page.route("**/api/traffic/settings", r => r.request().method() === "PUT" ? r.fulfill({ status: 403, json: { error: "Нет доступа к настройкам трафика" } }) : r.fallback())
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99"); await page.getByRole("button", { name: "Сохранить настройки планировщика" }).click()
await expect(page.getByText("Нет доступа к настройкам трафика", { exact: false })).toBeVisible(); await expect(field).toHaveValue("99")
})
test("Настройки: read-only claims отключают запись", async ({ page }) => {
const claims = { sub: "fixture", email: "fixture@example.test", name: "Viewer", apps: ["mm"], permissions: ["mm:dashboard:read", "mm:traffic:read"], exp: Math.floor(Date.now()/1000)+3600 }
const token = "header." + Buffer.from(JSON.stringify(claims)).toString("base64url") + ".fixture"
await page.addInitScript(value => localStorage.setItem("mmapp_token", value), token)
await setup(page); await page.route("**/api/auth/config", r => r.fulfill({ json: { required: true, portalUrl: "http://portal.example.test" } }))
await page.goto("/data-collection"); await expect(page.getByTestId("settings-traffic")).toContainText("настройки получены")
await expect(page.getByRole("textbox", { name: "Интервал: Трафик", exact: true })).toBeDisabled(); await expect(page.getByRole("switch", { name: "Включить: Трафик", exact: true })).toBeDisabled()
})
test("Настройки: смена режима прекращает оставшуюся часть batch", async ({ page }) => {
await setup(page); let started = false, completed = false, uptimeWrites = 0, release!: () => void; const gate = new Promise<void>(resolve => release = resolve)
await page.route("**/api/traffic/settings", async r => { if (r.request().method() !== "PUT") return r.fallback(); started = true; await gate; try { await r.fulfill({ json: { ok: true, settings: { ...values["/api/traffic/settings"], ...r.request().postDataJSON() } } }) } catch {} finally { completed = true } })
await page.route("**/api/uptime/settings", r => { if (r.request().method() !== "PUT") return r.fallback(); uptimeWrites++; return r.fulfill({ json: {} }) })
try {
await page.goto("/data-collection"); const field = page.getByRole("textbox", { name: "Интервал: Трафик", exact: true }); await expect(field).toHaveValue("45"); await field.fill("99"); await page.getByRole("textbox", { name: "Хранение uptime", exact: true }).fill("21"); await page.getByRole("button", { name: "Сохранить настройки планировщика" }).click(); await expect.poll(() => started).toBe(true)
await page.goto("/settings"); await page.getByRole("button", { name: "Моковые", exact: true }).click(); await page.goto("/data-collection"); release(); await expect.poll(() => completed).toBe(true)
await expect(page.getByText("Нужен live-режим", { exact: true })).toBeVisible(); await expect(page.getByText("Сохранение подтверждено: Трафик", { exact: true })).toHaveCount(0); expect(uptimeWrites).toBe(0)
} finally { release() }
})