/** * 素材库流程 E2E 测试 * * 覆盖:创建素材库、列出素材库、创建素材记录 * 每个测试独立,先注册登录获取 auth token。 */ import { expect, test, type APIRequestContext } from "@playwright/test" const PASSWORD = "Test123456!" const apiBase = process.env.E2E_API_BASE || "/api/v1" function uniqueEmail(prefix: string): string { return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}@example.com` } function uniqueUsername(prefix: string): string { return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}` } /** 登录操作,遇到 429 限流自动等待重试 */ async function loginWithRetry( request: APIRequestContext, email: string, password: string, maxRetries = 2, ) { for (let i = 0; i <= maxRetries; i++) { const response = await request.post(`${apiBase}/auth/login`, { data: { email, password }, }) if (response.status() !== 429) return response console.log(`[login] 触发限流,等待 65s 后重试 (${i + 1}/${maxRetries})`) await new Promise((r) => setTimeout(r, 65000)) } return request.post(`${apiBase}/auth/login`, { data: { email, password }, }) } /** 注册并登录,返回 { headers, email, username, userId } */ async function createAuthedUser(request: APIRequestContext, label: string) { const email = uniqueEmail(label) const username = uniqueUsername(label) const reg = await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: `E2E ${label}` }, }) expect(reg.ok(), `注册应成功: ${await reg.text()}`).toBeTruthy() const regData = await reg.json() const login = await loginWithRetry(request, email, PASSWORD) expect(login.ok(), `登录应成功: ${await login.text()}`).toBeTruthy() const loginData = await login.json() return { headers: { Authorization: `Bearer ${loginData.access_token}` }, email, username, userId: regData.user_id, } } /** 创建一个项目并返回 project id */ async function createProject( request: APIRequestContext, headers: Record, suffix: string, ): Promise { const resp = await request.post(`${apiBase}/projects`, { headers, data: { name: `Asset Test Proj ${suffix}`, description: "E2E asset test" }, }) expect(resp.ok(), `创建项目应成功: ${await resp.text()}`).toBeTruthy() const data = await resp.json() return data.id } test.describe("素材库流程", () => { // 登录限流 10次/60s,测试可能触发限流等待,给足够超时 test.describe.configure({ timeout: 180_000 }) test("创建素材库", async ({ request }) => { const { headers } = await createAuthedUser(request, "lib-create") const projectId = await createProject(request, headers, Date.now().toString()) const response = await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectId, name: `视频素材库 ${Date.now()}`, kind: "video", }, }) expect( response.ok(), `创建素材库应返回 2xx,实际: ${response.status()} ${await response.text()}`, ).toBeTruthy() const data = await response.json() expect(data.id, "应返回素材库 ID").toBeTruthy() expect(data.name).toContain("视频素材库") expect(data.kind).toBe("video") expect(data.project_id).toBe(projectId) }) test("创建素材库 - 无效 kind 反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "lib-badkind") const projectId = await createProject(request, headers, Date.now().toString()) const response = await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectId, name: "Bad Kind Library", kind: "invalid_kind", }, }) // kind 有 pattern 校验 ^(video|voice|image)$,应返回 422 expect([400, 422]).toContain(response.status()) }) test("创建素材库 - 不存在的项目反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "lib-nopj") const response = await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: "nonexistent-project-999", name: "Orphan Library", kind: "video", }, }) expect(response.status(), "不存在的项目应返回 404").toBe(404) }) test("列出素材库", async ({ request }) => { const { headers } = await createAuthedUser(request, "lib-list") const projectId = await createProject(request, headers, Date.now().toString()) // 创建 2 个不同类型的素材库 await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectId, name: `Video Lib ${Date.now()}`, kind: "video", }, }) await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectId, name: `Image Lib ${Date.now()}`, kind: "image", }, }) // 列出(按 project_id 过滤) const response = await request.get(`${apiBase}/asset-libraries`, { headers, params: { project_id: projectId }, }) expect( response.ok(), `列出素材库应返回 2xx,实际: ${response.status()} ${await response.text()}`, ).toBeTruthy() const data = await response.json() const items = data.items || [] expect(items.length, "应至少有 2 个素材库").toBeGreaterThanOrEqual(2) const kinds = items.map((i: { kind: string }) => i.kind) expect(kinds).toContain("video") expect(kinds).toContain("image") }) test("创建素材记录", async ({ request }) => { const { headers, userId } = await createAuthedUser(request, "asset-create") const projectId = await createProject(request, headers, Date.now().toString()) // 创建素材库 const lib = await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectId, name: `Asset Lib ${Date.now()}`, kind: "video", }, }) expect(lib.ok()).toBeTruthy() const libData = await lib.json() // 创建素材记录 const response = await request.post(`${apiBase}/assets`, { headers, data: { project_id: projectId, library_id: libData.id, name: `test_video_${Date.now()}.mp4`, storage_key: `uploads/e2e/test_${Date.now()}.mp4`, mime_type: "video/mp4", metadata: { duration: 15.5, resolution: "1080p" }, file_size: 1024000, status: "ready", uploaded_by_user_id: userId, }, }) expect( response.ok(), `创建素材应返回 2xx,实际: ${response.status()} ${await response.text()}`, ).toBeTruthy() const data = await response.json() expect(data.id, "应返回素材 ID").toBeTruthy() expect(data.name).toContain("test_video") expect(data.mime_type).toBe("video/mp4") expect(data.library_id).toBe(libData.id) }) test("列出素材", async ({ request }) => { const { headers, userId } = await createAuthedUser(request, "asset-list") const projectId = await createProject(request, headers, Date.now().toString()) // 创建素材库 const lib = await request.post(`${apiBase}/asset-libraries`, { headers, data: { project_id: projectId, name: `List Lib ${Date.now()}`, kind: "video", }, }) expect(lib.ok(), `创建素材库应成功: ${await lib.text()}`).toBeTruthy() const libData = await lib.json() // 创建 2 个素材 await request.post(`${apiBase}/assets`, { headers, data: { project_id: projectId, library_id: libData.id, name: `clip_a_${Date.now()}.mp4`, storage_key: `uploads/e2e/clip_a.mp4`, mime_type: "video/mp4", status: "ready", uploaded_by_user_id: userId, }, }) await request.post(`${apiBase}/assets`, { headers, data: { project_id: projectId, library_id: libData.id, name: `clip_b_${Date.now()}.mp4`, storage_key: `uploads/e2e/clip_b.mp4`, mime_type: "video/mp4", status: "ready", uploaded_by_user_id: userId, }, }) // 列出素材 const response = await request.get(`${apiBase}/assets`, { headers, params: { library_id: libData.id }, }) expect( response.ok(), `列出素材应返回 2xx,实际: ${response.status()} ${await response.text()}`, ).toBeTruthy() const data = await response.json() const items = data.items || [] expect(items.length, "应至少有 2 个素材").toBeGreaterThanOrEqual(2) }) test("未登录创建素材库 - 反向", async ({ request }) => { const response = await request.post(`${apiBase}/asset-libraries`, { data: { project_id: "some-project", name: "Unauthorized Library", kind: "video", }, }) expect([401, 403]).toContain(response.status()) }) })