/** * 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); }); }); });