UNPKG

matterbridge-roborock-vacuum-plugin

Version:
285 lines 14.4 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { VERIFICATION_CODE_RATE_LIMIT_MS } from '../../../constants/index.js'; import { AuthenticationError } from '../../../errors/index.js'; import { VerificationCodeService } from '../../../services/authentication/VerificationCodeService.js'; import { asPartial, createMockLogger } from '../../testUtils.js'; describe('VerificationCodeService', () => { let service; let mockAuthGateway; let mockStateRepository; let mockLogger; let dateNowSpy; const FIXED_NOW = 1000000000000; beforeEach(() => { mockLogger = createMockLogger(); mockAuthGateway = asPartial({ requestVerificationCode: vi.fn(), authenticate2FA: vi.fn(), authenticatePassword: vi.fn(), refreshToken: vi.fn(), }); mockStateRepository = asPartial({ getAuthState: vi.fn(), saveAuthState: vi.fn(), clearAuthState: vi.fn(), }); service = new VerificationCodeService(mockAuthGateway, mockStateRepository, mockLogger); dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(FIXED_NOW); vi.clearAllMocks(); }); afterEach(() => { dateNowSpy.mockRestore(); }); describe('requestVerificationCode', () => { it('should successfully request verification code', async () => { vi.mocked(mockAuthGateway.requestVerificationCode).mockResolvedValue(); await service.requestVerificationCode('test@example.com'); expect(mockAuthGateway.requestVerificationCode).toHaveBeenCalledWith('test@example.com'); expect(mockLogger.debug).toHaveBeenCalledWith('Requesting verification code for email:', 'test@example.com'); }); it('should throw AuthenticationError when gateway fails', async () => { const originalError = new Error('Network error'); vi.mocked(mockAuthGateway.requestVerificationCode).mockRejectedValue(originalError); await expect(service.requestVerificationCode('test@example.com')).rejects.toThrow(AuthenticationError); expect(mockLogger.error).toHaveBeenCalledWith('Failed to request verification code:', { email: 'test@example.com', error: originalError, }); }); it('should include email in thrown error context', async () => { vi.mocked(mockAuthGateway.requestVerificationCode).mockRejectedValue(new Error('API error')); const promise = service.requestVerificationCode('test@example.com'); await expect(promise).rejects.toThrow(AuthenticationError); const error = await promise.catch((e) => e); expect(error.metadata).toEqual({ email: 'test@example.com', originalError: 'API error', }); }); it('should include original error message in thrown error', async () => { vi.mocked(mockAuthGateway.requestVerificationCode).mockRejectedValue(new Error('Timeout')); const promise = service.requestVerificationCode('test@example.com'); await expect(promise).rejects.toThrow(AuthenticationError); const error = await promise.catch((e) => e); expect(error.message).toBe('Failed to send verification code. Please check your email address and try again.'); expect(error.metadata?.originalError).toBe('Timeout'); }); it('should handle non-Error objects in failures', async () => { vi.mocked(mockAuthGateway.requestVerificationCode).mockRejectedValue('String error'); const promise = service.requestVerificationCode('test@example.com'); await expect(promise).rejects.toThrow(AuthenticationError); const error = await promise.catch((e) => e); expect(error.metadata?.originalError).toBe('String error'); }); }); describe('isRateLimited', () => { it('should return false when no auth state exists', async () => { vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(undefined); const result = await service.isRateLimited(); expect(result).toBe(false); expect(mockStateRepository.getAuthState).toHaveBeenCalled(); }); it('should return false when codeRequestedAt is undefined', async () => { const authState = { email: 'test@example.com', codeRequestedAt: undefined, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.isRateLimited(); expect(result).toBe(false); }); it('should return false when rate limit period has expired', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - VERIFICATION_CODE_RATE_LIMIT_MS - 1000, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.isRateLimited(); expect(result).toBe(false); }); it('should return false when rate limit period has exactly expired', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - VERIFICATION_CODE_RATE_LIMIT_MS, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.isRateLimited(); expect(result).toBe(false); }); it('should return true when within rate limit period', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - 5 * 60 * 1000, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.isRateLimited(); expect(result).toBe(true); }); it('should return true when just requested', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.isRateLimited(); expect(result).toBe(true); }); it('should return true when one millisecond before expiry', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - VERIFICATION_CODE_RATE_LIMIT_MS + 1, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.isRateLimited(); expect(result).toBe(true); }); }); describe('getRemainingWaitTime', () => { it('should return 0 when no auth state exists', async () => { vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(undefined); const result = await service.getRemainingWaitTime(); expect(result).toBe(0); expect(mockStateRepository.getAuthState).toHaveBeenCalled(); }); it('should return 0 when codeRequestedAt is undefined', async () => { const authState = { email: 'test@example.com', codeRequestedAt: undefined, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(0); }); it('should return 0 when rate limit period has expired', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - VERIFICATION_CODE_RATE_LIMIT_MS - 1000, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(0); }); it('should return 0 when rate limit period has exactly expired', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - VERIFICATION_CODE_RATE_LIMIT_MS, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(0); }); it('should return correct remaining seconds when within rate limit period', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - 5 * 60 * 1000, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(600); }); it('should return full rate limit period when just requested', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(900); }); it('should round up to nearest second', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - 500, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(900); }); it('should round up fractional seconds', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - 14 * 60 * 1000 - 500, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(60); }); it('should return 1 second when less than 1 second remaining', async () => { const authState = { email: 'test@example.com', codeRequestedAt: FIXED_NOW - VERIFICATION_CODE_RATE_LIMIT_MS + 999, }; vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(authState); const result = await service.getRemainingWaitTime(); expect(result).toBe(1); }); }); describe('recordCodeRequest', () => { it('should save auth state with current timestamp', async () => { vi.mocked(mockStateRepository.saveAuthState).mockResolvedValue(); await service.recordCodeRequest('test@example.com'); expect(mockStateRepository.saveAuthState).toHaveBeenCalledWith({ email: 'test@example.com', codeRequestedAt: FIXED_NOW, }); }); it('should save auth state with email', async () => { vi.mocked(mockStateRepository.saveAuthState).mockResolvedValue(); await service.recordCodeRequest('user@test.com'); expect(mockStateRepository.saveAuthState).toHaveBeenCalledWith(expect.objectContaining({ email: 'user@test.com', })); }); it('should use Date.now() for timestamp', async () => { vi.mocked(mockStateRepository.saveAuthState).mockResolvedValue(); await service.recordCodeRequest('test@example.com'); expect(mockStateRepository.saveAuthState).toHaveBeenCalledWith(expect.objectContaining({ codeRequestedAt: FIXED_NOW, })); expect(dateNowSpy).toHaveBeenCalled(); }); }); describe('clearCodeRequestState', () => { it('should call clearAuthState on repository', async () => { vi.mocked(mockStateRepository.clearAuthState).mockResolvedValue(); await service.clearCodeRequestState(); expect(mockStateRepository.clearAuthState).toHaveBeenCalled(); }); it('should propagate repository errors', async () => { const error = new Error('Clear failed'); vi.mocked(mockStateRepository.clearAuthState).mockRejectedValue(error); await expect(service.clearCodeRequestState()).rejects.toThrow('Clear failed'); }); }); describe('integration scenarios', () => { it('should handle complete request flow', async () => { vi.mocked(mockAuthGateway.requestVerificationCode).mockResolvedValue(); vi.mocked(mockStateRepository.saveAuthState).mockResolvedValue(); await service.requestVerificationCode('test@example.com'); await service.recordCodeRequest('test@example.com'); expect(mockAuthGateway.requestVerificationCode).toHaveBeenCalledWith('test@example.com'); expect(mockStateRepository.saveAuthState).toHaveBeenCalledWith({ email: 'test@example.com', codeRequestedAt: FIXED_NOW, }); }); it('should properly check rate limit after recording request', async () => { vi.mocked(mockStateRepository.saveAuthState).mockResolvedValue(); vi.mocked(mockStateRepository.getAuthState).mockResolvedValue({ email: 'test@example.com', codeRequestedAt: FIXED_NOW, }); await service.recordCodeRequest('test@example.com'); const isLimited = await service.isRateLimited(); expect(isLimited).toBe(true); }); it('should allow request after clearing state', async () => { vi.mocked(mockStateRepository.clearAuthState).mockResolvedValue(); vi.mocked(mockStateRepository.getAuthState).mockResolvedValue(undefined); await service.clearCodeRequestState(); const isLimited = await service.isRateLimited(); expect(isLimited).toBe(false); }); }); }); //# sourceMappingURL=VerificationCodeService.test.js.map