core tests
This commit is contained in:
319
core/notifications/application/ports/NotificationGateway.test.ts
Normal file
319
core/notifications/application/ports/NotificationGateway.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Notification } from '../../domain/entities/Notification';
|
||||
import {
|
||||
NotificationGateway,
|
||||
NotificationGatewayRegistry,
|
||||
NotificationDeliveryResult,
|
||||
} from './NotificationGateway';
|
||||
|
||||
describe('NotificationGateway - Interface Contract', () => {
|
||||
it('NotificationGateway interface defines send method', () => {
|
||||
const mockGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('in_app'),
|
||||
};
|
||||
|
||||
const notification = Notification.create({
|
||||
id: 'test-id',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
expect(mockGateway.send).toBeDefined();
|
||||
expect(typeof mockGateway.send).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationGateway interface defines supportsChannel method', () => {
|
||||
const mockGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('in_app'),
|
||||
};
|
||||
|
||||
expect(mockGateway.supportsChannel).toBeDefined();
|
||||
expect(typeof mockGateway.supportsChannel).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationGateway interface defines isConfigured method', () => {
|
||||
const mockGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('in_app'),
|
||||
};
|
||||
|
||||
expect(mockGateway.isConfigured).toBeDefined();
|
||||
expect(typeof mockGateway.isConfigured).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationGateway interface defines getChannel method', () => {
|
||||
const mockGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('in_app'),
|
||||
};
|
||||
|
||||
expect(mockGateway.getChannel).toBeDefined();
|
||||
expect(typeof mockGateway.getChannel).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationDeliveryResult has required properties', () => {
|
||||
const result: NotificationDeliveryResult = {
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
|
||||
expect(result).toHaveProperty('success');
|
||||
expect(result).toHaveProperty('channel');
|
||||
expect(result).toHaveProperty('attemptedAt');
|
||||
});
|
||||
|
||||
it('NotificationDeliveryResult can have optional externalId', () => {
|
||||
const result: NotificationDeliveryResult = {
|
||||
success: true,
|
||||
channel: 'email',
|
||||
externalId: 'email-123',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
|
||||
expect(result.externalId).toBe('email-123');
|
||||
});
|
||||
|
||||
it('NotificationDeliveryResult can have optional error', () => {
|
||||
const result: NotificationDeliveryResult = {
|
||||
success: false,
|
||||
channel: 'discord',
|
||||
error: 'Failed to send to Discord',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
|
||||
expect(result.error).toBe('Failed to send to Discord');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationGatewayRegistry - Interface Contract', () => {
|
||||
it('NotificationGatewayRegistry interface defines register method', () => {
|
||||
const mockRegistry: NotificationGatewayRegistry = {
|
||||
register: vi.fn(),
|
||||
getGateway: vi.fn().mockReturnValue(null),
|
||||
getAllGateways: vi.fn().mockReturnValue([]),
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
};
|
||||
|
||||
expect(mockRegistry.register).toBeDefined();
|
||||
expect(typeof mockRegistry.register).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationGatewayRegistry interface defines getGateway method', () => {
|
||||
const mockRegistry: NotificationGatewayRegistry = {
|
||||
register: vi.fn(),
|
||||
getGateway: vi.fn().mockReturnValue(null),
|
||||
getAllGateways: vi.fn().mockReturnValue([]),
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
};
|
||||
|
||||
expect(mockRegistry.getGateway).toBeDefined();
|
||||
expect(typeof mockRegistry.getGateway).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationGatewayRegistry interface defines getAllGateways method', () => {
|
||||
const mockRegistry: NotificationGatewayRegistry = {
|
||||
register: vi.fn(),
|
||||
getGateway: vi.fn().mockReturnValue(null),
|
||||
getAllGateways: vi.fn().mockReturnValue([]),
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
};
|
||||
|
||||
expect(mockRegistry.getAllGateways).toBeDefined();
|
||||
expect(typeof mockRegistry.getAllGateways).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationGatewayRegistry interface defines send method', () => {
|
||||
const mockRegistry: NotificationGatewayRegistry = {
|
||||
register: vi.fn(),
|
||||
getGateway: vi.fn().mockReturnValue(null),
|
||||
getAllGateways: vi.fn().mockReturnValue([]),
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
};
|
||||
|
||||
expect(mockRegistry.send).toBeDefined();
|
||||
expect(typeof mockRegistry.send).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationGateway - Integration with Notification', () => {
|
||||
it('gateway can send notification and return delivery result', async () => {
|
||||
const mockGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
externalId: 'msg-123',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('in_app'),
|
||||
};
|
||||
|
||||
const notification = Notification.create({
|
||||
id: 'test-id',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const result = await mockGateway.send(notification);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.channel).toBe('in_app');
|
||||
expect(result.externalId).toBe('msg-123');
|
||||
expect(mockGateway.send).toHaveBeenCalledWith(notification);
|
||||
});
|
||||
|
||||
it('gateway can handle failed delivery', async () => {
|
||||
const mockGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
channel: 'email',
|
||||
error: 'SMTP server unavailable',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('email'),
|
||||
};
|
||||
|
||||
const notification = Notification.create({
|
||||
id: 'test-id',
|
||||
recipientId: 'driver-1',
|
||||
type: 'race_registration_open',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'email',
|
||||
});
|
||||
|
||||
const result = await mockGateway.send(notification);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.channel).toBe('email');
|
||||
expect(result.error).toBe('SMTP server unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationGatewayRegistry - Integration', () => {
|
||||
it('registry can route notification to appropriate gateway', async () => {
|
||||
const inAppGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'in_app',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('in_app'),
|
||||
};
|
||||
|
||||
const emailGateway: NotificationGateway = {
|
||||
send: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
channel: 'email',
|
||||
externalId: 'email-456',
|
||||
attemptedAt: new Date(),
|
||||
}),
|
||||
supportsChannel: vi.fn().mockReturnValue(true),
|
||||
isConfigured: vi.fn().mockReturnValue(true),
|
||||
getChannel: vi.fn().mockReturnValue('email'),
|
||||
};
|
||||
|
||||
const mockRegistry: NotificationGatewayRegistry = {
|
||||
register: vi.fn(),
|
||||
getGateway: vi.fn().mockImplementation((channel) => {
|
||||
if (channel === 'in_app') return inAppGateway;
|
||||
if (channel === 'email') return emailGateway;
|
||||
return null;
|
||||
}),
|
||||
getAllGateways: vi.fn().mockReturnValue([inAppGateway, emailGateway]),
|
||||
send: vi.fn().mockImplementation(async (notification) => {
|
||||
const gateway = mockRegistry.getGateway(notification.channel);
|
||||
if (gateway) {
|
||||
return gateway.send(notification);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
channel: notification.channel,
|
||||
error: 'No gateway found',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const inAppNotification = Notification.create({
|
||||
id: 'test-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const emailNotification = Notification.create({
|
||||
id: 'test-2',
|
||||
recipientId: 'driver-1',
|
||||
type: 'race_registration_open',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'email',
|
||||
});
|
||||
|
||||
const inAppResult = await mockRegistry.send(inAppNotification);
|
||||
expect(inAppResult.success).toBe(true);
|
||||
expect(inAppResult.channel).toBe('in_app');
|
||||
|
||||
const emailResult = await mockRegistry.send(emailNotification);
|
||||
expect(emailResult.success).toBe(true);
|
||||
expect(emailResult.channel).toBe('email');
|
||||
expect(emailResult.externalId).toBe('email-456');
|
||||
});
|
||||
});
|
||||
346
core/notifications/application/ports/NotificationService.test.ts
Normal file
346
core/notifications/application/ports/NotificationService.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
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 { Notification } from '../../domain/entities/Notification';
|
||||
import { NotificationRepository } from '../../domain/repositories/NotificationRepository';
|
||||
import {
|
||||
GetAllNotificationsUseCase,
|
||||
type GetAllNotificationsInput,
|
||||
} from './GetAllNotificationsUseCase';
|
||||
|
||||
interface NotificationRepositoryMock {
|
||||
findByRecipientId: Mock;
|
||||
}
|
||||
|
||||
describe('GetAllNotificationsUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let logger: Logger;
|
||||
let useCase: GetAllNotificationsUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationRepository = {
|
||||
findByRecipientId: vi.fn(),
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new GetAllNotificationsUseCase(
|
||||
notificationRepository as unknown as NotificationRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns all notifications and total count for recipient', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
const notifications: Notification[] = [
|
||||
Notification.create({
|
||||
id: 'n1',
|
||||
recipientId,
|
||||
type: 'system_announcement',
|
||||
title: 'Test 1',
|
||||
body: 'Body 1',
|
||||
channel: 'in_app',
|
||||
}),
|
||||
Notification.create({
|
||||
id: 'n2',
|
||||
recipientId,
|
||||
type: 'race_registration_open',
|
||||
title: 'Test 2',
|
||||
body: 'Body 2',
|
||||
channel: 'email',
|
||||
}),
|
||||
];
|
||||
|
||||
notificationRepository.findByRecipientId.mockResolvedValue(notifications);
|
||||
|
||||
const input: GetAllNotificationsInput = { recipientId };
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(notificationRepository.findByRecipientId).toHaveBeenCalledWith(recipientId);
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.notifications).toEqual(notifications);
|
||||
expect(successResult.totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it('returns empty array when no notifications exist', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
notificationRepository.findByRecipientId.mockResolvedValue([]);
|
||||
|
||||
const input: GetAllNotificationsInput = { recipientId };
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(notificationRepository.findByRecipientId).toHaveBeenCalledWith(recipientId);
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.notifications).toEqual([]);
|
||||
expect(successResult.totalCount).toBe(0);
|
||||
});
|
||||
|
||||
it('handles repository errors by logging and returning error result', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
const error = new Error('DB error');
|
||||
notificationRepository.findByRecipientId.mockRejectedValue(error);
|
||||
|
||||
const input: GetAllNotificationsInput = { recipientId };
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>;
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
expect(err.details.message).toBe('DB error');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs debug message when starting execution', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
notificationRepository.findByRecipientId.mockResolvedValue([]);
|
||||
|
||||
const input: GetAllNotificationsInput = { recipientId };
|
||||
|
||||
await useCase.execute(input);
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
`Attempting to retrieve all notifications for recipient ID: ${recipientId}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('logs info message on successful retrieval', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
const notifications: Notification[] = [
|
||||
Notification.create({
|
||||
id: 'n1',
|
||||
recipientId,
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
}),
|
||||
];
|
||||
|
||||
notificationRepository.findByRecipientId.mockResolvedValue(notifications);
|
||||
|
||||
const input: GetAllNotificationsInput = { recipientId };
|
||||
|
||||
await useCase.execute(input);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
`Successfully retrieved 1 notifications for recipient ID: ${recipientId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { NotificationDomainError } from './NotificationDomainError';
|
||||
|
||||
describe('NotificationDomainError', () => {
|
||||
it('creates an error with default validation kind', () => {
|
||||
const error = new NotificationDomainError('Invalid notification data');
|
||||
|
||||
expect(error.name).toBe('NotificationDomainError');
|
||||
expect(error.type).toBe('domain');
|
||||
expect(error.context).toBe('notifications');
|
||||
expect(error.kind).toBe('validation');
|
||||
expect(error.message).toBe('Invalid notification data');
|
||||
});
|
||||
|
||||
it('creates an error with custom kind', () => {
|
||||
const error = new NotificationDomainError('Notification not found', 'not_found');
|
||||
|
||||
expect(error.kind).toBe('not_found');
|
||||
expect(error.message).toBe('Notification not found');
|
||||
});
|
||||
|
||||
it('creates an error with business rule kind', () => {
|
||||
const error = new NotificationDomainError('Cannot send notification during quiet hours', 'business_rule');
|
||||
|
||||
expect(error.kind).toBe('business_rule');
|
||||
expect(error.message).toBe('Cannot send notification during quiet hours');
|
||||
});
|
||||
|
||||
it('creates an error with conflict kind', () => {
|
||||
const error = new NotificationDomainError('Notification already read', 'conflict');
|
||||
|
||||
expect(error.kind).toBe('conflict');
|
||||
expect(error.message).toBe('Notification already read');
|
||||
});
|
||||
|
||||
it('creates an error with unauthorized kind', () => {
|
||||
const error = new NotificationDomainError('Cannot access notification', 'unauthorized');
|
||||
|
||||
expect(error.kind).toBe('unauthorized');
|
||||
expect(error.message).toBe('Cannot access notification');
|
||||
});
|
||||
|
||||
it('inherits from Error', () => {
|
||||
const error = new NotificationDomainError('Test error');
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.stack).toBeDefined();
|
||||
});
|
||||
|
||||
it('has correct error properties', () => {
|
||||
const error = new NotificationDomainError('Test error', 'validation');
|
||||
|
||||
expect(error.name).toBe('NotificationDomainError');
|
||||
expect(error.type).toBe('domain');
|
||||
expect(error.context).toBe('notifications');
|
||||
expect(error.kind).toBe('validation');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { NotificationPreference } from '../entities/NotificationPreference';
|
||||
import { NotificationPreferenceRepository } from './NotificationPreferenceRepository';
|
||||
|
||||
describe('NotificationPreferenceRepository - Interface Contract', () => {
|
||||
it('NotificationPreferenceRepository interface defines findByDriverId method', () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
expect(mockRepository.findByDriverId).toBeDefined();
|
||||
expect(typeof mockRepository.findByDriverId).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationPreferenceRepository interface defines save method', () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
expect(mockRepository.save).toBeDefined();
|
||||
expect(typeof mockRepository.save).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationPreferenceRepository interface defines delete method', () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
expect(mockRepository.delete).toBeDefined();
|
||||
expect(typeof mockRepository.delete).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationPreferenceRepository interface defines getOrCreateDefault method', () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
expect(mockRepository.getOrCreateDefault).toBeDefined();
|
||||
expect(typeof mockRepository.getOrCreateDefault).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationPreferenceRepository - Integration', () => {
|
||||
it('can find preferences by driver ID', async () => {
|
||||
const mockPreference = NotificationPreference.create({
|
||||
id: 'driver-1',
|
||||
driverId: 'driver-1',
|
||||
channels: {
|
||||
in_app: { enabled: true },
|
||||
email: { enabled: true },
|
||||
discord: { enabled: false },
|
||||
push: { enabled: false },
|
||||
},
|
||||
quietHoursStart: 22,
|
||||
quietHoursEnd: 7,
|
||||
});
|
||||
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(mockPreference),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue(mockPreference),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findByDriverId('driver-1');
|
||||
|
||||
expect(result).toBe(mockPreference);
|
||||
expect(mockRepository.findByDriverId).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('returns null when preferences not found', async () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findByDriverId('driver-999');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockRepository.findByDriverId).toHaveBeenCalledWith('driver-999');
|
||||
});
|
||||
|
||||
it('can save preferences', async () => {
|
||||
const mockPreference = NotificationPreference.create({
|
||||
id: 'driver-1',
|
||||
driverId: 'driver-1',
|
||||
channels: {
|
||||
in_app: { enabled: true },
|
||||
email: { enabled: true },
|
||||
discord: { enabled: false },
|
||||
push: { enabled: false },
|
||||
},
|
||||
quietHoursStart: 22,
|
||||
quietHoursEnd: 7,
|
||||
});
|
||||
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(mockPreference),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue(mockPreference),
|
||||
};
|
||||
|
||||
await mockRepository.save(mockPreference);
|
||||
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(mockPreference);
|
||||
});
|
||||
|
||||
it('can delete preferences by driver ID', async () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
await mockRepository.delete('driver-1');
|
||||
|
||||
expect(mockRepository.delete).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('can get or create default preferences', async () => {
|
||||
const defaultPreference = NotificationPreference.createDefault('driver-1');
|
||||
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue(defaultPreference),
|
||||
};
|
||||
|
||||
const result = await mockRepository.getOrCreateDefault('driver-1');
|
||||
|
||||
expect(result).toBe(defaultPreference);
|
||||
expect(mockRepository.getOrCreateDefault).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('handles workflow: find, update, save', async () => {
|
||||
const existingPreference = NotificationPreference.create({
|
||||
id: 'driver-1',
|
||||
driverId: 'driver-1',
|
||||
channels: {
|
||||
in_app: { enabled: true },
|
||||
email: { enabled: false },
|
||||
discord: { enabled: false },
|
||||
push: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
const updatedPreference = NotificationPreference.create({
|
||||
id: 'driver-1',
|
||||
driverId: 'driver-1',
|
||||
channels: {
|
||||
in_app: { enabled: true },
|
||||
email: { enabled: true },
|
||||
discord: { enabled: true },
|
||||
push: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn()
|
||||
.mockResolvedValueOnce(existingPreference)
|
||||
.mockResolvedValueOnce(updatedPreference),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue(existingPreference),
|
||||
};
|
||||
|
||||
// Find existing preferences
|
||||
const found = await mockRepository.findByDriverId('driver-1');
|
||||
expect(found).toBe(existingPreference);
|
||||
|
||||
// Update preferences
|
||||
const updated = found!.updateChannel('email', { enabled: true });
|
||||
const updated2 = updated.updateChannel('discord', { enabled: true });
|
||||
|
||||
// Save updated preferences
|
||||
await mockRepository.save(updated2);
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(updated2);
|
||||
|
||||
// Verify update
|
||||
const updatedFound = await mockRepository.findByDriverId('driver-1');
|
||||
expect(updatedFound).toBe(updatedPreference);
|
||||
});
|
||||
|
||||
it('handles workflow: get or create, then update', async () => {
|
||||
const defaultPreference = NotificationPreference.createDefault('driver-1');
|
||||
|
||||
const updatedPreference = NotificationPreference.create({
|
||||
id: 'driver-1',
|
||||
driverId: 'driver-1',
|
||||
channels: {
|
||||
in_app: { enabled: true },
|
||||
email: { enabled: true },
|
||||
discord: { enabled: false },
|
||||
push: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue(defaultPreference),
|
||||
};
|
||||
|
||||
// Get or create default preferences
|
||||
const preferences = await mockRepository.getOrCreateDefault('driver-1');
|
||||
expect(preferences).toBe(defaultPreference);
|
||||
|
||||
// Update preferences
|
||||
const updated = preferences.updateChannel('email', { enabled: true });
|
||||
|
||||
// Save updated preferences
|
||||
await mockRepository.save(updated);
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(updated);
|
||||
});
|
||||
|
||||
it('handles workflow: delete preferences', async () => {
|
||||
const mockRepository: NotificationPreferenceRepository = {
|
||||
findByDriverId: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
getOrCreateDefault: vi.fn().mockResolvedValue({} as NotificationPreference),
|
||||
};
|
||||
|
||||
// Delete preferences
|
||||
await mockRepository.delete('driver-1');
|
||||
expect(mockRepository.delete).toHaveBeenCalledWith('driver-1');
|
||||
|
||||
// Verify deletion
|
||||
const result = await mockRepository.findByDriverId('driver-1');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,539 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Notification } from '../entities/Notification';
|
||||
import { NotificationRepository } from './NotificationRepository';
|
||||
|
||||
describe('NotificationRepository - Interface Contract', () => {
|
||||
it('NotificationRepository interface defines findById method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.findById).toBeDefined();
|
||||
expect(typeof mockRepository.findById).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines findByRecipientId method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.findByRecipientId).toBeDefined();
|
||||
expect(typeof mockRepository.findByRecipientId).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines findUnreadByRecipientId method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.findUnreadByRecipientId).toBeDefined();
|
||||
expect(typeof mockRepository.findUnreadByRecipientId).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines findByRecipientIdAndType method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.findByRecipientIdAndType).toBeDefined();
|
||||
expect(typeof mockRepository.findByRecipientIdAndType).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines countUnreadByRecipientId method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.countUnreadByRecipientId).toBeDefined();
|
||||
expect(typeof mockRepository.countUnreadByRecipientId).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines create method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.create).toBeDefined();
|
||||
expect(typeof mockRepository.create).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines update method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.update).toBeDefined();
|
||||
expect(typeof mockRepository.update).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines delete method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.delete).toBeDefined();
|
||||
expect(typeof mockRepository.delete).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines deleteAllByRecipientId method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.deleteAllByRecipientId).toBeDefined();
|
||||
expect(typeof mockRepository.deleteAllByRecipientId).toBe('function');
|
||||
});
|
||||
|
||||
it('NotificationRepository interface defines markAllAsReadByRecipientId method', () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
expect(mockRepository.markAllAsReadByRecipientId).toBeDefined();
|
||||
expect(typeof mockRepository.markAllAsReadByRecipientId).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationRepository - Integration', () => {
|
||||
it('can find notification by ID', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(notification),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([notification]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([notification]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([notification]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(1),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findById('notification-1');
|
||||
|
||||
expect(result).toBe(notification);
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith('notification-1');
|
||||
});
|
||||
|
||||
it('returns null when notification not found by ID', async () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findById('notification-999');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith('notification-999');
|
||||
});
|
||||
|
||||
it('can find all notifications for a recipient', async () => {
|
||||
const notifications = [
|
||||
Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test 1',
|
||||
body: 'Body 1',
|
||||
channel: 'in_app',
|
||||
}),
|
||||
Notification.create({
|
||||
id: 'notification-2',
|
||||
recipientId: 'driver-1',
|
||||
type: 'race_registration_open',
|
||||
title: 'Test 2',
|
||||
body: 'Body 2',
|
||||
channel: 'email',
|
||||
}),
|
||||
];
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue(notifications),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue(notifications),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue(notifications),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(2),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findByRecipientId('driver-1');
|
||||
|
||||
expect(result).toBe(notifications);
|
||||
expect(mockRepository.findByRecipientId).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('can find unread notifications for a recipient', async () => {
|
||||
const unreadNotifications = [
|
||||
Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test 1',
|
||||
body: 'Body 1',
|
||||
channel: 'in_app',
|
||||
}),
|
||||
];
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue(unreadNotifications),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue(unreadNotifications),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(1),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findUnreadByRecipientId('driver-1');
|
||||
|
||||
expect(result).toBe(unreadNotifications);
|
||||
expect(mockRepository.findUnreadByRecipientId).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('can find notifications by type for a recipient', async () => {
|
||||
const protestNotifications = [
|
||||
Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'protest_filed',
|
||||
title: 'Protest Filed',
|
||||
body: 'A protest has been filed',
|
||||
channel: 'in_app',
|
||||
}),
|
||||
];
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue(protestNotifications),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const result = await mockRepository.findByRecipientIdAndType('driver-1', 'protest_filed');
|
||||
|
||||
expect(result).toBe(protestNotifications);
|
||||
expect(mockRepository.findByRecipientIdAndType).toHaveBeenCalledWith('driver-1', 'protest_filed');
|
||||
});
|
||||
|
||||
it('can count unread notifications for a recipient', async () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(3),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const count = await mockRepository.countUnreadByRecipientId('driver-1');
|
||||
|
||||
expect(count).toBe(3);
|
||||
expect(mockRepository.countUnreadByRecipientId).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('can create a new notification', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await mockRepository.create(notification);
|
||||
|
||||
expect(mockRepository.create).toHaveBeenCalledWith(notification);
|
||||
});
|
||||
|
||||
it('can update an existing notification', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(notification),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([notification]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([notification]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([notification]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(1),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await mockRepository.update(notification);
|
||||
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(notification);
|
||||
});
|
||||
|
||||
it('can delete a notification by ID', async () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await mockRepository.delete('notification-1');
|
||||
|
||||
expect(mockRepository.delete).toHaveBeenCalledWith('notification-1');
|
||||
});
|
||||
|
||||
it('can delete all notifications for a recipient', async () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await mockRepository.deleteAllByRecipientId('driver-1');
|
||||
|
||||
expect(mockRepository.deleteAllByRecipientId).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('can mark all notifications as read for a recipient', async () => {
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn().mockResolvedValue(null),
|
||||
findByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findUnreadByRecipientId: vi.fn().mockResolvedValue([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await mockRepository.markAllAsReadByRecipientId('driver-1');
|
||||
|
||||
expect(mockRepository.markAllAsReadByRecipientId).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
|
||||
it('handles workflow: create, find, update, delete', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const updatedNotification = Notification.create({
|
||||
id: 'notification-1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Updated Test',
|
||||
body: 'Updated body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
const mockRepository: NotificationRepository = {
|
||||
findById: vi.fn()
|
||||
.mockResolvedValueOnce(notification)
|
||||
.mockResolvedValueOnce(updatedNotification)
|
||||
.mockResolvedValueOnce(null),
|
||||
findByRecipientId: vi.fn()
|
||||
.mockResolvedValueOnce([notification])
|
||||
.mockResolvedValueOnce([updatedNotification])
|
||||
.mockResolvedValueOnce([]),
|
||||
findUnreadByRecipientId: vi.fn()
|
||||
.mockResolvedValueOnce([notification])
|
||||
.mockResolvedValueOnce([updatedNotification])
|
||||
.mockResolvedValueOnce([]),
|
||||
findByRecipientIdAndType: vi.fn().mockResolvedValue([]),
|
||||
countUnreadByRecipientId: vi.fn()
|
||||
.mockResolvedValueOnce(1)
|
||||
.mockResolvedValueOnce(1)
|
||||
.mockResolvedValueOnce(0),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
deleteAllByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
markAllAsReadByRecipientId: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
// Create notification
|
||||
await mockRepository.create(notification);
|
||||
expect(mockRepository.create).toHaveBeenCalledWith(notification);
|
||||
|
||||
// Find notification
|
||||
const found = await mockRepository.findById('notification-1');
|
||||
expect(found).toBe(notification);
|
||||
|
||||
// Update notification
|
||||
await mockRepository.update(updatedNotification);
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(updatedNotification);
|
||||
|
||||
// Verify update
|
||||
const updatedFound = await mockRepository.findById('notification-1');
|
||||
expect(updatedFound).toBe(updatedNotification);
|
||||
|
||||
// Delete notification
|
||||
await mockRepository.delete('notification-1');
|
||||
expect(mockRepository.delete).toHaveBeenCalledWith('notification-1');
|
||||
|
||||
// Verify deletion
|
||||
const deletedFound = await mockRepository.findById('notification-1');
|
||||
expect(deletedFound).toBeNull();
|
||||
});
|
||||
});
|
||||
419
core/notifications/domain/types/NotificationTypes.test.ts
Normal file
419
core/notifications/domain/types/NotificationTypes.test.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
getChannelDisplayName,
|
||||
isExternalChannel,
|
||||
DEFAULT_ENABLED_CHANNELS,
|
||||
ALL_CHANNELS,
|
||||
getNotificationTypeTitle,
|
||||
getNotificationTypePriority,
|
||||
type NotificationChannel,
|
||||
type NotificationType,
|
||||
} from './NotificationTypes';
|
||||
|
||||
describe('NotificationTypes - Channel Functions', () => {
|
||||
describe('getChannelDisplayName', () => {
|
||||
it('returns correct display name for in_app channel', () => {
|
||||
expect(getChannelDisplayName('in_app')).toBe('In-App');
|
||||
});
|
||||
|
||||
it('returns correct display name for email channel', () => {
|
||||
expect(getChannelDisplayName('email')).toBe('Email');
|
||||
});
|
||||
|
||||
it('returns correct display name for discord channel', () => {
|
||||
expect(getChannelDisplayName('discord')).toBe('Discord');
|
||||
});
|
||||
|
||||
it('returns correct display name for push channel', () => {
|
||||
expect(getChannelDisplayName('push')).toBe('Push Notification');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExternalChannel', () => {
|
||||
it('returns false for in_app channel', () => {
|
||||
expect(isExternalChannel('in_app')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for email channel', () => {
|
||||
expect(isExternalChannel('email')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for discord channel', () => {
|
||||
expect(isExternalChannel('discord')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for push channel', () => {
|
||||
expect(isExternalChannel('push')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_ENABLED_CHANNELS', () => {
|
||||
it('contains only in_app channel', () => {
|
||||
expect(DEFAULT_ENABLED_CHANNELS).toEqual(['in_app']);
|
||||
});
|
||||
|
||||
it('is an array', () => {
|
||||
expect(Array.isArray(DEFAULT_ENABLED_CHANNELS)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ALL_CHANNELS', () => {
|
||||
it('contains all notification channels', () => {
|
||||
expect(ALL_CHANNELS).toEqual(['in_app', 'email', 'discord', 'push']);
|
||||
});
|
||||
|
||||
it('is an array', () => {
|
||||
expect(Array.isArray(ALL_CHANNELS)).toBe(true);
|
||||
});
|
||||
|
||||
it('has correct length', () => {
|
||||
expect(ALL_CHANNELS.length).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationTypes - Notification Type Functions', () => {
|
||||
describe('getNotificationTypeTitle', () => {
|
||||
it('returns correct title for protest_filed', () => {
|
||||
expect(getNotificationTypeTitle('protest_filed')).toBe('Protest Filed');
|
||||
});
|
||||
|
||||
it('returns correct title for protest_defense_requested', () => {
|
||||
expect(getNotificationTypeTitle('protest_defense_requested')).toBe('Defense Requested');
|
||||
});
|
||||
|
||||
it('returns correct title for protest_defense_submitted', () => {
|
||||
expect(getNotificationTypeTitle('protest_defense_submitted')).toBe('Defense Submitted');
|
||||
});
|
||||
|
||||
it('returns correct title for protest_comment_added', () => {
|
||||
expect(getNotificationTypeTitle('protest_comment_added')).toBe('New Comment');
|
||||
});
|
||||
|
||||
it('returns correct title for protest_vote_required', () => {
|
||||
expect(getNotificationTypeTitle('protest_vote_required')).toBe('Vote Required');
|
||||
});
|
||||
|
||||
it('returns correct title for protest_vote_cast', () => {
|
||||
expect(getNotificationTypeTitle('protest_vote_cast')).toBe('Vote Cast');
|
||||
});
|
||||
|
||||
it('returns correct title for protest_resolved', () => {
|
||||
expect(getNotificationTypeTitle('protest_resolved')).toBe('Protest Resolved');
|
||||
});
|
||||
|
||||
it('returns correct title for penalty_issued', () => {
|
||||
expect(getNotificationTypeTitle('penalty_issued')).toBe('Penalty Issued');
|
||||
});
|
||||
|
||||
it('returns correct title for penalty_appealed', () => {
|
||||
expect(getNotificationTypeTitle('penalty_appealed')).toBe('Penalty Appealed');
|
||||
});
|
||||
|
||||
it('returns correct title for penalty_appeal_resolved', () => {
|
||||
expect(getNotificationTypeTitle('penalty_appeal_resolved')).toBe('Appeal Resolved');
|
||||
});
|
||||
|
||||
it('returns correct title for race_registration_open', () => {
|
||||
expect(getNotificationTypeTitle('race_registration_open')).toBe('Registration Open');
|
||||
});
|
||||
|
||||
it('returns correct title for race_reminder', () => {
|
||||
expect(getNotificationTypeTitle('race_reminder')).toBe('Race Reminder');
|
||||
});
|
||||
|
||||
it('returns correct title for race_results_posted', () => {
|
||||
expect(getNotificationTypeTitle('race_results_posted')).toBe('Results Posted');
|
||||
});
|
||||
|
||||
it('returns correct title for race_performance_summary', () => {
|
||||
expect(getNotificationTypeTitle('race_performance_summary')).toBe('Performance Summary');
|
||||
});
|
||||
|
||||
it('returns correct title for race_final_results', () => {
|
||||
expect(getNotificationTypeTitle('race_final_results')).toBe('Final Results');
|
||||
});
|
||||
|
||||
it('returns correct title for league_invite', () => {
|
||||
expect(getNotificationTypeTitle('league_invite')).toBe('League Invitation');
|
||||
});
|
||||
|
||||
it('returns correct title for league_join_request', () => {
|
||||
expect(getNotificationTypeTitle('league_join_request')).toBe('Join Request');
|
||||
});
|
||||
|
||||
it('returns correct title for league_join_approved', () => {
|
||||
expect(getNotificationTypeTitle('league_join_approved')).toBe('Request Approved');
|
||||
});
|
||||
|
||||
it('returns correct title for league_join_rejected', () => {
|
||||
expect(getNotificationTypeTitle('league_join_rejected')).toBe('Request Rejected');
|
||||
});
|
||||
|
||||
it('returns correct title for league_role_changed', () => {
|
||||
expect(getNotificationTypeTitle('league_role_changed')).toBe('Role Changed');
|
||||
});
|
||||
|
||||
it('returns correct title for team_invite', () => {
|
||||
expect(getNotificationTypeTitle('team_invite')).toBe('Team Invitation');
|
||||
});
|
||||
|
||||
it('returns correct title for team_join_request', () => {
|
||||
expect(getNotificationTypeTitle('team_join_request')).toBe('Team Join Request');
|
||||
});
|
||||
|
||||
it('returns correct title for team_join_approved', () => {
|
||||
expect(getNotificationTypeTitle('team_join_approved')).toBe('Team Request Approved');
|
||||
});
|
||||
|
||||
it('returns correct title for sponsorship_request_received', () => {
|
||||
expect(getNotificationTypeTitle('sponsorship_request_received')).toBe('Sponsorship Request');
|
||||
});
|
||||
|
||||
it('returns correct title for sponsorship_request_accepted', () => {
|
||||
expect(getNotificationTypeTitle('sponsorship_request_accepted')).toBe('Sponsorship Accepted');
|
||||
});
|
||||
|
||||
it('returns correct title for sponsorship_request_rejected', () => {
|
||||
expect(getNotificationTypeTitle('sponsorship_request_rejected')).toBe('Sponsorship Rejected');
|
||||
});
|
||||
|
||||
it('returns correct title for sponsorship_request_withdrawn', () => {
|
||||
expect(getNotificationTypeTitle('sponsorship_request_withdrawn')).toBe('Sponsorship Withdrawn');
|
||||
});
|
||||
|
||||
it('returns correct title for sponsorship_activated', () => {
|
||||
expect(getNotificationTypeTitle('sponsorship_activated')).toBe('Sponsorship Active');
|
||||
});
|
||||
|
||||
it('returns correct title for sponsorship_payment_received', () => {
|
||||
expect(getNotificationTypeTitle('sponsorship_payment_received')).toBe('Payment Received');
|
||||
});
|
||||
|
||||
it('returns correct title for system_announcement', () => {
|
||||
expect(getNotificationTypeTitle('system_announcement')).toBe('Announcement');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationTypePriority', () => {
|
||||
it('returns correct priority for protest_filed', () => {
|
||||
expect(getNotificationTypePriority('protest_filed')).toBe(8);
|
||||
});
|
||||
|
||||
it('returns correct priority for protest_defense_requested', () => {
|
||||
expect(getNotificationTypePriority('protest_defense_requested')).toBe(9);
|
||||
});
|
||||
|
||||
it('returns correct priority for protest_defense_submitted', () => {
|
||||
expect(getNotificationTypePriority('protest_defense_submitted')).toBe(6);
|
||||
});
|
||||
|
||||
it('returns correct priority for protest_comment_added', () => {
|
||||
expect(getNotificationTypePriority('protest_comment_added')).toBe(4);
|
||||
});
|
||||
|
||||
it('returns correct priority for protest_vote_required', () => {
|
||||
expect(getNotificationTypePriority('protest_vote_required')).toBe(8);
|
||||
});
|
||||
|
||||
it('returns correct priority for protest_vote_cast', () => {
|
||||
expect(getNotificationTypePriority('protest_vote_cast')).toBe(3);
|
||||
});
|
||||
|
||||
it('returns correct priority for protest_resolved', () => {
|
||||
expect(getNotificationTypePriority('protest_resolved')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for penalty_issued', () => {
|
||||
expect(getNotificationTypePriority('penalty_issued')).toBe(9);
|
||||
});
|
||||
|
||||
it('returns correct priority for penalty_appealed', () => {
|
||||
expect(getNotificationTypePriority('penalty_appealed')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for penalty_appeal_resolved', () => {
|
||||
expect(getNotificationTypePriority('penalty_appeal_resolved')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for race_registration_open', () => {
|
||||
expect(getNotificationTypePriority('race_registration_open')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns correct priority for race_reminder', () => {
|
||||
expect(getNotificationTypePriority('race_reminder')).toBe(8);
|
||||
});
|
||||
|
||||
it('returns correct priority for race_results_posted', () => {
|
||||
expect(getNotificationTypePriority('race_results_posted')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns correct priority for race_performance_summary', () => {
|
||||
expect(getNotificationTypePriority('race_performance_summary')).toBe(9);
|
||||
});
|
||||
|
||||
it('returns correct priority for race_final_results', () => {
|
||||
expect(getNotificationTypePriority('race_final_results')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for league_invite', () => {
|
||||
expect(getNotificationTypePriority('league_invite')).toBe(6);
|
||||
});
|
||||
|
||||
it('returns correct priority for league_join_request', () => {
|
||||
expect(getNotificationTypePriority('league_join_request')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns correct priority for league_join_approved', () => {
|
||||
expect(getNotificationTypePriority('league_join_approved')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for league_join_rejected', () => {
|
||||
expect(getNotificationTypePriority('league_join_rejected')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for league_role_changed', () => {
|
||||
expect(getNotificationTypePriority('league_role_changed')).toBe(6);
|
||||
});
|
||||
|
||||
it('returns correct priority for team_invite', () => {
|
||||
expect(getNotificationTypePriority('team_invite')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns correct priority for team_join_request', () => {
|
||||
expect(getNotificationTypePriority('team_join_request')).toBe(4);
|
||||
});
|
||||
|
||||
it('returns correct priority for team_join_approved', () => {
|
||||
expect(getNotificationTypePriority('team_join_approved')).toBe(6);
|
||||
});
|
||||
|
||||
it('returns correct priority for sponsorship_request_received', () => {
|
||||
expect(getNotificationTypePriority('sponsorship_request_received')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for sponsorship_request_accepted', () => {
|
||||
expect(getNotificationTypePriority('sponsorship_request_accepted')).toBe(8);
|
||||
});
|
||||
|
||||
it('returns correct priority for sponsorship_request_rejected', () => {
|
||||
expect(getNotificationTypePriority('sponsorship_request_rejected')).toBe(6);
|
||||
});
|
||||
|
||||
it('returns correct priority for sponsorship_request_withdrawn', () => {
|
||||
expect(getNotificationTypePriority('sponsorship_request_withdrawn')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns correct priority for sponsorship_activated', () => {
|
||||
expect(getNotificationTypePriority('sponsorship_activated')).toBe(7);
|
||||
});
|
||||
|
||||
it('returns correct priority for sponsorship_payment_received', () => {
|
||||
expect(getNotificationTypePriority('sponsorship_payment_received')).toBe(8);
|
||||
});
|
||||
|
||||
it('returns correct priority for system_announcement', () => {
|
||||
expect(getNotificationTypePriority('system_announcement')).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationTypes - Type Safety', () => {
|
||||
it('ALL_CHANNELS contains all NotificationChannel values', () => {
|
||||
const channels: NotificationChannel[] = ['in_app', 'email', 'discord', 'push'];
|
||||
channels.forEach(channel => {
|
||||
expect(ALL_CHANNELS).toContain(channel);
|
||||
});
|
||||
});
|
||||
|
||||
it('DEFAULT_ENABLED_CHANNELS is a subset of ALL_CHANNELS', () => {
|
||||
DEFAULT_ENABLED_CHANNELS.forEach(channel => {
|
||||
expect(ALL_CHANNELS).toContain(channel);
|
||||
});
|
||||
});
|
||||
|
||||
it('all notification types have titles', () => {
|
||||
const types: NotificationType[] = [
|
||||
'protest_filed',
|
||||
'protest_defense_requested',
|
||||
'protest_defense_submitted',
|
||||
'protest_comment_added',
|
||||
'protest_vote_required',
|
||||
'protest_vote_cast',
|
||||
'protest_resolved',
|
||||
'penalty_issued',
|
||||
'penalty_appealed',
|
||||
'penalty_appeal_resolved',
|
||||
'race_registration_open',
|
||||
'race_reminder',
|
||||
'race_results_posted',
|
||||
'race_performance_summary',
|
||||
'race_final_results',
|
||||
'league_invite',
|
||||
'league_join_request',
|
||||
'league_join_approved',
|
||||
'league_join_rejected',
|
||||
'league_role_changed',
|
||||
'team_invite',
|
||||
'team_join_request',
|
||||
'team_join_approved',
|
||||
'sponsorship_request_received',
|
||||
'sponsorship_request_accepted',
|
||||
'sponsorship_request_rejected',
|
||||
'sponsorship_request_withdrawn',
|
||||
'sponsorship_activated',
|
||||
'sponsorship_payment_received',
|
||||
'system_announcement',
|
||||
];
|
||||
|
||||
types.forEach(type => {
|
||||
const title = getNotificationTypeTitle(type);
|
||||
expect(title).toBeDefined();
|
||||
expect(typeof title).toBe('string');
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('all notification types have priorities', () => {
|
||||
const types: NotificationType[] = [
|
||||
'protest_filed',
|
||||
'protest_defense_requested',
|
||||
'protest_defense_submitted',
|
||||
'protest_comment_added',
|
||||
'protest_vote_required',
|
||||
'protest_vote_cast',
|
||||
'protest_resolved',
|
||||
'penalty_issued',
|
||||
'penalty_appealed',
|
||||
'penalty_appeal_resolved',
|
||||
'race_registration_open',
|
||||
'race_reminder',
|
||||
'race_results_posted',
|
||||
'race_performance_summary',
|
||||
'race_final_results',
|
||||
'league_invite',
|
||||
'league_join_request',
|
||||
'league_join_approved',
|
||||
'league_join_rejected',
|
||||
'league_role_changed',
|
||||
'team_invite',
|
||||
'team_join_request',
|
||||
'team_join_approved',
|
||||
'sponsorship_request_received',
|
||||
'sponsorship_request_accepted',
|
||||
'sponsorship_request_rejected',
|
||||
'sponsorship_request_withdrawn',
|
||||
'sponsorship_activated',
|
||||
'sponsorship_payment_received',
|
||||
'system_announcement',
|
||||
];
|
||||
|
||||
types.forEach(type => {
|
||||
const priority = getNotificationTypePriority(type);
|
||||
expect(priority).toBeDefined();
|
||||
expect(typeof priority).toBe('number');
|
||||
expect(priority).toBeGreaterThanOrEqual(0);
|
||||
expect(priority).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user