import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest'; import { ResetPasswordUseCase } from './ResetPasswordUseCase'; import { EmailAddress } from '../../domain/value-objects/EmailAddress'; import { UserId } from '../../domain/value-objects/UserId'; import { User } from '../../domain/entities/User'; import type { IAuthRepository } from '../../domain/repositories/IAuthRepository'; import type { IMagicLinkRepository } from '../../domain/repositories/IMagicLinkRepository'; import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; type ResetPasswordOutput = { message: string; }; describe('ResetPasswordUseCase', () => { let authRepo: { findByEmail: Mock; save: Mock; }; let magicLinkRepo: { findByToken: Mock; markAsUsed: Mock; }; let passwordService: { hash: Mock; }; let logger: Logger; let output: UseCaseOutputPort & { present: Mock }; let useCase: ResetPasswordUseCase; beforeEach(() => { authRepo = { findByEmail: vi.fn(), save: vi.fn(), }; magicLinkRepo = { findByToken: vi.fn(), markAsUsed: vi.fn(), }; passwordService = { hash: vi.fn(), }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), } as unknown as Logger; output = { present: vi.fn(), }; useCase = new ResetPasswordUseCase( authRepo as unknown as IAuthRepository, magicLinkRepo as unknown as IMagicLinkRepository, passwordService as unknown as IPasswordHashingService, logger, output, ); }); it('should reset password with valid token', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'NewPass123!', }; const user = User.create({ id: UserId.create(), displayName: 'John Smith', email: 'test@example.com', }); const resetRequest = { email: 'test@example.com', token: input.token, expiresAt: new Date(Date.now() + 60000), // 1 minute from now userId: user.getId().value, }; magicLinkRepo.findByToken.mockResolvedValue(resetRequest); authRepo.findByEmail.mockResolvedValue(user); passwordService.hash.mockResolvedValue('hashed-new-password'); const result = await useCase.execute(input); expect(magicLinkRepo.findByToken).toHaveBeenCalledWith(input.token); expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create('test@example.com')); expect(passwordService.hash).toHaveBeenCalledWith(input.newPassword); expect(authRepo.save).toHaveBeenCalled(); expect(magicLinkRepo.markAsUsed).toHaveBeenCalledWith(input.token); expect(output.present).toHaveBeenCalledWith({ message: 'Password reset successfully. You can now log in with your new password.', }); expect(result.isOk()).toBe(true); }); it('should reject invalid token', async () => { const input = { token: 'invalid-token', newPassword: 'NewPass123!', }; magicLinkRepo.findByToken.mockResolvedValue(null); const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('INVALID_TOKEN'); }); it('should reject expired token', async () => { const input = { token: 'expired-token-12345678901234567890123456789012', newPassword: 'NewPass123!', }; const resetRequest = { email: 'test@example.com', token: input.token, expiresAt: new Date(Date.now() - 60000), // 1 minute ago userId: 'user-123', }; magicLinkRepo.findByToken.mockResolvedValue(resetRequest); const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('EXPIRED_TOKEN'); }); it('should reject weak password', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'weak', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('WEAK_PASSWORD'); }); it('should reject password without uppercase', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'newpass123!', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('WEAK_PASSWORD'); }); it('should reject password without number', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'NewPass!', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('WEAK_PASSWORD'); }); it('should reject password shorter than 8 characters', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'New1!', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('WEAK_PASSWORD'); }); it('should handle user no longer exists', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'NewPass123!', }; const resetRequest = { email: 'deleted@example.com', token: input.token, expiresAt: new Date(Date.now() + 60000), userId: 'user-123', }; magicLinkRepo.findByToken.mockResolvedValue(resetRequest); authRepo.findByEmail.mockResolvedValue(null); const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('INVALID_TOKEN'); }); it('should handle token format validation', async () => { const input = { token: 'short', newPassword: 'NewPass123!', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('INVALID_TOKEN'); }); it('should handle repository errors', async () => { const input = { token: 'valid-token-12345678901234567890123456789012', newPassword: 'NewPass123!', }; magicLinkRepo.findByToken.mockRejectedValue(new Error('Database error')); const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const error = result.unwrapErr(); expect(error.code).toBe('REPOSITORY_ERROR'); expect(error.details.message).toContain('Database error'); }); });