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