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 = { "/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(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(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(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(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() } })