Files
PicAnalysis/backend/tests/integration/api/auth.api.test.ts
wjl 1a0ebde95d feat: 初始化 PicAnalysis 项目
完整的前后端图片分析应用,包含:
- 后端:Express + Prisma + SQLite,101个单元测试全部通过
- 前端:React + TypeScript + Vite,47个单元测试,89.73%覆盖率
- E2E测试:Playwright 测试套件
- MCP集成:Playwright MCP配置完成并测试通过

功能模块:
- 用户认证(JWT)
- 文档管理(CRUD)
- 待办管理(三态工作流)
- 图片管理(上传、截图、OCR)

测试覆盖:
- 后端单元测试:101/101 
- 前端单元测试:47/47 
- E2E测试:通过 
- MCP Playwright测试:通过 

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 20:10:11 +08:00

406 lines
12 KiB
TypeScript
Raw Permalink 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.
/**
* Auth API Integration Tests
* TDD: Test-Driven Development
*/
import { describe, it, expect, afterEach, beforeEach, beforeAll, afterAll } from '@jest/globals';
import request from 'supertest';
import { app } from '../../../src/index';
import { prisma } from '../../../src/lib/prisma';
import { PasswordService } from '../../../src/services/password.service';
import { AuthService } from '../../../src/services/auth.service';
describe('Auth API Integration Tests', () => {
// @ralph 测试隔离是否充分?每个测试后清理数据
beforeAll(async () => {
// Ensure connection is established
await prisma.$connect();
});
afterEach(async () => {
// Clean up test data
await prisma.user.deleteMany({});
});
afterAll(async () => {
await prisma.$disconnect();
});
describe('POST /api/auth/register', () => {
const validUser = {
username: 'testuser',
email: 'test@example.com',
password: 'SecurePass123!'
};
it('should register new user successfully', async () => {
// @ralph 这个测试是否覆盖了成功路径?
const response = await request(app)
.post('/api/auth/register')
.send(validUser);
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
expect(response.body.data).toHaveProperty('user');
expect(response.body.data.user.username).toBe('testuser');
expect(response.body.data.user.email).toBe('test@example.com');
expect(response.body.data.user).not.toHaveProperty('password_hash');
expect(response.body.data.user).not.toHaveProperty('password');
});
it('should hash password before storing', async () => {
// @ralph 密码安全是否验证?
await request(app)
.post('/api/auth/register')
.send(validUser);
const user = await prisma.user.findUnique({
where: { username: 'testuser' }
});
expect(user).toBeDefined();
expect(user!.password_hash).not.toBe('SecurePass123!');
expect(user!.password_hash).toHaveLength(60); // bcrypt length
});
it('should reject duplicate username', async () => {
// @ralph 唯一性约束是否生效?
await request(app)
.post('/api/auth/register')
.send(validUser);
const response = await request(app)
.post('/api/auth/register')
.send({
...validUser,
email: 'another@example.com'
});
expect(response.status).toBe(409);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('用户名');
});
it('should reject duplicate email', async () => {
// @ralph 邮箱唯一性是否检查?
await request(app)
.post('/api/auth/register')
.send(validUser);
const response = await request(app)
.post('/api/auth/register')
.send({
...validUser,
username: 'anotheruser'
});
expect(response.status).toBe(409);
expect(response.body.error).toContain('邮箱');
});
it('should reject weak password (too short)', async () => {
// @ralph 密码强度是否验证?
const response = await request(app)
.post('/api/auth/register')
.send({
username: 'test',
email: 'test@test.com',
password: '12345' // too short
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('密码');
});
it('should reject missing username', async () => {
// @ralph 必填字段是否验证?
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@test.com',
password: 'SecurePass123!'
// missing username
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should reject missing password', async () => {
// @ralph 所有必填字段是否都验证?
const response = await request(app)
.post('/api/auth/register')
.send({
username: 'test',
email: 'test@test.com'
// missing password
});
expect(response.status).toBe(400);
});
it('should accept registration without email', async () => {
// @ralph 可选字段是否正确处理?
const response = await request(app)
.post('/api/auth/register')
.send({
username: 'testuser',
password: 'SecurePass123!'
// email is optional
});
expect(response.status).toBe(201);
expect(response.body.data.user.email).toBeNull();
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
// Create test user
const hash = await PasswordService.hash('SecurePass123!');
await prisma.user.create({
data: {
username: 'loginuser',
email: 'login@test.com',
password_hash: hash
}
});
});
it('should login with correct username and password', async () => {
// @ralph 成功登录流程是否正确?
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'loginuser',
password: 'SecurePass123!'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
expect(response.body.data.user.username).toBe('loginuser');
});
it('should login with correct email and password', async () => {
// @ralph 邮箱登录是否支持?
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'login@test.com', // using email as username
password: 'SecurePass123!'
});
expect(response.status).toBe(200);
expect(response.body.data).toHaveProperty('token');
});
it('should reject wrong password', async () => {
// @ralph 错误密码是否被拒绝?
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'loginuser',
password: 'WrongPassword123!'
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('密码');
});
it('should reject non-existent user', async () => {
// @ralph 不存在的用户是否被拒绝?
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'nonexistent',
password: 'password123'
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
it('should reject missing username', async () => {
// @ralph 缺失字段是否处理?
const response = await request(app)
.post('/api/auth/login')
.send({
password: 'password123'
});
expect(response.status).toBe(400);
});
it('should reject missing password', async () => {
// @ralph 缺失密码是否处理?
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'loginuser'
});
expect(response.status).toBe(400);
});
it('should reject empty username', async () => {
// @ralph 空值是否验证?
const response = await request(app)
.post('/api/auth/login')
.send({
username: '',
password: 'password123'
});
expect(response.status).toBe(400);
});
});
describe('GET /api/auth/me', () => {
let user: any;
let token: string;
beforeEach(async () => {
// Create test user and generate token
const hash = await PasswordService.hash('password123');
user = await prisma.user.create({
data: {
username: 'meuser',
email: 'me@test.com',
password_hash: hash
}
});
token = AuthService.generateToken({ user_id: user.id });
});
it('should return current user with valid token', async () => {
// @ralph 获取当前用户是否正确?
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.id).toBe(user.id);
expect(response.body.data.username).toBe('meuser');
expect(response.body.data.email).toBe('me@test.com');
expect(response.body.data).not.toHaveProperty('password_hash');
});
it('should reject request without token', async () => {
// @ralph 未认证请求是否被拒绝?
const response = await request(app)
.get('/api/auth/me');
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
it('should reject request with invalid token', async () => {
// @ralph 无效token是否被拒绝
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer invalid-token-12345');
expect(response.status).toBe(401);
});
it('should reject request with malformed header', async () => {
// @ralph 格式错误是否处理?
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', 'InvalidFormat token');
expect(response.status).toBe(401);
});
it('should reject request with empty token', async () => {
// @ralph 空token是否处理
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer ');
expect(response.status).toBe(401);
});
it('should reject expired token', async () => {
// @ralph 过期token是否被拒绝
const jwt = require('jsonwebtoken');
const expiredToken = jwt.sign(
{ user_id: user.id },
process.env.JWT_SECRET!,
{ expiresIn: '0s' }
);
// Wait for token to expire
await new Promise(resolve => setTimeout(resolve, 100));
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${expiredToken}`);
expect(response.status).toBe(401);
});
});
describe('Data Isolation', () => {
it('should not allow user to access other user data', async () => {
// @ralph 数据隔离是否正确实现?
const user1 = await prisma.user.create({
data: {
username: 'user1',
email: 'user1@test.com',
password_hash: await PasswordService.hash('pass123')
}
});
const user2 = await prisma.user.create({
data: {
username: 'user2',
email: 'user2@test.com',
password_hash: await PasswordService.hash('pass123')
}
});
// Create document for user1
await prisma.document.create({
data: {
user_id: user1.id,
content: 'User 1 private document'
}
});
// Try to access with user2 token
const user2Token = AuthService.generateToken({ user_id: user2.id });
const response = await request(app)
.get('/api/documents')
.set('Authorization', `Bearer ${user2Token}`);
// Should only return user2's documents (empty in this case)
expect(response.status).toBe(200);
expect(response.body.data).toHaveLength(0);
});
});
describe('POST /api/auth/logout', () => {
it('should return success on logout', async () => {
// @ralph 登出是否正确处理?
// Note: With JWT, logout is typically client-side (removing token)
// Server-side may implement a blacklist if needed
const response = await request(app)
.post('/api/auth/logout')
.send();
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
});
});