69 lines
3.2 KiB
TypeScript
69 lines
3.2 KiB
TypeScript
import { readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import { request, type APIRequestContext } from "@playwright/test";
|
||
|
||
export const BASE = process.env.E2E_BASE_URL ?? "http://localhost:5173";
|
||
export const FIXTURE_CBZ = fileURLToPath(new URL("../fixtures/e2e-sample.cbz", import.meta.url));
|
||
|
||
/** UI 登录与 API fixture 共用同一管理员凭据:E2E_ADMIN_* 优先,回落 deploy/.env 注入的 ADMIN_*。 */
|
||
export function creds(): { user: string; pass: string } {
|
||
const user = process.env.E2E_ADMIN_USER ?? process.env.ADMIN_USER;
|
||
const pass = process.env.E2E_ADMIN_PASSWORD ?? process.env.ADMIN_PASSWORD;
|
||
if (!user || !pass) {
|
||
throw new Error(
|
||
"缺少管理员凭据:export E2E_ADMIN_USER/E2E_ADMIN_PASSWORD," +
|
||
"或 `set -a; source ../deploy/.env; set +a` 后重跑",
|
||
);
|
||
}
|
||
return { user, pass };
|
||
}
|
||
|
||
export async function adminApi(): Promise<APIRequestContext> {
|
||
const { user, pass } = creds();
|
||
const res = await fetch(`${BASE}/api/auth/login`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ username: user, password: pass }),
|
||
});
|
||
if (!res.ok) throw new Error(`admin 登录失败:HTTP ${res.status}(dev 栈起了吗?凭据对吗?)`);
|
||
const { token } = (await res.json()) as { token: string };
|
||
return request.newContext({ baseURL: BASE, extraHTTPHeaders: { Authorization: `Bearer ${token}` } });
|
||
}
|
||
|
||
export async function ensureLibrary(ctx: APIRequestContext, name: string): Promise<number> {
|
||
const libs = (await (await ctx.get("/api/libraries")).json()) as { id: number; name: string }[];
|
||
const hit = libs.find((l) => l.name === name);
|
||
if (hit) return hit.id;
|
||
const created = (await (await ctx.post("/api/libraries", { data: { name } })).json()) as { id: number };
|
||
return created.id;
|
||
}
|
||
|
||
async function findBook(ctx: APIRequestContext, q: string): Promise<{ id: number; title: string } | null> {
|
||
const res = await ctx.get(`/api/books?q=${encodeURIComponent(q)}`);
|
||
const books = (await res.json()) as { id: number; title: string }[];
|
||
return books.find((b) => b.title.includes(q)) ?? null;
|
||
}
|
||
|
||
/** 幂等:书已存在直接返回 id;否则上传 fixture → 触发扫描 → 轮询等书出现(扫描是 fire-and-forget)。 */
|
||
export async function ensureSampleBook(ctx: APIRequestContext, libName: string): Promise<number> {
|
||
const title = "e2e-sample";
|
||
const existing = await findBook(ctx, title);
|
||
if (existing) return existing.id;
|
||
const libId = await ensureLibrary(ctx, libName);
|
||
const up = await ctx.post(`/api/libraries/${libId}/upload`, {
|
||
multipart: {
|
||
file: { name: "e2e-sample.cbz", mimeType: "application/zip", buffer: readFileSync(FIXTURE_CBZ) },
|
||
},
|
||
});
|
||
if (!up.ok()) throw new Error(`上传失败:HTTP ${up.status()} ${await up.text()}`);
|
||
const scan = await ctx.post(`/api/libraries/${libId}/scan`);
|
||
if (!scan.ok()) throw new Error(`触发扫描失败:HTTP ${scan.status()}`);
|
||
const deadline = Date.now() + 30_000;
|
||
for (;;) {
|
||
const hit = await findBook(ctx, title);
|
||
if (hit) return hit.id;
|
||
if (Date.now() > deadline) throw new Error(`扫描后 30s 内未找到书「${title}」`);
|
||
await new Promise((r) => setTimeout(r, 1000));
|
||
}
|
||
}
|