Files
xiaoxia-saas/apps/web/e2e/test_auth.spec.ts
T
xiaoxia 254ffd5391
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Staging E2E Tests (push) Failing after 116h7m54s
Deploy / Deploy Staging (push) Failing after 116h9m30s
CI/CD Pipeline / Frontend Lint (push) Failing after 116h10m2s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 116h10m2s
test(e2e): 修复生产环境稳定性问题 - force click + 限流重试 + 超时调整
- core-generation: 素材卡片和生成按钮点击加 force:true,跳过 actionability 检查
- core-upload: 素材库项点击加 force:true
- test_auth: 反向登录测试增加限流重试逻辑
- test_asset/test_auth/test_project: describe 超时从 120s 增至 180s
- 生产环境建议 workers=1 运行,避免登录限流累积
2026-07-04 16:43:35 +08:00

313 lines
10 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 认证流程 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)}`;
}
/** 从错误响应中提取错误消息文本,兼容新老格式 */
function extractErrorMessage(body: any): string {
if (!body) return "";
// 新格式: { error: { code: "...", message: "..." } }
if (body.error && typeof body.error === "object" && body.error.message) {
return String(body.error.message);
}
// 老格式: { detail: "..." } 或 { message: "..." } 或 { error: "..." }
return String(body.detail || body.message || body.error || "");
}
/** 登录操作,遇到 429 限流自动等待重试(最多等 65s) */
async function loginWithRetry(
request: any,
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 },
});
}
test.describe("认证流程", () => {
// 登录限流 10次/60s,测试可能触发限流等待,给足够超时
test.describe.configure({ timeout: 180_000 });
// ─── 注册 ────────────────────────────────────────────
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 = extractErrorMessage(body).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 loginWithRetry(request, email, 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 loginWithRetry(request, email, "WrongPassword999!");
expect(response.status(), "错误密码应返回 401").toBe(401);
});
test("登录不存在的邮箱 - 反向", async ({ request }) => {
// 带限流重试的反向登录测试
let response;
for (let attempt = 0; attempt < 3; attempt++) {
response = await request.post(`${apiBase}/auth/login`, {
data: { email: `ghost_${Date.now()}@nonexist.com`, password: PASSWORD },
});
if (response.status() !== 429) break;
console.log(`[反向登录测试] 触发限流,等待 65s 后重试 (${attempt + 1}/2)`);
await new Promise((r) => setTimeout(r, 65_000));
}
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 loginWithRetry(request, email, 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 loginWithRetry(request, email, 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 }) => {
// 使用一个伪造的过期 JWTheader.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());
});
});