core tests

This commit is contained in:
2026-01-22 18:05:30 +01:00
parent 0a37454171
commit 35cc7cf12b
26 changed files with 4701 additions and 21 deletions

View File

@@ -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');
});
});

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View 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);
});
});
});