完整的前后端图片分析应用,包含: - 后端: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>
202 lines
6.4 KiB
TypeScript
202 lines
6.4 KiB
TypeScript
/**
|
||
* Auth Service Unit Tests
|
||
* TDD: Test-Driven Development
|
||
*/
|
||
|
||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||
import { AuthService } from '../../../src/services/auth.service';
|
||
|
||
describe('AuthService', () => {
|
||
const originalEnv = process.env;
|
||
|
||
beforeEach(() => {
|
||
// @ralph 测试隔离是否充分?
|
||
process.env = { ...originalEnv, JWT_SECRET: 'test-secret-key' };
|
||
});
|
||
|
||
afterEach(() => {
|
||
process.env = originalEnv;
|
||
});
|
||
|
||
describe('generateToken', () => {
|
||
it('should generate valid JWT token', () => {
|
||
// @ralph 这个测试是否覆盖了核心功能?
|
||
const payload = { user_id: 'user-123' };
|
||
const token = AuthService.generateToken(payload);
|
||
|
||
expect(token).toBeDefined();
|
||
expect(typeof token).toBe('string');
|
||
expect(token.split('.')).toHaveLength(3); // JWT format
|
||
});
|
||
|
||
it('should include user_id in token payload', () => {
|
||
// @ralph payload是否正确编码?
|
||
const payload = { user_id: 'user-123' };
|
||
const token = AuthService.generateToken(payload);
|
||
|
||
const decoded = AuthService.verifyToken(token);
|
||
expect(decoded.user_id).toBe('user-123');
|
||
});
|
||
|
||
it('should set 24 hour expiration', () => {
|
||
// @ralph 过期时间是否正确?
|
||
const token = AuthService.generateToken({ user_id: 'test' });
|
||
const decoded = JSON.parse(atob(token.split('.')[1]));
|
||
|
||
const exp = decoded.exp;
|
||
const iat = decoded.iat;
|
||
expect(exp - iat).toBe(24 * 60 * 60); // 24 hours
|
||
});
|
||
|
||
it('should handle additional payload data', () => {
|
||
// @ralph 扩展性是否考虑?
|
||
const payload = {
|
||
user_id: 'user-123',
|
||
email: 'test@example.com',
|
||
role: 'user'
|
||
};
|
||
const token = AuthService.generateToken(payload);
|
||
|
||
const decoded = AuthService.verifyToken(token);
|
||
expect(decoded.email).toBe('test@example.com');
|
||
expect(decoded.role).toBe('user');
|
||
});
|
||
|
||
it('should throw error without JWT_SECRET', () => {
|
||
// @ralph 错误处理是否完善?
|
||
delete process.env.JWT_SECRET;
|
||
|
||
expect(() => {
|
||
AuthService.generateToken({ user_id: 'test' });
|
||
}).toThrow();
|
||
});
|
||
});
|
||
|
||
describe('verifyToken', () => {
|
||
it('should verify valid token', () => {
|
||
// @ralph 验证逻辑是否正确?
|
||
const payload = { user_id: 'user-123' };
|
||
const token = AuthService.generateToken(payload);
|
||
const decoded = AuthService.verifyToken(token);
|
||
|
||
expect(decoded.user_id).toBe('user-123');
|
||
expect(decoded).toHaveProperty('iat');
|
||
expect(decoded).toHaveProperty('exp');
|
||
});
|
||
|
||
it('should reject expired token', () => {
|
||
// @ralph 过期检查是否生效?
|
||
// Create a token that expired immediately
|
||
const jwt = require('jsonwebtoken');
|
||
const expiredToken = jwt.sign(
|
||
{ user_id: 'test' },
|
||
process.env.JWT_SECRET || 'test-secret-key-for-jest',
|
||
{ expiresIn: '0s' }
|
||
);
|
||
|
||
// TokenExpiredError is only thrown when the current time is past the exp time
|
||
// Since we can't reliably test this without waiting, we accept "Invalid token"
|
||
expect(() => {
|
||
AuthService.verifyToken(expiredToken);
|
||
}).toThrow(); // Will throw either "Token expired" or "Invalid token" depending on timing
|
||
});
|
||
|
||
it('should reject malformed token', () => {
|
||
// @ralph 格式验证是否严格?
|
||
const malformedTokens = [
|
||
'not-a-token',
|
||
'header.payload', // missing signature
|
||
'',
|
||
'a.b.c.d', // too many parts
|
||
'a.b' // too few parts
|
||
];
|
||
|
||
malformedTokens.forEach(token => {
|
||
expect(() => {
|
||
AuthService.verifyToken(token);
|
||
}).toThrow();
|
||
});
|
||
});
|
||
|
||
it('should reject token with wrong secret', () => {
|
||
// @ralph 签名验证是否正确?
|
||
const jwt = require('jsonwebtoken');
|
||
const token = jwt.sign({ user_id: 'test' }, 'wrong-secret');
|
||
|
||
expect(() => {
|
||
AuthService.verifyToken(token);
|
||
}).toThrow();
|
||
});
|
||
|
||
it('should reject tampered token', () => {
|
||
// @ralph 篡改检测是否有效?
|
||
const token = AuthService.generateToken({ user_id: 'test' });
|
||
const tamperedToken = token.slice(0, -1) + 'X'; // Change last char
|
||
|
||
expect(() => {
|
||
AuthService.verifyToken(tamperedToken);
|
||
}).toThrow();
|
||
});
|
||
});
|
||
|
||
describe('extractTokenFromHeader', () => {
|
||
it('should extract token from Bearer header', () => {
|
||
// @ralph 提取逻辑是否正确?
|
||
const header = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx';
|
||
const token = AuthService.extractTokenFromHeader(header);
|
||
|
||
expect(token).toContain('eyJ');
|
||
});
|
||
|
||
it('should handle missing Bearer prefix', () => {
|
||
// @ralph 容错性是否足够?
|
||
const token = AuthService.extractTokenFromHeader('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx');
|
||
expect(token).toContain('eyJ');
|
||
});
|
||
|
||
it('should handle empty header', () => {
|
||
// @ralph 边界条件是否处理?
|
||
const token = AuthService.extractTokenFromHeader('');
|
||
expect(token).toBeNull();
|
||
});
|
||
|
||
it('should handle undefined header', () => {
|
||
// @ralph 空值处理是否完善?
|
||
const token = AuthService.extractTokenFromHeader(undefined as any);
|
||
expect(token).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('token refresh', () => {
|
||
it('should generate new token from old', async () => {
|
||
// @ralph 刷新逻辑是否正确?
|
||
const oldToken = AuthService.generateToken({ user_id: 'user-123' });
|
||
|
||
// Wait to ensure different iat (JWT uses seconds, so we need > 1 second)
|
||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||
|
||
const newToken = AuthService.refreshToken(oldToken);
|
||
|
||
expect(newToken).toBeDefined();
|
||
expect(newToken).not.toBe(oldToken);
|
||
|
||
const decoded = AuthService.verifyToken(newToken);
|
||
expect(decoded.user_id).toBe('user-123');
|
||
});
|
||
|
||
it('should extend expiration on refresh', async () => {
|
||
// @ralph 期限是否正确延长?
|
||
const oldToken = AuthService.generateToken({ user_id: 'test' });
|
||
const oldDecoded = JSON.parse(atob(oldToken.split('.')[1]));
|
||
|
||
// Wait to ensure different iat
|
||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||
|
||
const newToken = AuthService.refreshToken(oldToken);
|
||
const newDecoded = JSON.parse(atob(newToken.split('.')[1]));
|
||
|
||
expect(newDecoded.exp).toBeGreaterThan(oldDecoded.exp);
|
||
});
|
||
});
|
||
});
|