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; let mockResponse: Partial; 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; }); }); });