import { test, expect } from '@playwright/test'; let counter = 500; function shortUid() { counter++; return `${counter}${Math.random().toString(36).slice(2, 7)}`; } const TEST_PASS = 'testpass123'; const BASE = 'http://localhost:8000'; /** Register user via API, return { username, tokens, user_id } */ async function registerUser(prefix = 'ex') { const uid = shortUid(); const username = `${prefix}${uid}`; const email = `${prefix}${uid}@t.co`; const resp = await fetch(`${BASE}/api/v1/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, password: TEST_PASS }), }); expect(resp.ok).toBeTruthy(); const body = await resp.json(); return { username, email, tokens: body.tokens, user_id: body.user.id }; } /** Login as pre-existing admin (username: admin, password: admin123) */ async function loginAdmin() { const resp = await fetch(`${BASE}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'admin123' }), }); expect(resp.ok).toBeTruthy(); const body = await resp.json(); return body.tokens.access as string; } /** Generate video (POST /api/v1/video/generate) */ async function generate(accessToken: string, duration = 10) { const form = new FormData(); form.append('prompt', 'stress test'); form.append('mode', 'universal'); form.append('model', 'seedance_2.0'); form.append('aspect_ratio', '16:9'); form.append('duration', String(duration)); return fetch(`${BASE}/api/v1/video/generate`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` }, body: form, }); } /** Set user quota via admin API */ async function setQuota(adminToken: string, userId: number, daily: number, monthly: number) { return fetch(`${BASE}/api/v1/admin/users/${userId}/quota`, { method: 'PUT', headers: { 'Authorization': `Bearer ${adminToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ daily_seconds_limit: daily, monthly_seconds_limit: monthly }), }); } /** Set user active status via admin API */ async function setUserStatus(adminToken: string, userId: number, isActive: boolean) { return fetch(`${BASE}/api/v1/admin/users/${userId}/status`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${adminToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ is_active: isActive }), }); } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 1. 配额耗尽极限测试 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ test.describe('极限测试: 日配额耗尽拦截', () => { test('连续生成直至日配额耗尽,超限请求应返回 429', async () => { const adminToken = await loginAdmin(); const user = await registerUser('dq'); // 管理员将用户日配额设为 30s(方便快速耗尽) const quotaResp = await setQuota(adminToken, user.user_id, 30, 6000); expect(quotaResp.ok).toBeTruthy(); // 第 1 次: 10s → 累计 10s, 剩余 20s → 应成功 const r1 = await generate(user.tokens.access, 10); expect(r1.status).toBe(202); const d1 = await r1.json(); expect(d1.seconds_consumed).toBe(10); expect(d1.remaining_seconds_today).toBe(20); // 第 2 次: 10s → 累计 20s, 剩余 10s → 应成功 const r2 = await generate(user.tokens.access, 10); expect(r2.status).toBe(202); const d2 = await r2.json(); expect(d2.remaining_seconds_today).toBe(10); // 第 3 次: 10s → 累计 30s, 剩余 0s → 应成功(刚好用完) const r3 = await generate(user.tokens.access, 10); expect(r3.status).toBe(202); const d3 = await r3.json(); expect(d3.remaining_seconds_today).toBe(0); // 第 4 次: 再请求 10s → 超限 → 应返回 429 const r4 = await generate(user.tokens.access, 10); expect(r4.status).toBe(429); const d4 = await r4.json(); expect(d4.error).toBe('quota_exceeded'); expect(d4.message).toContain('今日'); expect(d4.daily_seconds_used).toBe(30); // 第 5 次: 即使只请求 1s 也应被拦截 const r5 = await generate(user.tokens.access, 1); expect(r5.status).toBe(429); }); test('单次请求超出剩余额度应被拦截', async () => { const adminToken = await loginAdmin(); const user = await registerUser('ds'); // 日配额设为 15s await setQuota(adminToken, user.user_id, 15, 6000); // 先消耗 10s const r1 = await generate(user.tokens.access, 10); expect(r1.status).toBe(202); // 再请求 10s(剩余仅 5s)→ 应被拦截 const r2 = await generate(user.tokens.access, 10); expect(r2.status).toBe(429); const d2 = await r2.json(); expect(d2.error).toBe('quota_exceeded'); // 但请求 5s 应该成功(刚好用完) const r3 = await generate(user.tokens.access, 5); expect(r3.status).toBe(202); expect((await r3.json()).remaining_seconds_today).toBe(0); }); }); test.describe('极限测试: 月配额耗尽拦截', () => { test('月配额耗尽后应返回 429 并提示月度限制', async () => { const adminToken = await loginAdmin(); const user = await registerUser('mq'); // 日配额 100s,月配额仅 25s(月配额比日配额小,优先触发月限制) await setQuota(adminToken, user.user_id, 100, 25); // 消耗 10s const r1 = await generate(user.tokens.access, 10); expect(r1.status).toBe(202); // 消耗 10s(累计 20s) const r2 = await generate(user.tokens.access, 10); expect(r2.status).toBe(202); // 再请求 10s(20+10=30 > 25 月配额)→ 月配额拦截 const r3 = await generate(user.tokens.access, 10); expect(r3.status).toBe(429); const d3 = await r3.json(); expect(d3.error).toBe('quota_exceeded'); expect(d3.message).toContain('本月'); expect(d3.monthly_seconds_used).toBe(20); }); }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 2. 账号禁用/启用功能测试 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ test.describe('极限测试: 账号禁用后权限验证', () => { test('禁用账号后,使用已有 token 调用生成接口应被拒绝', async () => { const adminToken = await loginAdmin(); const user = await registerUser('dis'); // 先确认用户可以正常生成 const r1 = await generate(user.tokens.access, 10); expect(r1.status).toBe(202); // 管理员禁用该用户 const statusResp = await setUserStatus(adminToken, user.user_id, false); expect(statusResp.ok).toBeTruthy(); const statusBody = await statusResp.json(); expect(statusBody.is_active).toBe(false); // 使用已有 token 尝试生成 → 应被拒绝(401 或 403) const r2 = await generate(user.tokens.access, 10); expect([401, 403]).toContain(r2.status); // 使用已有 token 访问个人信息 → 也应被拒绝 const meResp = await fetch(`${BASE}/api/v1/auth/me`, { headers: { 'Authorization': `Bearer ${user.tokens.access}` }, }); expect([401, 403]).toContain(meResp.status); }); test('禁用账号后,重新登录应失败', async () => { const adminToken = await loginAdmin(); const user = await registerUser('dl'); // 管理员禁用该用户 await setUserStatus(adminToken, user.user_id, false); // 尝试登录 → 应失败 const loginResp = await fetch(`${BASE}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user.username, password: TEST_PASS }), }); expect(loginResp.ok).toBeFalsy(); expect(loginResp.status).toBe(401); }); test('重新启用账号后,用户可以登录并正常生成', async () => { const adminToken = await loginAdmin(); const user = await registerUser('re'); // 禁用 await setUserStatus(adminToken, user.user_id, false); // 确认无法登录 const loginFail = await fetch(`${BASE}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user.username, password: TEST_PASS }), }); expect(loginFail.status).toBe(401); // 重新启用 const enableResp = await setUserStatus(adminToken, user.user_id, true); expect(enableResp.ok).toBeTruthy(); expect((await enableResp.json()).is_active).toBe(true); // 重新登录 → 应成功 const loginOk = await fetch(`${BASE}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user.username, password: TEST_PASS }), }); expect(loginOk.ok).toBeTruthy(); const newTokens = (await loginOk.json()).tokens; // 使用新 token 生成 → 应成功 const r = await generate(newTokens.access, 10); expect(r.status).toBe(202); }); }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 3. 管理员动态调整额度测试 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ test.describe('极限测试: 管理员调整额度后立即生效', () => { test('调低额度后,已消耗量超新限制的请求应被拦截', async () => { const adminToken = await loginAdmin(); const user = await registerUser('ql'); // 默认日配额 600s,先消耗 50s for (let i = 0; i < 5; i++) { const r = await generate(user.tokens.access, 10); expect(r.status).toBe(202); } // 管理员将日配额调低为 40s(低于已消耗的 50s) await setQuota(adminToken, user.user_id, 40, 6000); // 再请求 → 应被拦截(已用 50s > 新限 40s) const r = await generate(user.tokens.access, 1); expect(r.status).toBe(429); const d = await r.json(); expect(d.error).toBe('quota_exceeded'); expect(d.daily_seconds_used).toBe(50); }); test('调高额度后,之前被拦截的请求应可以通过', async () => { const adminToken = await loginAdmin(); const user = await registerUser('qh'); // 日配额设为 20s await setQuota(adminToken, user.user_id, 20, 6000); // 消耗 20s 用完 const r1 = await generate(user.tokens.access, 10); expect(r1.status).toBe(202); const r2 = await generate(user.tokens.access, 10); expect(r2.status).toBe(202); // 确认被拦截 const r3 = await generate(user.tokens.access, 10); expect(r3.status).toBe(429); // 管理员调高日配额到 100s await setQuota(adminToken, user.user_id, 100, 6000); // 现在应该可以继续生成(已用 20s,新限 100s,还剩 80s) const r4 = await generate(user.tokens.access, 10); expect(r4.status).toBe(202); const d4 = await r4.json(); expect(d4.remaining_seconds_today).toBe(70); // 100 - 20 - 10 = 70 }); test('调整月配额同样立即生效', async () => { const adminToken = await loginAdmin(); const user = await registerUser('qm'); // 日配额 200s,月配额 30s await setQuota(adminToken, user.user_id, 200, 30); // 消耗 20s const r1 = await generate(user.tokens.access, 10); expect(r1.status).toBe(202); const r2 = await generate(user.tokens.access, 10); expect(r2.status).toBe(202); // 再请求 15s → 20+15=35 > 30 月配额 → 拦截 const r3 = await generate(user.tokens.access, 15); expect(r3.status).toBe(429); // 调高月配额到 100s await setQuota(adminToken, user.user_id, 200, 100); // 现在可以继续 const r4 = await generate(user.tokens.access, 15); expect(r4.status).toBe(202); }); }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 4. 边界条件测试 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ test.describe('极限测试: 边界条件', () => { test('配额恰好为 0s 时任何请求都应被拦截', async () => { const adminToken = await loginAdmin(); const user = await registerUser('z0'); // 日配额设为 0 await setQuota(adminToken, user.user_id, 0, 6000); const r = await generate(user.tokens.access, 1); expect(r.status).toBe(429); }); test('并发请求竞态条件检测(SQLite 无行锁,记录实际行为)', async () => { const adminToken = await loginAdmin(); const user = await registerUser('cc'); // 日配额 15s,并发发 3 个 10s 请求 await setQuota(adminToken, user.user_id, 15, 6000); const results = await Promise.all([ generate(user.tokens.access, 10), generate(user.tokens.access, 10), generate(user.tokens.access, 10), ]); const statuses = results.map(r => r.status); const successCount = statuses.filter(s => s === 202).length; // 记录并发行为: // - MySQL/PostgreSQL + select_for_update:最多成功 1 个 // - SQLite 无行锁:可能全部成功(已知限制) // 此测试验证并发请求被正确处理(无 500 错误) expect(statuses.every(s => s === 202 || s === 429)).toBeTruthy(); // 如果使用 MySQL 生产环境,取消下面注释启用严格断言: // expect(successCount).toBeLessThanOrEqual(1); console.log(`并发测试结果: ${successCount} 个成功, ${3 - successCount} 个被拦截 (SQLite 环境)`); }); });