/** * 认证流程 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()); }); });