import { test, expect } from "@playwright/test" import { setupBackups, backup } from "./fixtures/backups" import { openBackup, backupJob, backupAck } from "./fixtures/backup-job" test("Бэкапы job: single-flight POST и подтверждённый результат", async ({ page }) => { await setupBackups(page); let posts = 0, id = "", release!: () => void; const gate = new Promise(r => release = r) await page.route("**/api/backups/create", async r => { posts++; id = r.request().postDataJSON().requestId; await gate; await r.fulfill({ status: 202, json: backupAck(id) }).catch(() => {}) }) await page.route("**/api/backups/jobs/*", r => r.fulfill({ json: backupJob(id) })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).dblclick() await expect(dialog.getByRole("button", { name: "Проверить задачу", exact: true })).toBeDisabled() release(); await expect(page.getByText("API подтвердил создание всех резервных копий", { exact: true })).toBeVisible() expect(posts).toBe(1); await expect(dialog.getByRole("button", { name: "Новая задача", exact: true })).toBeVisible() }) test("Бэкапы job: отклонённый POST сохраняет форму для исправления", async ({ page }) => { await setupBackups(page); await page.route("**/api/backups/create", r => r.fulfill({ status: 403, json: { error: "Создание запрещено" } })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(dialog.getByRole("button", { name: /Снять бэкап/ })).toBeEnabled() await dialog.getByRole("button", { name: "Назад", exact: true }).click() await expect(dialog.getByRole("textbox", { name: "Заметка резервной копии", exact: true })).toHaveValue("manual") await expect(page.getByText("API подтвердил создание всех резервных копий", { exact: true })).toHaveCount(0) }) for (const kind of ["device", "S3"]) test("Бэкапы job: partial " + kind + " не называется полным успехом", async ({ page }) => { await setupBackups(page); let id = "" await page.route("**/api/backups/create", r => { id = r.request().postDataJSON().requestId; return r.fulfill({ status: 202, json: backupAck(id) }) }) await page.route("**/api/backups/jobs/*", r => r.fulfill({ json: backupJob(id, kind === "device" ? { created: [], failures: [{ serverId: "1", error: "RouterOS offline" }] } : { created: [{ ...backup, uploadError: "S3 upload denied" }] }) })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(page.getByRole("alert", { name: "Задача резервного копирования", exact: true })).toContainText(kind === "device" ? "RouterOS offline" : "S3 upload denied") await expect(page.getByText("API подтвердил создание всех резервных копий", { exact: true })).toHaveCount(0) await expect(dialog.getByRole("button", { name: "Новая задача", exact: true })).toBeVisible() }) test("Бэкапы job: отказ GET и повтор только чтения той же задачи", async ({ page }) => { await setupBackups(page); let posts = 0, id = "", fail = true await page.route("**/api/backups/create", r => { posts++; id = r.request().postDataJSON().requestId; return r.fulfill({ status: 202, json: backupAck(id) }) }) await page.route("**/api/backups/jobs/*", r => r.fulfill(fail ? { status: 503, json: { error: "Наблюдение отказало" } } : { json: backupJob(id) })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(dialog.getByRole("button", { name: "Проверить задачу", exact: true })).toBeEnabled() await expect(page.getByRole("alert", { name: "Задача резервного копирования", exact: true })).toContainText("Наблюдение отказало") fail = false; await dialog.getByRole("button", { name: "Проверить задачу", exact: true }).click() await expect(page.getByText("API подтвердил создание всех резервных копий", { exact: true })).toBeVisible(); expect(posts).toBe(1) }) test("Бэкапы job: потерянный POST/404 после reload не запускает второй POST", async ({ page }) => { await setupBackups(page); let posts = 0 await page.route("**/api/backups/create", r => { posts++; return r.fulfill({ status: 503, json: { error: "Неизвестный результат POST" } }) }) await page.route("**/api/backups/jobs/*", r => r.fulfill({ status: 404, json: { error: "Job lost" } })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(dialog.getByRole("button", { name: "Проверить задачу", exact: true })).toBeEnabled() await page.reload(); await page.getByRole("button", { name: "Задача резервного копирования", exact: true }).click() await page.getByRole("dialog").getByRole("button", { name: "Проверить задачу", exact: true }).click() await expect(page.getByRole("alert", { name: "Задача резервного копирования", exact: true })).toContainText("После рестарта") expect(posts).toBe(1); await expect(page.getByRole("dialog").getByRole("button", { name: "Новая задача", exact: true })).toHaveCount(0) }) test("Бэкапы job: чужая цель не подтверждает завершение", async ({ page }) => { await setupBackups(page); let id = "" await page.route("**/api/backups/create", r => { id = r.request().postDataJSON().requestId; return r.fulfill({ status: 202, json: backupAck(id) }) }) await page.route("**/api/backups/jobs/*", r => r.fulfill({ json: backupJob(id, { notes: "foreign" }) })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(page.getByRole("alert", { name: "Задача резервного копирования", exact: true })).toContainText("не соответствует") await expect(dialog.getByRole("button", { name: "Новая задача", exact: true })).toHaveCount(0) }) test("Бэкапы job: неизвестный POST восстанавливается GET без второго создания", async ({ page }) => { await setupBackups(page); let posts = 0, id = "" await page.route("**/api/backups/create", r => { posts++; id = r.request().postDataJSON().requestId; return r.fulfill({ status: 503, json: { error: "Ack lost" } }) }) await page.route("**/api/backups/jobs/*", r => r.fulfill({ json: backupJob(id) })) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(dialog.getByRole("button", { name: "Проверить задачу", exact: true })).toBeEnabled() await dialog.getByRole("button", { name: "Проверить задачу", exact: true }).click() await expect(page.getByText("API подтвердил создание всех резервных копий", { exact: true })).toBeVisible(); expect(posts).toBe(1) }) test("Бэкапы job: невозможность checkpoint записи не отправляет POST", async ({ page }) => { await setupBackups(page); let posts = 0 await page.addInitScript(() => { const set = Storage.prototype.setItem; Storage.prototype.setItem = function(key, value) { if (key.startsWith("mmapp:backup-job:")) throw new Error("denied"); return set.call(this, key, value) } }) await page.route("**/api/backups/create", r => { posts++; return r.fallback() }) const dialog = await openBackup(page); await dialog.getByRole("button", { name: /Снять бэкап/ }).click() await expect(page.getByRole("alert", { name: "Задача резервного копирования", exact: true })).toContainText("Запрос не отправлен") expect(posts).toBe(0) })