import { TokenService } from '../../../src/Application/Services/TokenService'; import * as crypto from 'crypto'; // Mock crypto module jest.mock('crypto'); describe('TokenService', () => { let mockRandomBytes: jest.Mock; let mockCreateHash: jest.Mock; let mockHashUpdate: jest.Mock; let mockHashDigest: jest.Mock; let dateSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); // Restore Date mock if it exists if (dateSpy) { dateSpy.mockRestore(); } mockRandomBytes = jest.mocked(crypto.randomBytes); mockHashUpdate = jest.fn().mockReturnThis(); mockHashDigest = jest.fn(); mockCreateHash = jest.fn().mockReturnValue({ update: mockHashUpdate, digest: mockHashDigest }); // Mock crypto.createHash properly jest.mocked(crypto.createHash).mockImplementation(mockCreateHash); }); afterEach(() => { // Clean up Date mock if (dateSpy) { dateSpy.mockRestore(); dateSpy = undefined as any; } }); describe('generateSecureToken', () => { it('should generate a secure token with default length', () => { // Arrange const mockBuffer = { toString: jest.fn().mockReturnValue('abcdef1234567890') }; mockRandomBytes.mockReturnValue(mockBuffer as any); // Act const token = TokenService.generateSecureToken(); // Assert expect(token).toBe('abcdef1234567890'); expect(mockRandomBytes).toHaveBeenCalledWith(32); expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); }); it('should generate a secure token with custom length', () => { // Arrange const mockBuffer = { toString: jest.fn().mockReturnValue('abcdef') }; mockRandomBytes.mockReturnValue(mockBuffer as any); // Act const token = TokenService.generateSecureToken(16); // Assert expect(token).toBe('abcdef'); expect(mockRandomBytes).toHaveBeenCalledWith(16); expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); }); it('should handle crypto errors', () => { // Arrange mockRandomBytes.mockImplementation(() => { throw new Error('Crypto error'); }); // Act & Assert expect(() => TokenService.generateSecureToken()).toThrow('Failed to generate secure token'); expect(mockRandomBytes).toHaveBeenCalledWith(32); }); }); describe('generateVerificationToken', () => { it('should generate verification token with correct expiration', () => { // Arrange const mockBuffer = { toString: jest.fn().mockReturnValue('verification123') }; mockRandomBytes.mockReturnValue(mockBuffer as any); const mockDate = new Date('2023-01-01T12:00:00Z'); dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); // Act const result = TokenService.generateVerificationToken(); // Assert expect(result.token).toBe('verification123'); expect(result.createdAt).toEqual(mockDate); expect(result.expiresAt).toEqual(new Date('2023-01-02T12:00:00Z')); // 24 hours later expect(mockRandomBytes).toHaveBeenCalledWith(32); expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); }); it('should handle token generation errors', () => { // Arrange mockRandomBytes.mockImplementation(() => { throw new Error('Random bytes failed'); }); // Act & Assert expect(() => TokenService.generateVerificationToken()).toThrow('Failed to generate verification token'); }); }); describe('generatePasswordResetToken', () => { it('should generate password reset token with correct expiration', () => { // Arrange const mockBuffer = { toString: jest.fn().mockReturnValue('reset456') }; mockRandomBytes.mockReturnValue(mockBuffer as any); const mockDate = new Date('2023-01-01T12:00:00Z'); dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); // Act const result = TokenService.generatePasswordResetToken(); // Assert expect(result.token).toBe('reset456'); expect(result.createdAt).toEqual(mockDate); expect(result.expiresAt).toEqual(new Date('2023-01-01T13:00:00Z')); // 1 hour later expect(mockRandomBytes).toHaveBeenCalledWith(32); expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); }); it('should handle token generation errors', () => { // Arrange mockRandomBytes.mockImplementation(() => { throw new Error('Random bytes failed'); }); // Act & Assert expect(() => TokenService.generatePasswordResetToken()).toThrow('Failed to generate password reset token'); }); }); describe('hashToken', () => { it('should hash token correctly', async () => { // Arrange const token = 'test-token-123'; const hashedToken = 'hashed-token-result'; mockHashDigest.mockReturnValue(hashedToken); // Act const result = await TokenService.hashToken(token); // Assert expect(result).toBe(hashedToken); expect(mockCreateHash).toHaveBeenCalledWith('sha256'); expect(mockHashUpdate).toHaveBeenCalledWith(token); expect(mockHashDigest).toHaveBeenCalledWith('hex'); }); it('should handle hashing errors', async () => { // Arrange const token = 'test-token-123'; mockCreateHash.mockImplementation(() => { throw new Error('Hashing failed'); }); // Act & Assert await expect(TokenService.hashToken(token)).rejects.toThrow('Failed to hash token'); }); }); describe('verifyToken', () => { it('should return true when tokens match', async () => { // Arrange const plainToken = 'plain-token'; const hashedToken = 'expected-hash'; mockHashDigest.mockReturnValue(hashedToken); // Act const result = await TokenService.verifyToken(plainToken, hashedToken); // Assert expect(result).toBe(true); expect(mockCreateHash).toHaveBeenCalledWith('sha256'); expect(mockHashUpdate).toHaveBeenCalledWith(plainToken); expect(mockHashDigest).toHaveBeenCalledWith('hex'); }); it('should return false when tokens do not match', async () => { // Arrange const plainToken = 'plain-token'; const hashedToken = 'expected-hash'; const actualHash = 'different-hash'; mockHashDigest.mockReturnValue(actualHash); // Act const result = await TokenService.verifyToken(plainToken, hashedToken); // Assert expect(result).toBe(false); expect(mockCreateHash).toHaveBeenCalledWith('sha256'); expect(mockHashUpdate).toHaveBeenCalledWith(plainToken); expect(mockHashDigest).toHaveBeenCalledWith('hex'); }); it('should handle verification errors', async () => { // Arrange const plainToken = 'plain-token'; const hashedToken = 'expected-hash'; mockCreateHash.mockImplementation(() => { throw new Error('Hash creation failed'); }); // Act & Assert - TokenService.verifyToken catches errors and returns false, doesn't throw const result = await TokenService.verifyToken(plainToken, hashedToken); expect(result).toBe(false); }); }); describe('isTokenExpired', () => { it('should return false for non-expired token', () => { // Arrange const currentTime = new Date('2023-01-01T12:00:00Z'); const futureDate = new Date('2023-01-01T13:00:00Z'); // 1 hour from now dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); // Act const result = TokenService.isTokenExpired(futureDate); // Assert expect(result).toBe(false); // Cleanup dateSpy.mockRestore(); }); it('should return true for expired token', () => { // Arrange const currentTime = new Date('2023-01-01T12:00:00Z'); const pastDate = new Date('2023-01-01T11:00:00Z'); // 1 hour ago dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); // Act const result = TokenService.isTokenExpired(pastDate); // Assert expect(result).toBe(true); // Cleanup dateSpy.mockRestore(); }); it('should return true for exactly expired token', () => { // Arrange const currentTime = new Date('2023-01-01T12:00:00Z'); const exactlyNow = new Date('2023-01-01T12:00:00Z'); dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); // Act const result = TokenService.isTokenExpired(exactlyNow); // Assert expect(result).toBe(false); // new Date() > expiresAt is false when they're equal // Cleanup dateSpy.mockRestore(); }); }); describe('generateTokenWithExpiry', () => { it('should validate token format correctly', () => { // Arrange - valid hex token with expected length (64 chars for 32 bytes) const validToken = 'a'.repeat(64); // 64 hex characters // Act const result = TokenService.isValidTokenFormat(validToken); // Assert expect(result).toBe(true); }); it('should reject invalid token format', () => { // Arrange const invalidTokens = [ '', // empty 'invalid-token-with-dashes', // non-hex characters 'abc123', // too short null as any, // null undefined as any, // undefined 123 as any // not string ]; invalidTokens.forEach(invalidToken => { // Act const result = TokenService.isValidTokenFormat(invalidToken); // Assert expect(result).toBe(false); }); }); }); describe('generateVerificationUrl', () => { it('should generate correct verification URL', () => { // Arrange const baseUrl = 'https://example.com'; const token = 'verification-token-123'; // Act const url = TokenService.generateVerificationUrl(baseUrl, token); // Assert expect(url).toBe('https://example.com/verify-email?token=verification-token-123'); }); it('should handle base URL with trailing slash', () => { // Arrange const baseUrl = 'https://example.com/'; const token = 'verification-token-123'; // Act const url = TokenService.generateVerificationUrl(baseUrl, token); // Assert expect(url).toBe('https://example.com/verify-email?token=verification-token-123'); }); it('should encode special characters in token', () => { // Arrange const baseUrl = 'https://example.com'; const token = 'token+with/special=chars'; // Act const url = TokenService.generateVerificationUrl(baseUrl, token); // Assert expect(url).toContain(encodeURIComponent(token)); }); }); describe('generatePasswordResetUrl', () => { it('should generate correct password reset URL', () => { // Arrange const baseUrl = 'https://example.com'; const token = 'reset-token-456'; // Act const url = TokenService.generatePasswordResetUrl(baseUrl, token); // Assert expect(url).toBe('https://example.com/reset-password?token=reset-token-456'); }); }); describe('getExpirationInfo', () => { it('should return correct info for non-expired token', () => { // Arrange const currentTime = new Date('2023-01-01T12:00:00Z'); const futureDate = new Date('2023-01-01T14:00:00Z'); // 2 hours from now dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); // Act const result = TokenService.getExpirationInfo(futureDate); // Assert expect(result.expired).toBe(false); expect(result.timeLeft).toContain('Expires in'); expect(result.timeLeft).toContain('hour(s)'); // Cleanup dateSpy.mockRestore(); }); it('should return correct info for expired token', () => { // Arrange const currentTime = new Date('2023-01-01T12:00:00Z'); const pastDate = new Date('2023-01-01T11:30:00Z'); // 30 minutes ago dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); // Act const result = TokenService.getExpirationInfo(pastDate); // Assert expect(result.expired).toBe(true); expect(result.timeLeft).toContain('Expired'); expect(result.timeLeft).toContain('minute(s) ago'); // Cleanup dateSpy.mockRestore(); }); }); });