Files

406 lines
12 KiB
TypeScript

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/api/auth/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/api/auth/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/api/auth/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();
});
});
});