87357c8bde
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Failing after 71h21m22s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 71h22m9s
- e2e: request: any → APIRequestContext 类型替换(6个文件) - e2e: 移除未使用变量 _sourceAssetId, _hasProgress - src: react-refresh/only-export-components 添加 eslint-disable 注释 (MainLayout, Input, router/index) - src: EditingPlanner useEffect 补全 resetClips 依赖 - src: MyVoices okButtonProps 类型修正 as ButtonProps Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
344 lines
11 KiB
TypeScript
Executable File
344 lines
11 KiB
TypeScript
Executable File
/**
|
||
* 认证流程 E2E 测试
|
||
*
|
||
* 覆盖:注册(正向/反向)、登录(正向/反向)、登出、获取当前用户信息
|
||
* 每个测试独立,使用随机邮箱避免冲突。
|
||
*/
|
||
import { expect, test, type APIRequestContext } 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)}`;
|
||
}
|
||
|
||
/** 从错误响应中提取错误消息文本,兼容新老格式 */
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
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: APIRequestContext,
|
||
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 response = await request.post(`${apiBase}/auth/register`, {
|
||
data: {
|
||
email: uniqueEmail("emptyuser"),
|
||
password: PASSWORD,
|
||
username: "",
|
||
display_name: "Empty Username",
|
||
},
|
||
});
|
||
|
||
expect([400, 422], "用户名为空应返回 4xx 校验错误").toContain(
|
||
response.status(),
|
||
);
|
||
});
|
||
|
||
test("注册缺用户名字段 - 反向", async ({ request }) => {
|
||
const response = await request.post(`${apiBase}/auth/register`, {
|
||
data: {
|
||
email: uniqueEmail("nouser"),
|
||
password: PASSWORD,
|
||
display_name: "No Username Field",
|
||
},
|
||
});
|
||
|
||
expect(
|
||
[400, 422],
|
||
"缺少用户名字段应返回 4xx 校验错误",
|
||
).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 }) => {
|
||
// 使用一个伪造的过期 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());
|
||
});
|
||
});
|