/** * 订阅管理 E2E 测试 * * 覆盖:路由守卫、订阅降级、过期处理、订阅状态检查 */ import { expect, test } 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)}`; } /** 注册并登录,返回 { headers, email, username, userId } */ async function createAuthedUser(request: any, 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 request.post(`${apiBase}/auth/login`, { data: { email, password: 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('redirects anonymous users to login', async ({ page }) => { await page.goto('/subscription'); await expect(page).toHaveURL(/\/login/); }); }); test.describe('订阅信息查看', () => { 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('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('取消订阅 - 反向(免费用户)', 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.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('获取账单记录 - 正向', 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()); }); });