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