Files
PicAnalysis/backend/tests/integration/api/auth.api.test.ts

406 lines
12 KiB
TypeScript
Raw Normal View History

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