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

This commit is contained in:
Denozordec committed 2026-10-14 14:30:33 +07:00
1 parent e452e528e1
commit 6617b776bb
26 files changed
+420 -36

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import { test, expect } from "@playwright/test"
import { backup, setupBackups } from "./fixtures/backups"
for (const width of [1440, 390]) test(`Бэкапы: отсутствующий локальный файл ${width}`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 }); await setupBackups(page)
await page.route("**/api/backups", r => r.fulfill({ json: [{ ...backup, localFileStatus: "missing" }] }))
let operations = 0; page.on("request", r => { if (/\/backups\/[^/]+\/(restore|download)$/.test(new URL(r.url()).pathname)) operations++ })
await page.goto("/backups")
await expect(page.getByText("Нет файла", { exact: true })).toBeVisible()
await expect(page.getByText("Локальный файл отсутствует.", { exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Скачать " + backup.filename, exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true })).toBeDisabled()
await page.getByRole("button", { name: "Удалить " + backup.filename, exact: true }).click()
const dialog = page.getByRole("alertdialog"); await expect(dialog).toContainText("Удалить запись о бэкапе?"); await expect(dialog).toContainText("Будет удалена только запись из истории.")
await dialog.getByRole("button", { name: "Отмена", exact: true }).click()
expect(operations).toBe(0); expect(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1)).toBe(false)
})
test("Бэкапы: потеря local copy сохраняет скачивание из S3", async ({ page }) => {
await setupBackups(page); await page.route("**/api/backups", r => r.fulfill({ json: [{ ...backup, storage: "both", localFileStatus: "missing" }] }))
await page.route("**/api/backups/backup-1/download", r => r.fulfill({ contentType: "text/plain; charset=utf-8", body: "# S3 fixture\n" }))
await page.goto("/backups"); await expect(page.getByText("Локальная копия отсутствует. Файл будет запрошен из S3.", { exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true })).toBeEnabled()
const download = page.waitForEvent("download"); await page.getByRole("button", { name: "Скачать " + backup.filename, exact: true }).click(); expect((await download).suggestedFilename()).toBe(backup.filename)
})
for (const status of ["unknown", undefined]) test(`Бэкапы: неизвестный или старый receipt ${status}`, async ({ page }) => {
await setupBackups(page); await page.route("**/api/backups", r => r.fulfill({ json: [{ ...backup, ...(status ? { localFileStatus: status } : {}) }] }))
await page.goto("/backups"); await expect(page.getByText(backup.filename, { exact: true })).toBeVisible()
await expect(page.getByText("Нет файла", { exact: true })).toHaveCount(0)
await expect(page.getByRole("button", { name: "Скачать " + backup.filename, exact: true })).toBeEnabled()
if (status) await expect(page.getByText("Не удалось проверить локальный файл.", { exact: true })).toBeVisible()
})
test("Бэкапы: исчезновение после первого чтения подтверждается новым receipt", async ({ page }) => {
await setupBackups(page); let missing = false
await page.route("**/api/backups", r => r.fulfill({ json: [{ ...backup, localFileStatus: missing ? "missing" : "available" }] }))
await page.goto("/backups"); const restore = page.getByRole("button", { name: "Восстановить " + backup.filename, exact: true }); await expect(restore).toBeEnabled()
missing = true; await page.getByRole("button", { name: "Обновить", exact: true }).click(); await expect(restore).toBeDisabled()
await expect(page.getByText("Нет файла", { exact: true })).toBeVisible()
})
+13
View File
@@ -0,0 +1,13 @@
import { test, expect } from "@playwright/test"
import { backup, setupBackups } from "./fixtures/backups"
for (const confirmed of [true, false]) test(`Бэкапы: удаление отсутствующего файла требует record-only receipt ${confirmed}`, async ({ page }) => {
await setupBackups(page); await page.route("**/api/backups", r => r.fulfill({ json: [{ ...backup, localFileStatus: "missing" }] }))
let calls = 0
await page.route("**/api/backups/backup-1?recordOnly=true", r => { calls++; return r.fulfill({ status: 204, headers: confirmed ? { "X-Backup-Delete-Mode": "record-only" } : {} }) })
await page.goto("/backups"); await page.getByRole("button", { name: "Удалить " + backup.filename, exact: true }).click()
const dialog = page.getByRole("alertdialog"); await dialog.getByRole("button", { name: "Удалить", exact: true }).click()
if (confirmed) await expect(dialog).toBeHidden()
else { await expect(dialog).toBeVisible(); await expect(dialog).toContainText("Не подтверждено удаление только записи истории") }
expect(calls).toBe(1)
})