Files
flyer-crawler.projectium.com/src/services/db/notification.db.test.ts
Torben Sorensen c9b7a75429
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m59s
more and more test fixes
2026-01-04 12:30:44 -08:00

347 lines
13 KiB
TypeScript

// src/services/db/notification.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Pool } from 'pg';
vi.unmock('./notification.db');
import { NotificationRepository } from './notification.db';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import type { Notification } from '../../types';
import { createMockNotification } from '../../tests/utils/mockFactories';
// 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(),
},
}));
import { logger as mockLogger } from '../logger.server';
describe('Notification DB Service', () => {
let notificationRepo: NotificationRepository;
const mockDb = {
query: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
// Instantiate the repository with the mock pool for each test
notificationRepo = new NotificationRepository(mockPoolInstance as unknown as Pool);
});
describe('getNotificationsForUser', () => {
it('should only return unread notifications by default', async () => {
const mockNotifications: Notification[] = [
createMockNotification({
notification_id: 1,
user_id: 'user-123',
content: 'Test 1',
is_read: false,
}),
];
mockPoolInstance.query.mockResolvedValue({ rows: mockNotifications });
const result = await notificationRepo.getNotificationsForUser(
'user-123',
10,
5,
false,
mockLogger,
);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('is_read = false'),
['user-123', 10, 5],
);
expect(result).toEqual(mockNotifications);
});
it('should return all notifications when includeRead is true', async () => {
const mockNotifications: Notification[] = [
createMockNotification({ is_read: true }),
createMockNotification({ is_read: false }),
];
mockPoolInstance.query.mockResolvedValue({ rows: mockNotifications });
await notificationRepo.getNotificationsForUser('user-123', 10, 0, true, mockLogger);
// The query should NOT contain the is_read filter
expect(mockPoolInstance.query.mock.calls[0][0]).not.toContain('is_read = false');
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), ['user-123', 10, 0]);
});
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,
false,
mockLogger,
);
expect(result).toEqual([]);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('is_read = false'),
['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, false, mockLogger),
).rejects.toThrow('Failed to retrieve notifications.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, userId: 'user-123', limit: 10, offset: 5, includeRead: false },
'Database error in getNotificationsForUser',
);
});
});
describe('createNotification', () => {
it('should insert a new notification and return it', async () => {
const mockNotification = createMockNotification({
notification_id: 1,
user_id: 'user-123',
content: 'Test',
is_read: false,
});
mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification] });
const result = await notificationRepo.createNotification('user-123', 'Test', mockLogger);
expect(result).toEqual(mockNotification);
});
it('should insert a notification with a linkUrl', async () => {
const mockNotification = createMockNotification({
notification_id: 2,
user_id: 'user-123',
content: 'Test with link',
link_url: '/some/link',
is_read: false,
});
mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification] });
const result = await notificationRepo.createNotification(
'user-123',
'Test with link',
mockLogger,
'/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 Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValueOnce(dbError);
await expect(
notificationRepo.createNotification('non-existent-user', 'Test', mockLogger),
).rejects.toThrow('The specified user does not exist.');
expect(mockLogger.error).toHaveBeenCalledWith(
{
err: dbError,
userId: 'non-existent-user',
content: 'Test',
linkUrl: undefined,
code: '23503',
constraint: undefined,
detail: undefined,
},
'Database error in createNotification',
);
});
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', mockLogger),
).rejects.toThrow('Failed to create notification.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, userId: 'user-123', content: 'Test', linkUrl: undefined },
'Database error in createNotification',
);
});
});
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, mockLogger);
// 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([], mockLogger);
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 Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
const notificationsToCreate = [{ user_id: 'non-existent', content: 'msg' }];
await expect(
notificationRepo.createBulkNotifications(notificationsToCreate, mockLogger),
).rejects.toThrow(ForeignKeyConstraintError);
expect(mockLogger.error).toHaveBeenCalledWith(
{
err: dbError,
notifications: notificationsToCreate,
code: '23503',
constraint: undefined,
detail: undefined,
},
'Database error in createBulkNotifications',
);
});
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, mockLogger),
).rejects.toThrow('Failed to create bulk notifications.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, notifications: notificationsToCreate },
'Database error in createBulkNotifications',
);
});
});
describe('markNotificationAsRead', () => {
it('should update a single notification and return the updated record', async () => {
const mockNotification = createMockNotification({
notification_id: 123,
user_id: 'abc',
content: 'msg',
is_read: true,
});
mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification], rowCount: 1 });
const result = await notificationRepo.markNotificationAsRead(123, 'abc', mockLogger);
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', mockLogger)).rejects.toThrow(
NotFoundError,
);
});
it('should re-throw the specific "not found" error if it occurs', async () => {
// This tests the `if (error instanceof NotFoundError)` line
const notFoundError = new NotFoundError(
'Notification not found or user does not have permission.',
);
mockPoolInstance.query.mockImplementation(() => {
throw notFoundError;
});
await expect(
notificationRepo.markNotificationAsRead(999, 'user-abc', mockLogger),
).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', mockLogger)).rejects.toThrow(
'Failed to mark notification as read.',
);
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, notificationId: 123, userId: 'abc' },
'Database error in markNotificationAsRead',
);
});
});
describe('markNotificationAsRead - Ownership Check', () => {
it('should not mark a notification as read if the user does not own it', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0 });
await expect(notificationRepo.markNotificationAsRead(1, 'wrong-user', mockLogger)).rejects.toThrow(
'Notification not found or user does not have permission.',
);
});
});
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', mockLogger);
// 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', mockLogger),
).rejects.toThrow('Failed to mark notifications as read.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, userId: 'user-xyz' },
'Database error in markAllNotificationsAsRead',
);
});
});
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, mockLogger);
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, mockLogger);
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, mockLogger)).rejects.toThrow(
'Failed to delete old notifications.',
);
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, daysOld: 30 },
'Database error in deleteOldNotifications',
);
});
});
});