fix(collectors): сохранять только изменённые настройки с учётом прав

This commit is contained in:
Denozordec committed 2026-10-03 11:50:23 +07:00
1 parent 117aa9a21a
commit cb377fb60c
6 files changed
+193 -208

No files matched your search

@@ -57,3 +57,77 @@ test("Настройки: успешное обновление не затир
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: "[email protected]", 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()
})