import type { Logger } from '@core/shared/domain/Logger'; import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import { SendNotificationUseCase, type SendNotificationCommand, type SendNotificationErrorCode, type SendNotificationResult, } from './SendNotificationUseCase'; import { NotificationPreferenceRepository } from '../../domain/repositories/NotificationPreferenceRepository'; import { NotificationRepository } from '../../domain/repositories/NotificationRepository'; import type { NotificationChannel, NotificationType } from '../../domain/types/NotificationTypes'; import type { NotificationGatewayRegistry } from '../ports/NotificationGateway'; vi.mock('uuid', () => ({ v4: () => 'notif-1', })); describe('SendNotificationUseCase', () => { let notificationRepository: { create: Mock }; let preferenceRepository: { getOrCreateDefault: Mock }; let gatewayRegistry: { send: Mock }; let logger: Logger; let useCase: SendNotificationUseCase; beforeEach(() => { notificationRepository = { create: vi.fn(), }; preferenceRepository = { getOrCreateDefault: vi.fn(), }; gatewayRegistry = { send: vi.fn(), }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), } as unknown as Logger; useCase = new SendNotificationUseCase( notificationRepository as unknown as NotificationRepository, preferenceRepository as unknown as NotificationPreferenceRepository, gatewayRegistry as unknown as NotificationGatewayRegistry, logger, ); }); it('creates but does not deliver when type is disabled', async () => { const preferences = { isTypeEnabled: vi.fn().mockReturnValue(false), getEnabledChannelsForType: vi.fn().mockReturnValue(['email'] as NotificationChannel[]), isInQuietHours: vi.fn().mockReturnValue(false), }; preferenceRepository.getOrCreateDefault.mockResolvedValue(preferences); const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement' as NotificationType, title: 'Hello', body: 'World', }; const result = await useCase.execute(command); expect(result).toBeInstanceOf(Result); expect(result.isOk()).toBe(true); expect(preferenceRepository.getOrCreateDefault).toHaveBeenCalledWith('driver-1'); expect(preferences.isTypeEnabled).toHaveBeenCalledWith('system_announcement'); expect(notificationRepository.create).toHaveBeenCalledTimes(1); expect(gatewayRegistry.send).not.toHaveBeenCalled(); const successResult = result.unwrap(); expect(successResult.deliveryResults).toEqual([]); expect(successResult.notification.channel).toBe('in_app'); expect(successResult.notification.status).toBe('dismissed'); }); it('ensures in_app is used and sends external channels when enabled', async () => { const preferences = { isTypeEnabled: vi.fn().mockReturnValue(true), getEnabledChannelsForType: vi.fn().mockReturnValue(['email'] as NotificationChannel[]), isInQuietHours: vi.fn().mockReturnValue(false), }; preferenceRepository.getOrCreateDefault.mockResolvedValue(preferences); gatewayRegistry.send.mockResolvedValue({ success: true, channel: 'email' as NotificationChannel, attemptedAt: new Date(), externalId: 'email-1', }); const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement' as NotificationType, title: 'Hello', body: 'World', }; const result = await useCase.execute(command); expect(result.isOk()).toBe(true); // in_app notification must be created, email should be sent via gateway expect(notificationRepository.create).toHaveBeenCalledTimes(1); expect(gatewayRegistry.send).toHaveBeenCalledTimes(1); const successResult = result.unwrap(); expect(successResult.notification.channel).toBe('in_app'); expect(successResult.deliveryResults.length).toBe(2); const channels = successResult.deliveryResults.map(r => r.channel).sort(); expect(channels).toEqual(['email', 'in_app']); }); it('filters external channels during quiet hours', async () => { const preferences = { isTypeEnabled: vi.fn().mockReturnValue(true), getEnabledChannelsForType: vi.fn().mockReturnValue(['email', 'discord'] as NotificationChannel[]), isInQuietHours: vi.fn().mockReturnValue(true), }; preferenceRepository.getOrCreateDefault.mockResolvedValue(preferences); const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement' as NotificationType, title: 'Hello', body: 'World', }; const result = await useCase.execute(command); expect(result.isOk()).toBe(true); expect(notificationRepository.create).toHaveBeenCalledTimes(1); expect(gatewayRegistry.send).not.toHaveBeenCalled(); const successResult = result.unwrap(); expect(successResult.deliveryResults.length).toBe(1); expect(successResult.deliveryResults[0]?.channel).toBe('in_app'); }); it('returns REPOSITORY_ERROR when preference repository throws', async () => { preferenceRepository.getOrCreateDefault.mockRejectedValue(new Error('DB error')); const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement' as NotificationType, title: 'Hello', body: 'World', }; const result: Result> = await useCase.execute(command); expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('REPOSITORY_ERROR'); expect(err.details?.message).toBe('DB error'); }); });