138 lines
5.5 KiB
TypeScript
138 lines
5.5 KiB
TypeScript
import { mkdirSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
import { createServer, type ViteDevServer } from "vite";
|
|
|
|
import { adminOverviewFixture } from "../fixtures/wp6-01-admin-overview.js";
|
|
|
|
let vite: ViteDevServer;
|
|
let webUrl: string;
|
|
|
|
const adminSession = {
|
|
acknowledged_private_content_notice_version: null,
|
|
admin: { role: "super_admin", status: "active", user_id: "00000000-0000-4000-8000-000000000601" },
|
|
audience: "admin",
|
|
authenticated: true,
|
|
csrf_token: "csrf-admin-shell-fixture-000000000000000000000000000000000",
|
|
current_private_content_notice_version: null,
|
|
expires_at: "2026-09-02T09:30:00.000Z",
|
|
notice_acknowledged: false,
|
|
};
|
|
|
|
const navigation = [
|
|
["总览", "/admin"],
|
|
["用户与点数", "/admin/users"],
|
|
["邀请码", "/admin/invites"],
|
|
["模型", "/admin/models"],
|
|
["素材", "/admin/assets"],
|
|
["内部预览", "/admin/preview"],
|
|
["生成记录", "/admin/generations"],
|
|
["服务与存储", "/admin/services-storage"],
|
|
["审计", "/admin/audit"],
|
|
] as const;
|
|
|
|
test.beforeAll(async () => {
|
|
vite = await createServer({
|
|
configFile: resolve("apps/web/vite.config.ts"),
|
|
root: resolve("apps/web"),
|
|
server: { host: "127.0.0.1", port: 0 },
|
|
});
|
|
await vite.listen();
|
|
const address = vite.httpServer?.address();
|
|
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
|
|
webUrl = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
test.afterAll(async () => vite.close());
|
|
|
|
async function routeActiveAdmin(page: Page) {
|
|
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({
|
|
body: JSON.stringify(adminSession),
|
|
contentType: "application/json",
|
|
status: 200,
|
|
}));
|
|
await page.route("**/api/v1/admin/overview", (route) => route.fulfill({
|
|
body: JSON.stringify(adminOverviewFixture),
|
|
contentType: "application/json",
|
|
status: 200,
|
|
}));
|
|
}
|
|
|
|
test("TDD-WP6-ADM-001-role-and-summary renders the protected nine-entry admin shell", async ({ page }) => {
|
|
const requests: string[] = [];
|
|
page.on("request", (request) => requests.push(request.url()));
|
|
await routeActiveAdmin(page);
|
|
await page.goto(`${webUrl}/admin`);
|
|
|
|
await expect(page.getByRole("heading", { level: 2, name: "运营总览" })).toBeVisible();
|
|
const sidebar = page.getByRole("navigation", { name: "后台主导航" });
|
|
await expect(sidebar).toBeVisible();
|
|
for (const [name, href] of navigation) {
|
|
await expect(sidebar.getByRole("link", { name, exact: true })).toHaveAttribute("href", href);
|
|
}
|
|
expect(Math.round((await sidebar.boundingBox())?.width ?? 0)).toBe(216);
|
|
await expect(page.getByText("4 / 10", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("待人工核对 1", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("85.0%", { exact: true })).toBeVisible();
|
|
await expect(page.getByRole("link", { name: "有异常", exact: true })).toBeVisible();
|
|
await expect(page.locator("body")).not.toContainText(/forbidden|example\.invalid|api[_ -]?key|完整提示词/i);
|
|
expect(requests.some((url) => /prompt|private-content|image-content/i.test(url))).toBe(false);
|
|
|
|
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_ADMIN;
|
|
if (evidenceRoot) {
|
|
const screenshotDirectory = resolve(evidenceRoot, "screenshots");
|
|
mkdirSync(screenshotDirectory, { recursive: true });
|
|
await page.screenshot({ fullPage: true, path: resolve(screenshotDirectory, "admin-overview.png") });
|
|
}
|
|
});
|
|
|
|
test("TDD-WP6-ADM-001-role-and-summary keeps ordinary and preview sessions outside admin", async ({ page }) => {
|
|
let overviewCalls = 0;
|
|
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({
|
|
body: JSON.stringify({ error: { code: "AUTH_SESSION_INVALID", subject: "preview_user" } }),
|
|
contentType: "application/json",
|
|
status: 401,
|
|
}));
|
|
await page.route("**/api/v1/admin/overview", (route) => {
|
|
overviewCalls += 1;
|
|
return route.fulfill({ body: "null", contentType: "application/json", status: 401 });
|
|
});
|
|
|
|
await page.goto(`${webUrl}/admin`);
|
|
await expect(page).toHaveURL(`${webUrl}/admin/login`);
|
|
await expect(page.getByRole("heading", { name: "管理员邮箱验证码登录" })).toBeVisible();
|
|
expect(overviewCalls).toBe(0);
|
|
await expect(page.getByText("DADA ADMIN", { exact: true })).toHaveCount(0);
|
|
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_ADMIN;
|
|
if (evidenceRoot) {
|
|
const screenshotDirectory = resolve(evidenceRoot, "screenshots");
|
|
mkdirSync(screenshotDirectory, { recursive: true });
|
|
await page.screenshot({ fullPage: true, path: resolve(screenshotDirectory, "admin-denied.png") });
|
|
}
|
|
});
|
|
|
|
test("TDD-WP6-ADM-001-role-and-summary ejects a disabled admin when the session is rechecked", async ({ page }) => {
|
|
let active = true;
|
|
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill(active ? {
|
|
body: JSON.stringify(adminSession),
|
|
contentType: "application/json",
|
|
status: 200,
|
|
} : {
|
|
body: JSON.stringify({ error: { code: "AUTH_SESSION_INVALID" } }),
|
|
contentType: "application/json",
|
|
status: 401,
|
|
}));
|
|
await page.route("**/api/v1/admin/overview", (route) => route.fulfill({
|
|
body: JSON.stringify(adminOverviewFixture),
|
|
contentType: "application/json",
|
|
status: 200,
|
|
}));
|
|
await page.goto(`${webUrl}/admin`);
|
|
await expect(page.getByRole("heading", { level: 2, name: "运营总览" })).toBeVisible();
|
|
|
|
active = false;
|
|
await page.evaluate(() => window.dispatchEvent(new Event("dada:session-invalid")));
|
|
await expect(page).toHaveURL(`${webUrl}/admin/login`);
|
|
});
|