/** * 认证流程 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)}`; } test.describe('认证流程', () => { // ─── 注册 ──────────────────────────────────────────── test('注册新用户 - 正向', async ({ request }) => { const email = uniqueEmail('reg-ok'); const username = uniqueUsername('regok'); const response = await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: 'E2E 注册测试', }, }); expect(response.ok(), `注册应返回 2xx,实际: ${response.status()} ${await response.text()}`).toBeTruthy(); const data = await response.json(); expect(data.user_id, '应返回 user_id').toBeTruthy(); expect(data.email).toBe(email); expect(data.username).toBe(username); }); test('注册已存在邮箱 - 反向', async ({ request }) => { const email = uniqueEmail('reg-dup'); const username1 = uniqueUsername('regdup1'); const username2 = uniqueUsername('regdup2'); // 第一次注册 const first = await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username: username1, display_name: 'User 1' }, }); expect(first.ok(), '第一次注册应成功').toBeTruthy(); // 第二次使用相同邮箱 const second = await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username: username2, display_name: 'User 2' }, }); expect(second.status(), '重复邮箱注册应返回 4xx').toBeGreaterThanOrEqual(400); expect(second.status()).toBeLessThan(500); const body = await second.json(); // 错误信息应包含"已注册"或"exists"相关提示 const detail = (body.detail || body.message || body.error || '').toString().toLowerCase(); expect( detail.includes('已') || detail.includes('exist') || detail.includes('registered') || detail.includes('duplicate'), `错误信息应提示邮箱已注册,实际: "${detail}"`, ).toBeTruthy(); }); test('注册无效邮箱格式 - 反向', async ({ request }) => { const response = await request.post(`${apiBase}/auth/register`, { data: { email: 'not-an-email', password: PASSWORD, username: uniqueUsername('bademail'), display_name: 'Bad Email', }, }); // 422 是 FastAPI 参数校验失败的标准状态码 expect([400, 422]).toContain(response.status()); }); test('注册弱密码 - 反向', async ({ request }) => { const response = await request.post(`${apiBase}/auth/register`, { data: { email: uniqueEmail('weakpwd'), password: '123', username: uniqueUsername('weakpwd'), display_name: 'Weak', }, }); expect([400, 422]).toContain(response.status()); }); // ─── 登录 ──────────────────────────────────────────── test('登录成功 - 正向', async ({ request }) => { const email = uniqueEmail('login-ok'); const username = uniqueUsername('loginok'); // 先注册 const reg = await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: 'Login Test' }, }); expect(reg.ok(), '注册应成功').toBeTruthy(); // 登录 const response = await request.post(`${apiBase}/auth/login`, { data: { email, password: PASSWORD }, }); expect(response.ok(), `登录应返回 2xx,实际: ${response.status()} ${await response.text()}`).toBeTruthy(); const data = await response.json(); expect(data.access_token, '应返回 access_token').toBeTruthy(); expect(data.token_type).toBe('bearer'); expect(data.email).toBe(email); }); test('登录错误密码 - 反向', async ({ request }) => { const email = uniqueEmail('login-bad'); const username = uniqueUsername('loginbad'); // 先注册 await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: 'Bad Login' }, }); // 使用错误密码登录 const response = await request.post(`${apiBase}/auth/login`, { data: { email, password: 'WrongPassword999!' }, }); expect(response.status(), '错误密码应返回 401').toBe(401); }); test('登录不存在的邮箱 - 反向', async ({ request }) => { const response = await request.post(`${apiBase}/auth/login`, { data: { email: `ghost_${Date.now()}@nonexist.com`, password: PASSWORD }, }); expect(response.status(), '不存在的用户应返回 401').toBe(401); }); // ─── 登出 ──────────────────────────────────────────── test('登出成功', async ({ request }) => { const email = uniqueEmail('logout'); const username = uniqueUsername('logout'); // 注册 & 登录 await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: 'Logout Test' }, }); const login = await request.post(`${apiBase}/auth/login`, { data: { email, password: PASSWORD }, }); const { access_token } = await login.json(); const headers = { Authorization: `Bearer ${access_token}` }; // 登出 const logout = await request.post(`${apiBase}/auth/logout`, { headers }); expect(logout.ok(), `登出应返回 2xx,实际: ${logout.status()}`).toBeTruthy(); const body = await logout.json(); expect(body.message).toBeTruthy(); // 登出后 token 应失效,尝试访问 /auth/me const me = await request.get(`${apiBase}/auth/me`, { headers }); expect([401, 403]).toContain(me.status()); }); // ─── 获取当前用户信息 ───────────────────────────────── test('获取当前用户信息 - 正向', async ({ request }) => { const email = uniqueEmail('me-ok'); const username = uniqueUsername('meok'); await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: 'Me Test' }, }); const login = await request.post(`${apiBase}/auth/login`, { data: { email, password: PASSWORD }, }); const { access_token } = await login.json(); const response = await request.get(`${apiBase}/auth/me`, { headers: { Authorization: `Bearer ${access_token}` }, }); expect(response.ok(), `获取用户信息应返回 2xx,实际: ${response.status()}`).toBeTruthy(); const data = await response.json(); expect(data.user_id).toBeTruthy(); expect(data.email).toBe(email); expect(data.username).toBe(username); }); test('无 token 获取用户信息 - 反向', async ({ request }) => { const response = await request.get(`${apiBase}/auth/me`); // HTTPBearer 无凭证返回 403 expect([401, 403]).toContain(response.status()); }); test('无效 token 获取用户信息 - 反向', async ({ request }) => { const response = await request.get(`${apiBase}/auth/me`, { headers: { Authorization: 'Bearer invalid.token.here' }, }); expect(response.status()).toBe(401); }); test('过期 token 获取用户信息 - 反向', async ({ request }) => { // 使用一个伪造的过期 JWT(header.payload.signature) // eyJhbGciOiJIUzI1NiJ9 = {"alg":"HS256"} // eyJleHAiOjF9 = {"exp":1} (1970-01-01 过期) const expiredToken = 'eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjEsInN1YiI6InRlc3QtdXNlciJ9.expired_signature'; const response = await request.get(`${apiBase}/auth/me`, { headers: { Authorization: `Bearer ${expiredToken}` }, }); expect([401, 403]).toContain(response.status()); }); test('token 格式错误 - 反向', async ({ request }) => { const response = await request.get(`${apiBase}/auth/me`, { headers: { Authorization: 'Bearer not-a-jwt' }, }); expect([401, 403]).toContain(response.status()); }); test('空 Bearer token - 反向', async ({ request }) => { const response = await request.get(`${apiBase}/auth/me`, { headers: { Authorization: 'Bearer ' }, }); expect([401, 403]).toContain(response.status()); }); });