/** * Full-stack backup smoke: API seeds submit (Postgres + MinIO), browser downloads ZIP as admin. * * Prerequisites (same as be0/tests/test_backup_e2e.py): * - docker compose: postgres, minio, be0, fe0 (or host services on expected ports) * - DB migrated through 009; MinIO buckets exist * - DB migration 013 (email verification): Playwright cannot capture verify tokens; either * use API/integration path for full coverage or extend this spec with a token source. * * be0 must be started with AUTH_ADMIN_EMAILS containing the same address as E2E_ADMIN_EMAIL. * * Run: * export E2E_BACKUP=1 * export E2E_BASE_URL=http://localhost:8081 * export E2E_ADMIN_EMAIL=e2e-backup-admin@ump.edu.vn * # In docker-compose be0 environment: AUTH_ADMIN_EMAILS=e2e-backup-admin@ump.edu.vn * cd fe0 && npm install && npx playwright install chromium * npm run test:e2e -- e2e/backup-admin-download.spec.ts */ import { test, expect } from "@playwright/test"; import { randomUUID } from "node:crypto"; const PASSWORD = "Testpass1!"; const MIN_PDF = Buffer.concat([ Buffer.from("%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n"), Buffer.alloc(120, 0x30), ]); function registerStaffFields(): Record { const suffix = randomUUID().replace(/-/g, "").slice(0, 8).toUpperCase(); return { employeeId: `CB-${suffix}`, academicTitleCode: "master", unitNameFreetext: "Khoa kiểm thử", jobTitle: "Cán bộ", }; } const stackEnabled = process.env.E2E_BACKUP === "1"; const adminEmail = process.env.E2E_ADMIN_EMAIL?.trim() ?? ""; test.describe("Admin backup ZIP (frontend → API → DB → MinIO)", () => { test("admin can download backup after applicant submits PDF", async ({ page, request }) => { test.skip(!stackEnabled, "Set E2E_BACKUP=1"); test.skip(!adminEmail, "Set E2E_ADMIN_EMAIL and list it in be0 AUTH_ADMIN_EMAILS"); const applicantEmail = `e2e-fe-app-${randomUUID().slice(0, 10)}@ump.edu.vn`; const registerJson = (body: object) => request.post("/api/v1/auth/register", { headers: { "Content-Type": "application/json" }, data: JSON.stringify(body), }); const loginJson = (email: string) => request.post("/api/v1/auth/login", { headers: { "Content-Type": "application/json" }, data: JSON.stringify({ email, password: PASSWORD }), }); let r = await registerJson({ fullName: "E2E Applicant", email: applicantEmail, password: PASSWORD, passwordConfirm: PASSWORD, ...registerStaffFields(), }); expect(r.ok(), await r.text()).toBeTruthy(); r = await loginJson(applicantEmail); expect(r.ok(), await r.text()).toBeTruthy(); const applicantToken = (await r.json()) as { accessToken: string }; expect(applicantToken.accessToken).toBeTruthy(); r = await request.post("/api/applications/new", { headers: { Authorization: `Bearer ${applicantToken.accessToken}`, "Content-Type": "application/json", }, data: JSON.stringify({ name: "E2E FE backup" }), }); expect(r.ok(), await r.text()).toBeTruthy(); const created = (await r.json()) as { id?: string; application?: { draft_case_id?: string; id?: string } }; const caseId = String(created.application?.draft_case_id ?? "").trim(); let applicationId = String(created.id ?? created.application?.id ?? "").trim(); expect(caseId, JSON.stringify(created)).toBeTruthy(); expect(applicationId).toBeTruthy(); const meta = { initiativeCaseId: caseId, initiativeName: "E2E FE Backup", authorName: "Applicant", authorEmail: applicantEmail, subjectId: "s1", groupId: "g1", topicType: "Hồ sơ PDF", }; r = await request.post("/api/applications/submit", { headers: { Authorization: `Bearer ${applicantToken.accessToken}` }, multipart: { file: { name: "e2e.pdf", mimeType: "application/pdf", buffer: MIN_PDF, }, metadata: JSON.stringify(meta), }, }); expect(r.ok(), await r.text()).toBeTruthy(); const submitted = (await r.json()) as { id?: string }; applicationId = String(submitted.id ?? applicationId); r = await registerJson({ fullName: "E2E Admin", email: adminEmail, password: PASSWORD, passwordConfirm: PASSWORD, ...registerStaffFields(), }); if (r.ok()) { const regBody = (await r.json()) as { user?: { roles?: string[] } }; expect(regBody.user?.roles).toContain("admin"); } else { expect([400, 409, 422]).toContain(r.status()); } r = await loginJson(adminEmail); expect(r.ok(), await r.text()).toBeTruthy(); await page.goto("/login"); await page.locator("#login-email").fill(adminEmail); await page.locator("#login-password").fill(PASSWORD); await page.getByRole("button", { name: "Đăng nhập" }).click(); await page.waitForURL(/\/dashboard/u, { timeout: 30_000 }); await page.goto(`/dashboard/admin/applications/review?applicationId=${encodeURIComponent(applicationId)}`); await expect(page.getByRole("heading", { name: /Xem hồ sơ đã nộp \(quản trị\)/u })).toBeVisible({ timeout: 30_000, }); const backupBtn = page.getByRole("button", { name: /Tải bản sao lưu/u }); await expect(backupBtn).toBeVisible(); const [download] = await Promise.all([page.waitForEvent("download"), backupBtn.click()]); expect(download.suggestedFilename()).toMatch(/\.zip$/i); const path = await download.path(); expect(path).toBeTruthy(); const fs = await import("node:fs/promises"); const buf = await fs.readFile(path!); expect(buf.length).toBeGreaterThan(100); expect(buf[0]).toBe(0x50); expect(buf[1]).toBe(0x4b); }); });