fix(backups): разделить загрузку каталогов и права действий

This commit is contained in:
Denozordec committed 2026-10-01 15:06:52 +07:00
1 parent b460997cbf
commit 3dcc7c9ea6
11 files changed
+259 -91

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
import { test, expect } from "@playwright/test"
import { backup, setupBackups } from "./fixtures/backups"
const headers = { "access-control-allow-origin": "*", "access-control-allow-methods": "GET,POST,PUT,DELETE,OPTIONS", "access-control-allow-headers": "Content-Type,Authorization" }
for (const change of ["backend", "mode"]) for (const fails of [false, true]) test("Бэкапы: позднее чтение/" + change + "/" + fails, async ({ page }) => {
await setupBackups(page, headers)
let started = false, completed = false, release!: () => void; const gate = new Promise<void>(r => release = r)
await page.route("**/api/backups", async r => {
if (new URL(r.request().url()).port !== "8000") return r.fallback()
started = true; await gate
try { await r.fulfill(fails ? { headers, status: 503, json: { error: "СТАРАЯ ОШИБКА БЭКАПА" } } : { headers, json: [{ ...backup, filename: "СТАРЫЙ БЭКАП.rsc" }] }) } catch {} finally { completed = true }
})
await page.goto("/backups"); await expect.poll(() => started).toBe(true); await page.goto("/settings")
if (change === "backend") { const input = page.getByPlaceholder("http://localhost:8000"); await input.fill("http://localhost:8101"); await input.press("Enter") }
else await page.getByRole("button", { name: "Моковые", exact: true }).click()
await page.goto("/backups"); release(); await expect.poll(() => completed).toBe(true)
if (change === "backend") await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
else { await expect(page.getByText("Демонстрационные данные. Операции и сохранение недоступны.", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Новый бэкап", exact: true }).first()).toBeDisabled() }
await expect(page.getByText("СТАРЫЙ БЭКАП.rsc", { exact: true })).toHaveCount(0)
await expect(page.getByText(/СТАРАЯ ОШИБКА БЭКАПА/)).toHaveCount(0)
})
+61
View File
@@ -0,0 +1,61 @@
import { test, expect } from "@playwright/test"
import { backup, setupBackups } from "./fixtures/backups"
for (const invalid of ["API", "malformed", "duplicate"]) test("Бэкапы: первоначальный отказ " + invalid + " и повтор", async ({ page }) => {
await setupBackups(page); let fail = true
await page.route("**/api/backups", r => r.fulfill(fail ? invalid === "API" ? { status: 503, json: { error: "История отказала" } } : { json: invalid === "duplicate" ? [backup, backup] : {} } : { json: [backup] }))
await page.goto("/backups")
await expect(page.getByRole("alert", { name: "Данные резервных копий: backups" })).toContainText("Данные пока неизвестны")
await expect(page.getByText("Нет бэкапов", { exact: true })).toHaveCount(0)
fail = false; await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
})
test("Бэкапы: подтверждённая пустая история", async ({ page }) => {
await setupBackups(page); await page.route("**/api/backups", r => r.fulfill({ json: [] })); await page.goto("/backups")
await expect(page.getByText("Нет бэкапов", { exact: true })).toBeVisible()
})
for (const resource of ["servers", "backups/schedule", "backups/storage"]) test("Бэкапы: отказ зависимости " + resource + " сохраняет историю", async ({ page }) => {
await setupBackups(page); await page.route("**/api/" + resource, r => r.fulfill({ status: 503, json: { error: "Зависимость отказала" } }))
await page.goto("/backups"); await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
await expect(page.getByText("0 Б", { exact: true })).toBeVisible()
if (resource !== "backups/schedule") await expect(page.getByRole("button", { name: "Новый бэкап", exact: true })).toBeDisabled()
})
test("Бэкапы: поздняя зависимость не задерживает историю", async ({ page }) => {
await setupBackups(page); let release!: () => void; const gate = new Promise<void>(r => release = r)
await page.route("**/api/backups/storage", async r => { await gate; await r.fallback().catch(() => {}) })
await page.goto("/backups"); await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
release(); await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled()
})
test("Бэкапы: до ответа нет ложного empty, фоновая ошибка сохраняет строки", async ({ page }) => {
await setupBackups(page); let release!: () => void; const gate = new Promise<void>(r => release = r); let fail = false
await page.route("**/api/backups", async r => { await gate; await r.fulfill(fail ? { status: 503, json: { error: "Фоновый отказ" } } : { json: [backup] }).catch(() => {}) })
await page.goto("/backups"); await expect(page.getByText("Загрузка резервных копий…", { exact: true })).toBeVisible()
await expect(page.getByText("Нет бэкапов", { exact: true })).toHaveCount(0)
release(); await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
fail = true; await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByRole("alert", { name: "Данные резервных копий: backups" })).toContainText("Последние данные сохранены")
await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
})
for (const serverRead of [false, true]) test("Бэкапы: read-only и server read " + serverRead, async ({ page }) => {
await setupBackups(page)
await page.addInitScript(({ serverRead }) => localStorage.setItem("mmapp_token", "header." + btoa(JSON.stringify({ sub: "test", apps: ["mm"], permissions: ["mm:backups:read", ...(serverRead ? ["mm:servers:read"] : [])], exp: Math.floor(Date.now() / 1000) + 3600 })) + ".signature"), { serverRead })
await page.route("**/api/auth/config", r => r.fulfill({ json: { required: true } }))
let serverRequests = 0; await page.route("**/api/servers", r => { serverRequests++; return r.fallback() })
await page.goto("/backups"); await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Новый бэкап", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Удалить " + backup.filename, exact: true })).toBeDisabled()
await page.getByRole("tab", { name: "Настройки", exact: true }).click()
await expect(page.getByRole("button", { name: "Сохранить настройки", exact: true })).toBeDisabled()
if (!serverRead) expect(serverRequests).toBe(0)
})
test("Бэкапы: фоновое чтение сохраняет черновик хранилища", async ({ page }) => {
await setupBackups(page); await page.goto("/backups")
await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
await page.getByRole("tab", { name: "Настройки", exact: true }).click()
await page.getByRole("button", { name: "S3", exact: true }).click()
await page.getByPlaceholder("mikrotik-backups", { exact: true }).fill("draft-bucket")
await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled()
await expect(page.getByPlaceholder("mikrotik-backups", { exact: true })).toHaveValue("draft-bucket")
})
+13
View File
@@ -0,0 +1,13 @@
import type { Page } from "@playwright/test"
export { server } from "./users"
import { server } from "./users"
export const backup = { id: "backup-1", serverId: 1, serverName: "router", filename: "live-config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "local" }
export const schedule = { enabled: false, frequency: "daily", hour: 0, minute: 0, weekDay: 0, monthDay: 1, keepCount: 7, format: "rsc", serverIds: ["1"] }
export const storage = { provider: "local", s3Endpoint: "", s3Region: "", s3Bucket: "", s3Prefix: "", s3AccessKeyId: "", secretConfigured: false, s3ForcePathStyle: false, keepLocalCopy: false }
export async function setupBackups(page: Page, headers: Record<string, string> = {}) {
await page.route("**/health", r => r.fulfill({ headers, json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
return r.fulfill({ headers, json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [server] : path === "/api/backups" ? [backup] : path === "/api/backups/schedule" ? schedule : path === "/api/backups/storage" ? storage : {} })
})
}