Files
SerpentRace/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts
T
Donat 86211923db Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization:
- Created IBaseRepository.ts and IPaginatedRepository.ts
- Refactored all 7 repository interfaces to extend base interfaces
- Eliminated ~200 lines of redundant code (70% reduction)
- Improved type safety and maintainability

 Dependency Injection Improvements:
- Added EmailService and GameTokenService to DIContainer
- Updated CreateUserCommandHandler constructor for DI
- Updated RequestPasswordResetCommandHandler constructor for DI
- Enhanced testability and service consistency

 Environment Configuration:
- Created comprehensive .env.example with 40+ variables
- Organized into 12 logical sections (Database, Security, Email, etc.)
- Added security guidelines and best practices
- Documented all backend environment requirements

 Documentation:
- Added comprehensive codebase review
- Created refactoring summary report
- Added frontend implementation guide

Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
2025-09-21 03:27:57 +02:00

404 lines
12 KiB
TypeScript

import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService';
import { Request, Response } from 'express';
import { UserState } from '../../../src/Domain/User/UserAggregate';
describe('JWTService', () => {
let jwtService: JWTService;
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
beforeEach(() => {
jest.clearAllMocks();
jwtService = new JWTService();
// Set a test secret for consistent testing
process.env.JWT_SECRET = 'test-secret-key-for-testing';
process.env.JWT_EXPIRY = '3600'; // 1 hour
// Mock express Request and Response
mockRequest = {
cookies: {}
};
mockResponse = {
cookie: jest.fn()
};
});
afterEach(() => {
// Clean up environment
delete process.env.JWT_SECRET;
delete process.env.JWT_EXPIRY;
});
describe('create', () => {
it('should create a valid JWT token and set cookie', () => {
// Arrange
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
// Act
const token = jwtService.create(payload, mockResponse as Response);
// Assert
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
expect(mockResponse.cookie).toHaveBeenCalledWith(
'auth_token',
token,
expect.objectContaining({
httpOnly: true,
sameSite: 'strict',
maxAge: 86400000 // 24 hours in milliseconds
})
);
});
it('should create different tokens for different payloads', () => {
// Arrange
const payload1: TokenPayload = {
userId: 'user-1',
authLevel: 0 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-1'
};
const payload2: TokenPayload = {
userId: 'user-2',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_PREMIUM,
orgId: 'org-2'
};
// Act
const token1 = jwtService.create(payload1, mockResponse as Response);
const token2 = jwtService.create(payload2, mockResponse as Response);
// Assert
expect(token1).toBeDefined();
expect(token2).toBeDefined();
expect(token1).not.toBe(token2);
});
it('should set secure cookie in production environment', () => {
// Arrange
process.env.NODE_ENV = 'production';
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
// Act
const token = jwtService.create(payload, mockResponse as Response);
// Assert
expect(mockResponse.cookie).toHaveBeenCalledWith(
'auth_token',
token,
expect.objectContaining({
secure: true
})
);
// Clean up
delete process.env.NODE_ENV;
});
});
describe('verify', () => {
it('should verify a valid token from cookies', () => {
// Arrange
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
const token = jwtService.create(payload, mockResponse as Response);
mockRequest.cookies = { auth_token: token };
// Act
const result = jwtService.verify(mockRequest as Request);
// Assert
expect(result).toBeDefined();
expect(result!.userId).toBe('user-123');
expect(result!.authLevel).toBe(1);
expect(result!.orgId).toBe('org-456');
});
it('should return null when no token is present in cookies', () => {
// Arrange
mockRequest.cookies = {};
// Act
const result = jwtService.verify(mockRequest as Request);
// Assert
expect(result).toBeNull();
});
it('should return null for invalid token', () => {
// Arrange
mockRequest.cookies = { auth_token: 'invalid.jwt.token' };
// Act
const result = jwtService.verify(mockRequest as Request);
// Assert
expect(result).toBeNull();
});
it('should return null for malformed token', () => {
// Arrange
mockRequest.cookies = { auth_token: 'not-a-jwt-token' };
// Act
const result = jwtService.verify(mockRequest as Request);
// Assert
expect(result).toBeNull();
});
});
describe('token creation with different payloads', () => {
it('should create tokens with dynamic user data', () => {
// Arrange
const timestamp = Date.now();
const testPayload: TokenPayload = {
userId: `test-user-${timestamp}`,
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: `test-org-${timestamp}`
};
// Act
const token = jwtService.create(testPayload, mockResponse as Response);
// Assert
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(mockResponse.cookie).toHaveBeenCalled();
// Verify we can decode it back
mockRequest.cookies = { auth_token: token };
const verifiedPayload = jwtService.verify(mockRequest as Request);
expect(verifiedPayload).toBeDefined();
expect(verifiedPayload!.userId).toBe(testPayload.userId);
expect(verifiedPayload!.orgId).toBe(testPayload.orgId);
expect(verifiedPayload!.authLevel).toBe(testPayload.authLevel);
});
it('should create different tokens for different timestamps', async () => {
// Arrange
const timestamp1 = Date.now();
const payload1: TokenPayload = {
userId: `test-user-${timestamp1}`,
authLevel: 0 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: `test-org-${timestamp1}`
};
// Add a small delay to ensure different timestamps
await new Promise(resolve => setTimeout(resolve, 1));
const timestamp2 = Date.now();
const payload2: TokenPayload = {
userId: `test-user-${timestamp2}`,
authLevel: 1 as const,
userStatus: UserState.VERIFIED_PREMIUM,
orgId: `test-org-${timestamp2}`
};
// Act
const token1 = jwtService.create(payload1, mockResponse as Response);
const token2 = jwtService.create(payload2, mockResponse as Response);
// Assert
expect(token1).not.toBe(token2);
expect(payload1.userId).not.toBe(payload2.userId);
expect(payload1.orgId).not.toBe(payload2.orgId);
});
});
describe('integration scenarios', () => {
it('should create and verify token in complete flow', () => {
// Arrange
const originalPayload: TokenPayload = {
userId: 'integration-user',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'integration-org'
};
// Act - Complete flow
const token = jwtService.create(originalPayload, mockResponse as Response);
mockRequest.cookies = { auth_token: token };
const verifiedPayload = jwtService.verify(mockRequest as Request);
// Assert
expect(token).toBeDefined();
expect(verifiedPayload).toBeDefined();
expect(verifiedPayload!.userId).toBe('integration-user');
expect(verifiedPayload!.authLevel).toBe(1);
expect(verifiedPayload!.orgId).toBe('integration-org');
});
});
describe('JWT_EXPIRATION duration parsing', () => {
it('should parse JWT_EXPIRATION in hours format', () => {
// Arrange
delete process.env.JWT_EXPIRY;
process.env.JWT_EXPIRATION = '2h';
// Act
const newJwtService = new JWTService();
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
const token = newJwtService.create(payload, mockResponse as Response);
// Update mock request with the created token
mockRequest.cookies = { auth_token: token };
const verifiedPayload = newJwtService.verify(mockRequest as Request);
// Assert
expect(token).toBeDefined();
expect(verifiedPayload).toBeDefined();
expect(verifiedPayload!.exp).toBeDefined();
// Token should expire in approximately 2 hours (7200 seconds)
const expectedExp = Math.floor(Date.now() / 1000) + 7200;
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
// Cleanup
delete process.env.JWT_EXPIRATION;
});
it('should parse JWT_EXPIRATION in days format', () => {
// Arrange
delete process.env.JWT_EXPIRY;
process.env.JWT_EXPIRATION = '7d';
// Act
const newJwtService = new JWTService();
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
const token = newJwtService.create(payload, mockResponse as Response);
// Update mock request with the created token
mockRequest.cookies = { auth_token: token };
const verifiedPayload = newJwtService.verify(mockRequest as Request);
// Assert
expect(token).toBeDefined();
expect(verifiedPayload).toBeDefined();
// Token should expire in approximately 7 days (604800 seconds)
const expectedExp = Math.floor(Date.now() / 1000) + 604800;
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
// Cleanup
delete process.env.JWT_EXPIRATION;
});
it('should parse JWT_EXPIRATION in minutes format', () => {
// Arrange
delete process.env.JWT_EXPIRY;
process.env.JWT_EXPIRATION = '30m';
// Act
const newJwtService = new JWTService();
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
const token = newJwtService.create(payload, mockResponse as Response);
// Update mock request with the created token
mockRequest.cookies = { auth_token: token };
const verifiedPayload = newJwtService.verify(mockRequest as Request);
// Assert
expect(token).toBeDefined();
expect(verifiedPayload).toBeDefined();
// Token should expire in approximately 30 minutes (1800 seconds)
const expectedExp = Math.floor(Date.now() / 1000) + 1800;
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
// Cleanup
delete process.env.JWT_EXPIRATION;
});
it('should prioritize JWT_EXPIRY over JWT_EXPIRATION when both are set', () => {
// Arrange
process.env.JWT_EXPIRY = '1800'; // 30 minutes in seconds
process.env.JWT_EXPIRATION = '1h'; // 1 hour
// Act
const newJwtService = new JWTService();
const payload: TokenPayload = {
userId: 'user-123',
authLevel: 1 as const,
userStatus: UserState.VERIFIED_REGULAR,
orgId: 'org-456'
};
const token = newJwtService.create(payload, mockResponse as Response);
// Update mock request with the created token
mockRequest.cookies = { auth_token: token };
const verifiedPayload = newJwtService.verify(mockRequest as Request);
// Assert
expect(token).toBeDefined();
expect(verifiedPayload).toBeDefined();
// Should use JWT_EXPIRY (1800 seconds), not JWT_EXPIRATION (3600 seconds)
const expectedExp = Math.floor(Date.now() / 1000) + 1800;
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
// Cleanup
delete process.env.JWT_EXPIRY;
delete process.env.JWT_EXPIRATION;
});
it('should throw error for invalid JWT_EXPIRATION format', () => {
// Arrange
delete process.env.JWT_EXPIRY;
process.env.JWT_EXPIRATION = 'invalid-format';
// Act & Assert
expect(() => {
new JWTService();
}).toThrow('Invalid duration format: invalid-format. Use format like \'24h\', \'7d\', \'30m\'');
// Cleanup
delete process.env.JWT_EXPIRATION;
});
});
});