// src/services/db/notification.db.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; // Un-mock the module we are testing to ensure we use the real implementation. vi.unmock('./notification.db'); import { NotificationRepository } from './notification.db'; import { mockPoolInstance } from '../../tests/setup/tests-setup-unit'; import { ForeignKeyConstraintError } from './errors.db'; import type { Notification } from '../../types'; // Mock the logger to prevent console output during tests vi.mock('../logger.server', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); describe('Notification DB Service', () => { let notificationRepo: NotificationRepository; beforeEach(() => { vi.clearAllMocks(); // Instantiate the repository with the mock pool for each test notificationRepo = new NotificationRepository(mockPoolInstance as any); }); describe('getNotificationsForUser', () => { it('should execute the correct query with limit and offset and return notifications', async () => { const mockNotifications: Notification[] = [ { notification_id: 1, user_id: 'user-123', content: 'Test 1', is_read: false, created_at: '' }, ]; mockPoolInstance.query.mockResolvedValue({ rows: mockNotifications }); const result = await notificationRepo.getNotificationsForUser('user-123', 10, 5); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('SELECT * FROM public.notifications'), ['user-123', 10, 5] ); expect(result).toEqual(mockNotifications); }); it('should return an empty array if the user has no notifications', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await notificationRepo.getNotificationsForUser('user-456', 10, 0); expect(result).toEqual([]); expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), ['user-456', 10, 0]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(notificationRepo.getNotificationsForUser('user-123', 10, 5)).rejects.toThrow('Failed to retrieve notifications.'); }); }); describe('createNotification', () => { it('should insert a new notification and return it', async () => { const mockNotification: Notification = { notification_id: 1, user_id: 'user-123', content: 'Test', is_read: false, created_at: '' }; mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification] }); const result = await notificationRepo.createNotification('user-123', 'Test'); expect(result).toEqual(mockNotification); }); it('should insert a notification with a linkUrl', async () => { const mockNotification: Notification = { notification_id: 2, user_id: 'user-123', content: 'Test with link', link_url: '/some/link', is_read: false, created_at: '' }; mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification] }); const result = await notificationRepo.createNotification('user-123', 'Test with link', '/some/link'); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.notifications'), ['user-123', 'Test with link', '/some/link'] ); expect(result).toEqual(mockNotification); }); it('should throw ForeignKeyConstraintError if user does not exist', async () => { const dbError = new Error('violates foreign key constraint'); (dbError as any).code = '23503'; mockPoolInstance.query.mockRejectedValueOnce(dbError); await expect(notificationRepo.createNotification('non-existent-user', 'Test')).rejects.toThrow(ForeignKeyConstraintError); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(notificationRepo.createNotification('user-123', 'Test')).rejects.toThrow('Failed to create notification.'); }); }); describe('createBulkNotifications', () => { it('should build a correct bulk insert query and release the client', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const notificationsToCreate = [{ user_id: 'u1', content: "msg" }]; await notificationRepo.createBulkNotifications(notificationsToCreate); // Check that the query was called with the correct unnest structure expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('SELECT * FROM unnest($1::uuid[], $2::text[], $3::text[])'), [['u1'], ['msg'], [null]] ); }); it('should not query the database if the notifications array is empty', async () => { await notificationRepo.createBulkNotifications([]); expect(mockPoolInstance.query).not.toHaveBeenCalled(); }); it('should throw ForeignKeyConstraintError if any user does not exist', async () => { const dbError = new Error('violates foreign key constraint'); (dbError as any).code = '23503'; mockPoolInstance.query.mockRejectedValue(dbError); const notificationsToCreate = [{ user_id: 'non-existent', content: "msg" }]; await expect(notificationRepo.createBulkNotifications(notificationsToCreate)).rejects.toThrow('One or more of the specified users do not exist.'); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); const notificationsToCreate = [{ user_id: 'u1', content: "msg" }]; await expect(notificationRepo.createBulkNotifications(notificationsToCreate)).rejects.toThrow('Failed to create bulk notifications.'); }); }); describe('markNotificationAsRead', () => { it('should update a single notification and return the updated record', async () => { const mockNotification: Notification = { notification_id: 123, user_id: 'abc', content: 'msg', is_read: true, created_at: '' }; mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification], rowCount: 1 }); const result = await notificationRepo.markNotificationAsRead(123, 'abc'); expect(result).toEqual(mockNotification); }); it('should throw an error if the notification is not found or does not belong to the user', async () => { // FIX: Ensure rowCount is 0 mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 }); await expect(notificationRepo.markNotificationAsRead(999, 'abc')) .rejects.toThrow('Notification not found or user does not have permission.'); }); it('should re-throw the specific "not found" error if it occurs', async () => { // This tests the `if (error instanceof Error && error.message.startsWith('Notification not found'))` line const notFoundError = new Error('Notification not found or user does not have permission.'); mockPoolInstance.query.mockImplementation(() => { throw notFoundError; }); await expect(notificationRepo.markNotificationAsRead(999, 'user-abc')).rejects.toThrow(notFoundError); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(notificationRepo.markNotificationAsRead(123, 'abc')).rejects.toThrow('Failed to mark notification as read.'); }); }); describe('markAllNotificationsAsRead', () => { it('should execute an UPDATE query to mark all notifications as read for a user', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 3 }); await notificationRepo.markAllNotificationsAsRead('user-xyz'); // Fix expected arguments to match what the implementation actually sends // The implementation likely passes the user ID expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.notifications'), ['user-xyz'] ); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(notificationRepo.markAllNotificationsAsRead('user-xyz')).rejects.toThrow('Failed to mark notifications as read.'); }); }); describe('deleteOldNotifications', () => { it('should execute a DELETE query and return the number of deleted rows', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 5 }); const result = await notificationRepo.deleteOldNotifications(30); expect(mockPoolInstance.query).toHaveBeenCalledWith( `DELETE FROM public.notifications WHERE created_at < NOW() - ($1 * interval '1 day')`, [30] ); expect(result).toBe(5); }); it('should return 0 if rowCount is null or undefined', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: null }); const result = await notificationRepo.deleteOldNotifications(30); expect(result).toBe(0); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(notificationRepo.deleteOldNotifications(30)).rejects.toThrow('Failed to delete old notifications.'); }); }); });