import { describe, expect, it, vi } from 'vitest'; import { NotificationService, SendNotificationCommand, NotificationData, NotificationAction, } from './NotificationService'; describe('NotificationService - Interface Contract', () => { it('NotificationService interface defines sendNotification method', () => { const mockService: NotificationService = { sendNotification: vi.fn().mockResolvedValue(undefined), }; expect(mockService.sendNotification).toBeDefined(); expect(typeof mockService.sendNotification).toBe('function'); }); it('SendNotificationCommand has required properties', () => { const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement', title: 'Test Notification', body: 'This is a test notification', channel: 'in_app', urgency: 'toast', }; expect(command).toHaveProperty('recipientId'); expect(command).toHaveProperty('type'); expect(command).toHaveProperty('title'); expect(command).toHaveProperty('body'); expect(command).toHaveProperty('channel'); expect(command).toHaveProperty('urgency'); }); it('SendNotificationCommand can have optional data', () => { const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'race_results_posted', title: 'Race Results', body: 'Your race results are available', channel: 'email', urgency: 'toast', data: { raceEventId: 'event-123', sessionId: 'session-456', position: 5, positionChange: 2, }, }; expect(command.data).toBeDefined(); expect(command.data?.raceEventId).toBe('event-123'); expect(command.data?.position).toBe(5); }); it('SendNotificationCommand can have optional actionUrl', () => { const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'protest_vote_required', title: 'Vote Required', body: 'You need to vote on a protest', channel: 'in_app', urgency: 'modal', actionUrl: '/protests/vote/123', }; expect(command.actionUrl).toBe('/protests/vote/123'); }); it('SendNotificationCommand can have optional actions array', () => { const actions: NotificationAction[] = [ { label: 'View Details', type: 'primary', href: '/protests/123', }, { label: 'Dismiss', type: 'secondary', actionId: 'dismiss', }, ]; const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'protest_filed', title: 'Protest Filed', body: 'A protest has been filed against you', channel: 'in_app', urgency: 'modal', actions, }; expect(command.actions).toBeDefined(); expect(command.actions?.length).toBe(2); expect(command.actions?.[0].label).toBe('View Details'); expect(command.actions?.[1].type).toBe('secondary'); }); it('SendNotificationCommand can have optional requiresResponse', () => { const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'protest_vote_required', title: 'Vote Required', body: 'You need to vote on a protest', channel: 'in_app', urgency: 'modal', requiresResponse: true, }; expect(command.requiresResponse).toBe(true); }); it('NotificationData can have various optional fields', () => { const data: NotificationData = { raceEventId: 'event-123', sessionId: 'session-456', leagueId: 'league-789', position: 3, positionChange: 1, incidents: 2, provisionalRatingChange: 15, finalRatingChange: 10, hadPenaltiesApplied: true, deadline: new Date('2024-01-01'), protestId: 'protest-999', customField: 'custom value', }; expect(data.raceEventId).toBe('event-123'); expect(data.sessionId).toBe('session-456'); expect(data.leagueId).toBe('league-789'); expect(data.position).toBe(3); expect(data.positionChange).toBe(1); expect(data.incidents).toBe(2); expect(data.provisionalRatingChange).toBe(15); expect(data.finalRatingChange).toBe(10); expect(data.hadPenaltiesApplied).toBe(true); expect(data.deadline).toBeInstanceOf(Date); expect(data.protestId).toBe('protest-999'); expect(data.customField).toBe('custom value'); }); it('NotificationData can have minimal fields', () => { const data: NotificationData = { raceEventId: 'event-123', }; expect(data.raceEventId).toBe('event-123'); }); it('NotificationAction has required properties', () => { const action: NotificationAction = { label: 'View Details', type: 'primary', }; expect(action).toHaveProperty('label'); expect(action).toHaveProperty('type'); }); it('NotificationAction can have optional href', () => { const action: NotificationAction = { label: 'View Details', type: 'primary', href: '/protests/123', }; expect(action.href).toBe('/protests/123'); }); it('NotificationAction can have optional actionId', () => { const action: NotificationAction = { label: 'Dismiss', type: 'secondary', actionId: 'dismiss', }; expect(action.actionId).toBe('dismiss'); }); it('NotificationAction type can be primary, secondary, or danger', () => { const primaryAction: NotificationAction = { label: 'Accept', type: 'primary', }; const secondaryAction: NotificationAction = { label: 'Cancel', type: 'secondary', }; const dangerAction: NotificationAction = { label: 'Delete', type: 'danger', }; expect(primaryAction.type).toBe('primary'); expect(secondaryAction.type).toBe('secondary'); expect(dangerAction.type).toBe('danger'); }); }); describe('NotificationService - Integration', () => { it('service can send notification with all optional fields', async () => { const mockService: NotificationService = { sendNotification: vi.fn().mockResolvedValue(undefined), }; const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'race_performance_summary', title: 'Performance Summary', body: 'Your performance summary is ready', channel: 'email', urgency: 'toast', data: { raceEventId: 'event-123', sessionId: 'session-456', position: 5, positionChange: 2, incidents: 1, provisionalRatingChange: 10, finalRatingChange: 8, hadPenaltiesApplied: false, }, actionUrl: '/performance/summary/123', actions: [ { label: 'View Details', type: 'primary', href: '/performance/summary/123', }, { label: 'Dismiss', type: 'secondary', actionId: 'dismiss', }, ], requiresResponse: false, }; await mockService.sendNotification(command); expect(mockService.sendNotification).toHaveBeenCalledWith(command); }); it('service can send notification with minimal fields', async () => { const mockService: NotificationService = { sendNotification: vi.fn().mockResolvedValue(undefined), }; const command: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement', title: 'System Update', body: 'System will be down for maintenance', channel: 'in_app', urgency: 'toast', }; await mockService.sendNotification(command); expect(mockService.sendNotification).toHaveBeenCalledWith(command); }); it('service can send notification with different urgency levels', async () => { const mockService: NotificationService = { sendNotification: vi.fn().mockResolvedValue(undefined), }; const silentCommand: SendNotificationCommand = { recipientId: 'driver-1', type: 'race_reminder', title: 'Race Reminder', body: 'Your race starts in 30 minutes', channel: 'in_app', urgency: 'silent', }; const toastCommand: SendNotificationCommand = { recipientId: 'driver-1', type: 'league_invite', title: 'League Invite', body: 'You have been invited to a league', channel: 'in_app', urgency: 'toast', }; const modalCommand: SendNotificationCommand = { recipientId: 'driver-1', type: 'protest_vote_required', title: 'Vote Required', body: 'You need to vote on a protest', channel: 'in_app', urgency: 'modal', }; await mockService.sendNotification(silentCommand); await mockService.sendNotification(toastCommand); await mockService.sendNotification(modalCommand); expect(mockService.sendNotification).toHaveBeenCalledTimes(3); }); it('service can send notification through different channels', async () => { const mockService: NotificationService = { sendNotification: vi.fn().mockResolvedValue(undefined), }; const inAppCommand: SendNotificationCommand = { recipientId: 'driver-1', type: 'system_announcement', title: 'System Update', body: 'System will be down for maintenance', channel: 'in_app', urgency: 'toast', }; const emailCommand: SendNotificationCommand = { recipientId: 'driver-1', type: 'race_results_posted', title: 'Race Results', body: 'Your race results are available', channel: 'email', urgency: 'toast', }; const discordCommand: SendNotificationCommand = { recipientId: 'driver-1', type: 'sponsorship_request_received', title: 'Sponsorship Request', body: 'A sponsor wants to sponsor you', channel: 'discord', urgency: 'toast', }; await mockService.sendNotification(inAppCommand); await mockService.sendNotification(emailCommand); await mockService.sendNotification(discordCommand); expect(mockService.sendNotification).toHaveBeenCalledTimes(3); }); });