/** * 订阅管理 E2E 测试 * * 覆盖:路由守卫、订阅降级、过期处理、订阅状态检查 */ 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 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, } } test.describe("Subscription route guard", () => { test.describe.configure({ timeout: 120_000 }) test("redirects anonymous users to login", async ({ page }) => { await page.goto("/app/subscription") await expect(page).toHaveURL(/\/login/) }) }) test.describe("订阅信息查看", () => { test.describe.configure({ timeout: 120_000 }) test("获取当前订阅信息 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-info") const response = await request.get(`${apiBase}/subscription/current`, { headers, }) expect( response.ok(), `获取订阅信息应返回 2xx,实际: ${response.status()} ${await response.text()}`, ).toBeTruthy() const data = await response.json() expect(data.plan_id, "应返回 plan_id").toBeTruthy() expect(data.status, "应返回 status").toBeTruthy() }) test("未登录获取订阅信息 - 反向", async ({ request }) => { const response = await request.get(`${apiBase}/subscription/current`) expect([401, 403]).toContain(response.status()) }) }) test.describe("订阅降级", () => { test.describe.configure({ timeout: 120_000 }) test("Pro 用户降级到 Standard - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-downgrade") // 先升级到 Pro const upgrade = await request.post(`${apiBase}/subscription/change-plan`, { headers, data: { target_plan_id: "pro", billing_cycle: "monthly", }, }) expect(upgrade.ok(), `升级到 Pro 应成功: ${await upgrade.text()}`).toBeTruthy() // 降级到 Standard const downgrade = await request.post(`${apiBase}/subscription/change-plan`, { headers, data: { target_plan_id: "standard", billing_cycle: "monthly", }, }) // 降级应成功或返回提示信息(某些业务可能限制降级) expect(downgrade.status(), "降级请求应返回 2xx 或 4xx").toBeLessThan(500) const data = await downgrade.json() // 成功或失败都应有明确响应 expect(data).toBeTruthy() }) test("降级到相同套餐 - 反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-same") // 用户默认为 free,再次选择 free const response = await request.post(`${apiBase}/subscription/change-plan`, { headers, data: { target_plan_id: "free", billing_cycle: "monthly", }, }) // 相同套餐应返回 200 + success=false,或者 400 if (response.ok()) { const data = await response.json() expect(data.success).toBe(false) } else { expect([400, 422]).toContain(response.status()) } }) test("降级到无效套餐 - 反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-badplan") const response = await request.post(`${apiBase}/subscription/change-plan`, { headers, data: { target_plan_id: "nonexistent_plan", billing_cycle: "monthly", }, }) expect(response.status(), "无效套餐应返回 4xx").toBeGreaterThanOrEqual(400) expect(response.status()).toBeLessThan(500) }) }) test.describe("订阅过期处理", () => { test.describe.configure({ timeout: 120_000 }) test("取消订阅 - 反向(免费用户)", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-cancel") // 免费用户取消订阅应返回错误 const response = await request.post(`${apiBase}/subscription/cancel`, { headers, }) // 免费用户可能不需要取消,返回 400 或类似错误 if (!response.ok()) { const data = await response.json() expect(data.error?.message || data.detail || data.message, "应返回错误信息").toBeTruthy() } }) test("未登录取消订阅 - 反向", async ({ request }) => { const response = await request.post(`${apiBase}/subscription/cancel`) expect([401, 403]).toContain(response.status()) }) test("切换自动续费 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-autorenew") // 关闭自动续费 const disableResp = await request.post(`${apiBase}/subscription/toggle-auto-renew`, { headers, data: { enabled: false }, }) expect(disableResp.ok(), `关闭自动续费应成功: ${await disableResp.text()}`).toBeTruthy() // 重新开启自动续费 const enableResp = await request.post(`${apiBase}/subscription/toggle-auto-renew`, { headers, data: { enabled: true }, }) expect(enableResp.ok(), `开启自动续费应成功: ${await enableResp.text()}`).toBeTruthy() }) test("无效参数切换自动续费 - 反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-autoren-bad") // 缺少 enabled 字段 const response = await request.post(`${apiBase}/subscription/toggle-auto-renew`, { headers, data: {}, }) expect([400, 422]).toContain(response.status()) }) }) test.describe("账单记录", () => { test.describe.configure({ timeout: 120_000 }) test("获取账单记录 - 正向", async ({ request }) => { const { headers } = await createAuthedUser(request, "sub-bills") const response = await request.get(`${apiBase}/subscription/billing-records`, { headers }) expect(response.ok(), `获取账单记录应返回 2xx,实际: ${response.status()}`).toBeTruthy() const data = await response.json() expect(Array.isArray(data), "账单记录应为数组").toBeTruthy() }) test("未登录获取账单记录 - 反向", async ({ request }) => { const response = await request.get(`${apiBase}/subscription/billing-records`) expect([401, 403]).toContain(response.status()) }) })