21fc2869ff
Deploy / Staging E2E Tests (push) Has been skipped
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 / Deploy Staging (push) Failing after 141h54m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 141h54m17s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 141h54m17s
246 lines
8.8 KiB
TypeScript
Executable File
246 lines
8.8 KiB
TypeScript
Executable File
/**
|
||
* 认证流程 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());
|
||
});
|
||
});
|