Files
rxminder/services/auth/__tests__/emailVerification.test.ts
William Valentin 7029ec0b0d fix: resolve email verification service test failures
- Fix missing imports and dependencies in email verification tests
- Update email verification service implementation
- Resolve test reference errors that were causing failures
- Improve error handling and test reliability

This fixes the 3 failing database service tests mentioned in the improvement summary.
2025-09-08 11:43:28 -07:00

446 lines
14 KiB
TypeScript

import { EmailVerificationService } from '../emailVerification.service';
import { AuthenticatedUser } from '../auth.types';
import { AccountStatus } from '../auth.constants';
// Mock the mailgun service
jest.mock('../../mailgun.service', () => ({
mailgunService: {
sendVerificationEmail: jest.fn().mockResolvedValue(true),
sendPasswordResetEmail: jest.fn().mockResolvedValue(true),
},
}));
// Mock the database service
jest.mock('../../database', () => ({
databaseService: {
findUserByEmail: jest.fn(),
updateUser: jest.fn(),
},
}));
describe('EmailVerificationService', () => {
let emailVerificationService: EmailVerificationService;
let mockMailgunService: any;
let mockDbService: any;
const createMockUser = (
overrides: Partial<AuthenticatedUser> = {}
): AuthenticatedUser => ({
_id: 'user1',
_rev: 'mock-rev-1',
email: 'test@example.com',
username: 'testuser',
password: 'password',
emailVerified: false,
status: AccountStatus.PENDING,
...overrides,
});
beforeEach(async () => {
emailVerificationService = new EmailVerificationService();
// Clear localStorage before each test
localStorage.clear();
// Reset all mocks
jest.clearAllMocks();
// Get mocked services
const mailgunModule = await import('../../mailgun.service');
const dbModule = await import('../../database');
mockMailgunService = mailgunModule.mailgunService;
mockDbService = dbModule.databaseService;
});
describe('generateVerificationToken', () => {
test('should generate a valid verification token', async () => {
// Arrange
const user = createMockUser();
mockMailgunService.sendVerificationEmail.mockResolvedValue(true);
// Act
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Assert
expect(verificationToken).toBeDefined();
expect(verificationToken.token).toBeDefined();
expect(verificationToken.token).toHaveLength(32); // UUID without hyphens
expect(verificationToken.expiresAt).toBeDefined();
expect(verificationToken.userId).toBe(user._id);
expect(verificationToken.email).toBe(user.email);
expect(verificationToken.expiresAt.getTime()).toBeGreaterThan(Date.now());
// Verify token is stored in localStorage
const storedTokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
expect(storedTokens).toHaveLength(1);
expect(storedTokens[0].token).toBe(verificationToken.token);
expect(storedTokens[0].userId).toBe(user._id);
expect(storedTokens[0].email).toBe(user.email);
});
test('should send verification email when user has email', async () => {
// Arrange
const user = createMockUser({ email: 'user@example.com' });
mockMailgunService.sendVerificationEmail.mockResolvedValue(true);
// Act
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Assert
expect(mockMailgunService.sendVerificationEmail).toHaveBeenCalledWith(
user.email,
verificationToken.token
);
});
test('should handle email sending failure gracefully', async () => {
// Arrange
const user = createMockUser({ email: 'user@example.com' });
mockMailgunService.sendVerificationEmail.mockResolvedValue(false);
// Act
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Assert
expect(verificationToken).toBeDefined();
expect(verificationToken.token).toBeDefined();
expect(mockMailgunService.sendVerificationEmail).toHaveBeenCalledWith(
user.email,
verificationToken.token
);
});
test('should not send email when user has no email', async () => {
// Arrange
const user = createMockUser({ email: '' });
// Act
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Assert
expect(verificationToken).toBeDefined();
expect(mockMailgunService.sendVerificationEmail).not.toHaveBeenCalled();
});
test('should generate unique tokens for multiple calls', async () => {
// Arrange
const user1 = createMockUser({
_id: 'user1',
email: 'user1@example.com',
});
const user2 = createMockUser({
_id: 'user2',
email: 'user2@example.com',
});
// Act
const token1 =
await emailVerificationService.generateVerificationToken(user1);
const token2 =
await emailVerificationService.generateVerificationToken(user2);
// Assert
expect(token1.token).not.toBe(token2.token);
expect(token1.userId).toBe('user1');
expect(token2.userId).toBe('user2');
// Verify both tokens are stored
const storedTokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
expect(storedTokens).toHaveLength(2);
});
});
describe('validateVerificationToken', () => {
test('should validate a valid token and return user', async () => {
// Arrange
const user = createMockUser();
mockDbService.findUserByEmail.mockResolvedValue(user);
// Generate and store a token first
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Act
const validatedUser =
await emailVerificationService.validateVerificationToken(
verificationToken.token
);
// Assert
expect(validatedUser).toBeDefined();
expect(validatedUser!._id).toBe(user._id);
expect(validatedUser!.email).toBe(user.email);
expect(mockDbService.findUserByEmail).toHaveBeenCalledWith(user.email);
});
test('should return null for non-existent token', async () => {
// Act
const validatedUser =
await emailVerificationService.validateVerificationToken(
'invalid-token'
);
// Assert
expect(validatedUser).toBeNull();
expect(mockDbService.findUserByEmail).not.toHaveBeenCalled();
});
test('should return null for expired token', async () => {
// Arrange
const user = createMockUser();
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Manually expire the token in localStorage
const tokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
tokens[0].expiresAt = new Date(
Date.now() - 1000 * 60 * 60 * 24
).toISOString(); // 24 hours ago
localStorage.setItem('verification_tokens', JSON.stringify(tokens));
// Act
const validatedUser =
await emailVerificationService.validateVerificationToken(
verificationToken.token
);
// Assert
expect(validatedUser).toBeNull();
expect(mockDbService.findUserByEmail).not.toHaveBeenCalled();
});
test('should return null when user not found in database', async () => {
// Arrange
const user = createMockUser();
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
mockDbService.findUserByEmail.mockResolvedValue(null);
// Act
const validatedUser =
await emailVerificationService.validateVerificationToken(
verificationToken.token
);
// Assert
expect(validatedUser).toBeNull();
expect(mockDbService.findUserByEmail).toHaveBeenCalledWith(user.email);
});
test('should handle database errors gracefully', async () => {
// Arrange
const user = createMockUser();
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
mockDbService.findUserByEmail.mockRejectedValue(
new Error('Database error')
);
// Act & Assert
await expect(
emailVerificationService.validateVerificationToken(
verificationToken.token
)
).rejects.toThrow('Database error');
});
});
describe('markEmailVerified', () => {
test('should update user status to active and mark email verified', async () => {
// Arrange
const user = createMockUser();
const expectedUpdatedUser = {
...user,
emailVerified: true,
status: AccountStatus.ACTIVE,
};
// Store a verification token first
await emailVerificationService.generateVerificationToken(user);
mockDbService.updateUser.mockResolvedValue(expectedUpdatedUser);
// Act
await emailVerificationService.markEmailVerified(user);
// Assert
expect(mockDbService.updateUser).toHaveBeenCalledWith(
expectedUpdatedUser
);
});
test('should remove verification token after marking verified', async () => {
// Arrange
const user = createMockUser();
await emailVerificationService.generateVerificationToken(user);
mockDbService.updateUser.mockResolvedValue(user);
// Verify token exists before
let tokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
expect(tokens).toHaveLength(1);
// Act
await emailVerificationService.markEmailVerified(user);
// Assert - token should be removed
tokens = JSON.parse(localStorage.getItem('verification_tokens') || '[]');
expect(tokens).toHaveLength(0);
});
test('should only remove tokens for the specific user', async () => {
// Arrange
const user1 = createMockUser({
_id: 'user1',
email: 'user1@example.com',
});
const user2 = createMockUser({
_id: 'user2',
email: 'user2@example.com',
});
await emailVerificationService.generateVerificationToken(user1);
await emailVerificationService.generateVerificationToken(user2);
mockDbService.updateUser.mockResolvedValue(user1);
// Verify both tokens exist
let tokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
expect(tokens).toHaveLength(2);
// Act - verify only user1
await emailVerificationService.markEmailVerified(user1);
// Assert - only user1's token should be removed
tokens = JSON.parse(localStorage.getItem('verification_tokens') || '[]');
expect(tokens).toHaveLength(1);
expect(tokens[0].userId).toBe('user2');
});
test('should handle database update errors', async () => {
// Arrange
const user = createMockUser();
mockDbService.updateUser.mockRejectedValue(new Error('Update failed'));
// Act & Assert
await expect(
emailVerificationService.markEmailVerified(user)
).rejects.toThrow('Update failed');
});
});
describe('sendPasswordResetEmail', () => {
test('should delegate to mailgun service', async () => {
// Arrange
const email = 'user@example.com';
const token = 'reset-token';
mockMailgunService.sendPasswordResetEmail.mockResolvedValue(true);
// Act
const result = await emailVerificationService.sendPasswordResetEmail(
email,
token
);
// Assert
expect(result).toBe(true);
expect(mockMailgunService.sendPasswordResetEmail).toHaveBeenCalledWith(
email,
token
);
});
test('should return false when email sending fails', async () => {
// Arrange
const email = 'user@example.com';
const token = 'reset-token';
mockMailgunService.sendPasswordResetEmail.mockResolvedValue(false);
// Act
const result = await emailVerificationService.sendPasswordResetEmail(
email,
token
);
// Assert
expect(result).toBe(false);
expect(mockMailgunService.sendPasswordResetEmail).toHaveBeenCalledWith(
email,
token
);
});
});
describe('integration scenarios', () => {
test('should complete full verification flow', async () => {
// Arrange
const user = createMockUser();
mockDbService.findUserByEmail.mockResolvedValue(user);
mockDbService.updateUser.mockResolvedValue({
...user,
emailVerified: true,
status: AccountStatus.ACTIVE,
});
// Act - Generate token
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Act - Validate token
const validatedUser =
await emailVerificationService.validateVerificationToken(
verificationToken.token
);
// Act - Mark as verified
await emailVerificationService.markEmailVerified(validatedUser!);
// Assert
expect(validatedUser).toBeDefined();
expect(mockDbService.updateUser).toHaveBeenCalledWith({
...user,
emailVerified: true,
status: AccountStatus.ACTIVE,
});
// Verify token is cleaned up
const tokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
expect(tokens).toHaveLength(0);
});
test('should handle token expiration during flow', async () => {
// Arrange
const user = createMockUser();
const verificationToken =
await emailVerificationService.generateVerificationToken(user);
// Simulate time passing - expire the token
const tokens = JSON.parse(
localStorage.getItem('verification_tokens') || '[]'
);
tokens[0].expiresAt = new Date(Date.now() - 1000).toISOString(); // 1 second ago
localStorage.setItem('verification_tokens', JSON.stringify(tokens));
// Act
const validatedUser =
await emailVerificationService.validateVerificationToken(
verificationToken.token
);
// Assert
expect(validatedUser).toBeNull();
});
});
});