sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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<string, string> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Full-browser check (Playwright) for the same class of bug as
|
||||
* `src/components/InitiativeApplicationForm.draftTyping.integration.test.tsx`.
|
||||
*
|
||||
* Requires a **running** frontend (default E2E_BASE_URL) and an **authenticated** applicant
|
||||
* session with the « Đơn Đề nghị Công nhận » tab reachable (Báo cáo gate completed if applicable).
|
||||
*
|
||||
* Run (example):
|
||||
* cd fe0 && npm run dev
|
||||
* E2E_FORM_TYPING=1 npm run test:e2e -- e2e/initiative-application-form-typing.spec.ts
|
||||
*/
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Initiative Đơn form — keystroke regressions", () => {
|
||||
test.skip(
|
||||
process.env.E2E_FORM_TYPING !== "1",
|
||||
"Opt-in only: export E2E_FORM_TYPING=1 with a logged-in dev server.",
|
||||
);
|
||||
|
||||
test("browser typing does not surface React Maximum update depth in console", async ({ page }) => {
|
||||
const panics: string[] = [];
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() !== "error" && msg.type() !== "warning") return;
|
||||
const t = msg.text();
|
||||
if (/Maximum update depth exceeded|Too many re-renders/u.test(t)) panics.push(t);
|
||||
});
|
||||
|
||||
await page.goto("/dashboard", { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||
const appTab = page.getByRole("tab", { name: /Đơn Đề nghị Công nhận/u });
|
||||
await expect(appTab).toBeVisible({ timeout: 20_000 });
|
||||
await appTab.click();
|
||||
|
||||
const summary = page.locator("textarea.min-h-32").first();
|
||||
await expect(summary).toBeVisible({ timeout: 25_000 });
|
||||
await page.getByPlaceholder("Nhập tên sáng kiến...").focus();
|
||||
await page.keyboard.insertText("Đánh máy ");
|
||||
await summary.click();
|
||||
await page.keyboard.insertText("Mô tả từng ký tự ");
|
||||
await page.waitForTimeout(500);
|
||||
expect(panics).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user