// src/services/db/admin.db.test.ts import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import type { Pool, PoolClient } from 'pg'; import { ForeignKeyConstraintError, NotFoundError } from './errors.db'; import { AdminRepository } from './admin.db'; import type { SuggestedCorrection, AdminUserView, Profile } from '../../types'; import { createMockSuggestedCorrection, createMockAdminUserView, createMockProfile, } from '../../tests/utils/mockFactories'; // Un-mock the module we are testing vi.unmock('./admin.db'); // Mock the logger to prevent console output during tests vi.mock('../logger.server', () => ({ logger: { info: vi.fn(), warn: vi.fn(), // Keep warn for other tests that might use it error: vi.fn(), debug: vi.fn(), }, })); import { logger as mockLogger } from '../logger.server'; // Mock the withTransaction helper vi.mock('./connection.db', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, withTransaction: vi.fn() }; }); import { withTransaction } from './connection.db'; describe('Admin DB Service', () => { let adminRepo: AdminRepository; const mockDb = { query: vi.fn(), }; beforeEach(() => { // Reset the global mock's call history before each test. vi.clearAllMocks(); // Reset the withTransaction mock before each test vi.mocked(withTransaction).mockImplementation(async (callback) => { const mockClient = { query: vi.fn() }; return callback(mockClient as unknown as PoolClient); }); // Instantiate the repository with the minimal mock db for each test adminRepo = new AdminRepository(mockDb); }); describe('getSuggestedCorrections', () => { it('should execute the correct query and return corrections', async () => { const mockCorrections: SuggestedCorrection[] = [ createMockSuggestedCorrection({ suggested_correction_id: 1 }), ]; mockDb.query.mockResolvedValue({ rows: mockCorrections }); const result = await adminRepo.getSuggestedCorrections(mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.suggested_corrections sc'), ); expect(result).toEqual(mockCorrections); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.getSuggestedCorrections(mockLogger)).rejects.toThrow( 'Failed to retrieve suggested corrections.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError }, 'Database error in getSuggestedCorrections', ); }); }); describe('approveCorrection', () => { it('should call the approve_correction database function', async () => { mockDb.query.mockResolvedValue({ rows: [] }); // Mock the function call await adminRepo.approveCorrection(123, mockLogger); expect(mockDb.query).toHaveBeenCalledWith( 'SELECT public.approve_correction($1)', [123], ); }); it('should throw an error if the database function fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.approveCorrection(123, mockLogger)).rejects.toThrow( 'Failed to approve correction.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, correctionId: 123 }, 'Database transaction error in approveCorrection', ); }); }); describe('rejectCorrection', () => { it('should update the correction status to rejected', async () => { mockDb.query.mockResolvedValue({ rowCount: 1 }); await adminRepo.rejectCorrection(123, mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining("UPDATE public.suggested_corrections SET status = 'rejected'"), [123], ); }); it('should throw NotFoundError if the correction is not found or not pending', async () => { mockDb.query.mockResolvedValue({ rowCount: 0 }); await expect(adminRepo.rejectCorrection(123, mockLogger)).rejects.toThrow(NotFoundError); await expect(adminRepo.rejectCorrection(123, mockLogger)).rejects.toThrow( "Correction with ID 123 not found or not in 'pending' state.", ); }); it('should throw an error if the database query fails', async () => { mockDb.query.mockRejectedValue(new Error('DB Error')); await expect(adminRepo.rejectCorrection(123, mockLogger)).rejects.toThrow( 'Failed to reject correction.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: expect.any(Error), correctionId: 123 }, 'Database error in rejectCorrection', ); }); }); describe('updateSuggestedCorrection', () => { it('should update the suggested value and return the updated correction', async () => { const mockCorrection = createMockSuggestedCorrection({ suggested_correction_id: 1, suggested_value: '300', }); mockDb.query.mockResolvedValue({ rows: [mockCorrection], rowCount: 1 }); const result = await adminRepo.updateSuggestedCorrection(1, '300', mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.suggested_corrections SET suggested_value = $1'), ['300', 1], ); expect(result).toEqual(mockCorrection); }); it('should throw an error if the correction is not found (rowCount is 0)', async () => { mockDb.query.mockResolvedValue({ rowCount: 0, rows: [] }); await expect( adminRepo.updateSuggestedCorrection(999, 'new value', mockLogger), ).rejects.toThrow(NotFoundError); await expect( adminRepo.updateSuggestedCorrection(999, 'new value', mockLogger), ).rejects.toThrow("Correction with ID 999 not found or is not in 'pending' state."); }); it('should throw a generic error if the database query fails', async () => { mockDb.query.mockRejectedValue(new Error('DB Error')); await expect(adminRepo.updateSuggestedCorrection(1, 'new value', mockLogger)).rejects.toThrow( 'Failed to update suggested correction.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: expect.any(Error), correctionId: 1 }, 'Database error in updateSuggestedCorrection', ); }); }); describe('getApplicationStats', () => { it('should execute 5 parallel count queries and return the aggregated stats', async () => { // Mock responses for each of the 5 parallel queries mockDb.query .mockResolvedValueOnce({ rows: [{ count: '10' }] }) // flyerCount .mockResolvedValueOnce({ rows: [{ count: '20' }] }) // userCount .mockResolvedValueOnce({ rows: [{ count: '300' }] }) // flyerItemCount .mockResolvedValueOnce({ rows: [{ count: '5' }] }) // storeCount .mockResolvedValueOnce({ rows: [{ count: '2' }] }) // pendingCorrectionCount .mockResolvedValueOnce({ rows: [{ count: '15' }] }); // recipeCount const stats = await adminRepo.getApplicationStats(mockLogger); expect(mockDb.query).toHaveBeenCalledTimes(6); expect(stats).toEqual({ flyerCount: 10, userCount: 20, flyerItemCount: 300, storeCount: 5, pendingCorrectionCount: 2, recipeCount: 15, }); }); it('should throw an error if one of the parallel queries fails', async () => { // Mock one query to succeed and another to fail mockDb.query .mockResolvedValueOnce({ rows: [{ count: '10' }] }) .mockRejectedValueOnce(new Error('DB Read Error')); // The Promise.all should reject, and the function should re-throw the error await expect(adminRepo.getApplicationStats(mockLogger)).rejects.toThrow('DB Read Error'); expect(mockLogger.error).toHaveBeenCalledWith( { err: expect.any(Error) }, 'Database error in getApplicationStats', ); }); }); describe('getDailyStatsForLast30Days', () => { it('should execute the correct query to get daily stats', async () => { const mockStats = [{ date: '2023-01-01', new_users: 5, new_flyers: 2 }]; mockDb.query.mockResolvedValue({ rows: mockStats }); const result = await adminRepo.getDailyStatsForLast30Days(mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('WITH date_series AS'), ); expect(result).toEqual(mockStats); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.getDailyStatsForLast30Days(mockLogger)).rejects.toThrow( 'Failed to retrieve daily statistics.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError }, 'Database error in getDailyStatsForLast30Days', ); }); }); describe('logActivity', () => { it('should insert a new activity log entry', async () => { mockDb.query.mockResolvedValue({ rows: [] }); const logData = { userId: 'user-123', action: 'test_action', displayText: 'Test activity' }; await adminRepo.logActivity(logData, mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.activity_log'), [logData.userId, logData.action, logData.displayText, null, null], ); }); it('should not throw an error if the database query fails (non-critical)', async () => { mockDb.query.mockRejectedValue(new Error('DB Error')); const logData = { action: 'test_action', displayText: 'Test activity' }; await expect(adminRepo.logActivity(logData, mockLogger)).resolves.toBeUndefined(); expect(mockLogger.error).toHaveBeenCalledWith( { err: expect.any(Error), logData }, 'Database error in logActivity', ); }); }); describe('getMostFrequentSaleItems', () => { it('should call the correct database function', async () => { mockDb.query.mockResolvedValue({ rows: [] }); await adminRepo.getMostFrequentSaleItems(30, 10, mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.flyer_items fi'), [30, 10], ); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.getMostFrequentSaleItems(30, 10, mockLogger)).rejects.toThrow( 'Failed to get most frequent sale items.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError }, 'Database error in getMostFrequentSaleItems', ); }); }); describe('updateRecipeCommentStatus', () => { it('should update the comment status and return the updated comment', async () => { const mockComment = { comment_id: 1, status: 'hidden' }; mockDb.query.mockResolvedValue({ rows: [mockComment], rowCount: 1 }); const result = await adminRepo.updateRecipeCommentStatus(1, 'hidden', mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.recipe_comments'), ['hidden', 1], ); expect(result).toEqual(mockComment); }); it('should throw an error if the comment is not found (rowCount is 0)', async () => { mockDb.query.mockResolvedValue({ rowCount: 0, rows: [] }); await expect(adminRepo.updateRecipeCommentStatus(999, 'hidden', mockLogger)).rejects.toThrow( 'Recipe comment with ID 999 not found.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.updateRecipeCommentStatus(1, 'hidden', mockLogger)).rejects.toThrow( 'Failed to update recipe comment status.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, commentId: 1, status: 'hidden' }, 'Database error in updateRecipeCommentStatus', ); }); }); describe('getUnmatchedFlyerItems', () => { it('should execute the correct query to get unmatched items', async () => { mockDb.query.mockResolvedValue({ rows: [] }); await adminRepo.getUnmatchedFlyerItems(mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.unmatched_flyer_items ufi'), ); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.getUnmatchedFlyerItems(mockLogger)).rejects.toThrow( 'Failed to retrieve unmatched flyer items.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError }, 'Database error in getUnmatchedFlyerItems', ); }); }); describe('updateRecipeStatus', () => { it('should update the recipe status and return the updated recipe', async () => { const mockRecipe = { recipe_id: 1, status: 'public' }; mockDb.query.mockResolvedValue({ rows: [mockRecipe], rowCount: 1 }); const result = await adminRepo.updateRecipeStatus(1, 'public', mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.recipes'), ['public', 1], ); expect(result).toEqual(mockRecipe); }); it('should throw an error if the recipe is not found (rowCount is 0)', async () => { mockDb.query.mockResolvedValue({ rowCount: 0, rows: [] }); await expect(adminRepo.updateRecipeStatus(999, 'public', mockLogger)).rejects.toThrow( NotFoundError, ); await expect(adminRepo.updateRecipeStatus(999, 'public', mockLogger)).rejects.toThrow( 'Recipe with ID 999 not found.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.updateRecipeStatus(1, 'public', mockLogger)).rejects.toThrow( 'Failed to update recipe status.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, recipeId: 1, status: 'public' }, 'Database error in updateRecipeStatus', ); }); }); describe('resolveUnmatchedFlyerItem', () => { it('should execute a transaction to resolve an unmatched item', async () => { // Create a mock client that we can reference both inside and outside the transaction mock. const mockClient = { query: vi.fn() }; (mockClient.query as Mock) .mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id from unmatched_flyer_items .mockResolvedValueOnce({ rowCount: 1 }) // UPDATE flyer_items table .mockResolvedValueOnce({ rowCount: 1 }); // UPDATE unmatched_flyer_items table vi.mocked(withTransaction).mockImplementation(async (callback) => { return callback(mockClient as unknown as PoolClient); }); await adminRepo.resolveUnmatchedFlyerItem(1, 101, mockLogger); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining('SELECT flyer_item_id FROM public.unmatched_flyer_items'), [1], ); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.flyer_items'), [101, 55], ); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining("UPDATE public.unmatched_flyer_items SET status = 'resolved'"), [1], ); }); it('should throw NotFoundError if the unmatched item is not found', async () => { vi.mocked(withTransaction).mockImplementation(async (callback) => { const mockClient = { query: vi.fn() }; (mockClient.query as Mock).mockResolvedValueOnce({ rowCount: 0, rows: [] }); // SELECT finds nothing await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(NotFoundError); throw new NotFoundError(`Unmatched flyer item with ID 999 not found.`); // Re-throw for the outer expect }); await expect(adminRepo.resolveUnmatchedFlyerItem(999, 101, mockLogger)).rejects.toThrow( NotFoundError, ); await expect(adminRepo.resolveUnmatchedFlyerItem(999, 101, mockLogger)).rejects.toThrow( 'Unmatched flyer item with ID 999 not found.', ); }); it('should rollback transaction on generic error', async () => { const dbError = new Error('DB Error'); vi.mocked(withTransaction).mockImplementation(async (callback) => { const mockClient = { query: vi.fn() }; (mockClient.query as Mock) .mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id .mockRejectedValueOnce(dbError); // UPDATE flyer_items fails await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError); throw dbError; // Re-throw for the outer expect }); await expect(adminRepo.resolveUnmatchedFlyerItem(1, 101, mockLogger)).rejects.toThrow( 'Failed to resolve unmatched flyer item.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, unmatchedFlyerItemId: 1, masterItemId: 101 }, 'Database transaction error in resolveUnmatchedFlyerItem', ); }); }); describe('ignoreUnmatchedFlyerItem', () => { it('should update the status of an unmatched item to "ignored"', async () => { mockDb.query.mockResolvedValue({ rowCount: 1 }); await adminRepo.ignoreUnmatchedFlyerItem(1, mockLogger); expect(mockDb.query).toHaveBeenCalledWith( "UPDATE public.unmatched_flyer_items SET status = 'ignored' WHERE unmatched_flyer_item_id = $1 AND status = 'pending'", [1], ); }); it('should throw NotFoundError if the unmatched item is not found or not pending', async () => { mockDb.query.mockResolvedValue({ rowCount: 0 }); await expect(adminRepo.ignoreUnmatchedFlyerItem(999, mockLogger)).rejects.toThrow( NotFoundError, ); await expect(adminRepo.ignoreUnmatchedFlyerItem(999, mockLogger)).rejects.toThrow( "Unmatched flyer item with ID 999 not found or not in 'pending' state.", ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.ignoreUnmatchedFlyerItem(1, mockLogger)).rejects.toThrow( 'Failed to ignore unmatched flyer item.', ); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining("UPDATE public.unmatched_flyer_items SET status = 'ignored'"), [1], ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, unmatchedFlyerItemId: 1 }, 'Database error in ignoreUnmatchedFlyerItem', ); }); }); describe('resetFailedLoginAttempts', () => { it('should execute a specific UPDATE query to reset attempts and log login details', async () => { mockDb.query.mockResolvedValue({ rows: [] }); await adminRepo.resetFailedLoginAttempts('user-123', '127.0.0.1', mockLogger); // Use a regular expression to match the SQL query while ignoring whitespace differences. // This makes the test more specific and robust. const expectedQueryRegex = /UPDATE\s+public\.users\s+SET\s+failed_login_attempts\s*=\s*0,\s*last_failed_login\s*=\s*NULL,\s*last_login_ip\s*=\s*\$2,\s*last_login_at\s*=\s*NOW\(\)\s+WHERE\s+user_id\s*=\s*\$1\s+AND\s+failed_login_attempts\s*>\s*0/; expect(mockDb.query).toHaveBeenCalledWith( // The test now verifies the full structure of the query. expect.stringMatching(expectedQueryRegex), ['user-123', '127.0.0.1'], ); }); it('should not throw an error if the database query fails (non-critical)', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect( adminRepo.resetFailedLoginAttempts('user-123', '127.0.0.1', mockLogger), ).resolves.toBeUndefined(); const { logger } = await import('../logger.server'); expect(logger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-123' }, 'Database error in resetFailedLoginAttempts', ); }); }); describe('incrementFailedLoginAttempts', () => { it('should execute an UPDATE query and return the new attempt count', async () => { // Mock the DB to return the new count mockDb.query.mockResolvedValue({ rows: [{ failed_login_attempts: 3 }], rowCount: 1, }); const newCount = await adminRepo.incrementFailedLoginAttempts('user-123', mockLogger); expect(newCount).toBe(3); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('RETURNING failed_login_attempts'), ['user-123'], ); }); it('should return 0 if the user is not found (rowCount is 0)', async () => { mockDb.query.mockResolvedValue({ rows: [], rowCount: 0 }); const newCount = await adminRepo.incrementFailedLoginAttempts('user-not-found', mockLogger); expect(newCount).toBe(0); expect(mockLogger.warn).toHaveBeenCalledWith( { userId: 'user-not-found' }, 'Attempted to increment failed login attempts for a non-existent user.', ); }); it('should return -1 if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); const newCount = await adminRepo.incrementFailedLoginAttempts('user-123', mockLogger); expect(newCount).toBe(-1); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-123' }, 'Database error in incrementFailedLoginAttempts', ); }); }); describe('updateBrandLogo', () => { it('should execute an UPDATE query for the brand logo', async () => { mockDb.query.mockResolvedValue({ rows: [] }); await adminRepo.updateBrandLogo(1, '/logo.png', mockLogger); expect(mockDb.query).toHaveBeenCalledWith( 'UPDATE public.brands SET logo_url = $1 WHERE brand_id = $2', ['/logo.png', 1], ); }); it('should throw NotFoundError if the brand is not found', async () => { mockDb.query.mockResolvedValue({ rowCount: 0 }); await expect(adminRepo.updateBrandLogo(999, '/logo.png', mockLogger)).rejects.toThrow( NotFoundError, ); await expect(adminRepo.updateBrandLogo(999, '/logo.png', mockLogger)).rejects.toThrow( 'Brand with ID 999 not found.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.updateBrandLogo(1, '/logo.png', mockLogger)).rejects.toThrow( 'Failed to update brand logo in database.', ); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.brands SET logo_url'), ['/logo.png', 1], ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, brandId: 1 }, 'Database error in updateBrandLogo', ); }); }); describe('updateReceiptStatus', () => { it('should update the receipt status and return the updated receipt', async () => { const mockReceipt = { receipt_id: 1, status: 'completed' }; mockDb.query.mockResolvedValue({ rows: [mockReceipt], rowCount: 1 }); const result = await adminRepo.updateReceiptStatus(1, 'completed', mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.receipts'), ['completed', 1], ); expect(result).toEqual(mockReceipt); }); it('should throw an error if the receipt is not found (rowCount is 0)', async () => { mockDb.query.mockResolvedValue({ rowCount: 0, rows: [] }); await expect(adminRepo.updateReceiptStatus(999, 'completed', mockLogger)).rejects.toThrow( NotFoundError, ); await expect(adminRepo.updateReceiptStatus(999, 'completed', mockLogger)).rejects.toThrow( 'Receipt with ID 999 not found.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.updateReceiptStatus(1, 'completed', mockLogger)).rejects.toThrow( 'Failed to update receipt status.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, receiptId: 1, status: 'completed' }, 'Database error in updateReceiptStatus', ); }); }); describe('getActivityLog', () => { it('should call the get_activity_log database function', async () => { mockDb.query.mockResolvedValue({ rows: [] }); await adminRepo.getActivityLog(50, 0, mockLogger); expect(mockDb.query).toHaveBeenCalledWith( 'SELECT * FROM public.get_activity_log($1, $2)', [50, 0], ); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.getActivityLog(50, 0, mockLogger)).rejects.toThrow( 'Failed to retrieve activity log.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, limit: 50, offset: 0 }, 'Database error in getActivityLog', ); }); }); describe('getAllUsers', () => { it('should return a list of all users for the admin view', async () => { const mockUsers: AdminUserView[] = [ createMockAdminUserView({ user_id: '1', email: 'test@test.com' }), ]; mockDb.query.mockResolvedValue({ rows: mockUsers }); const result = await adminRepo.getAllUsers(mockLogger); expect(mockDb.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.users u JOIN public.profiles p'), ); expect(result).toEqual(mockUsers); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.getAllUsers(mockLogger)).rejects.toThrow( 'Failed to retrieve all users.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError }, 'Database error in getAllUsers', ); }); }); describe('updateUserRole', () => { it('should update the user role and return the updated user', async () => { const mockProfile: Profile = createMockProfile({ role: 'admin' }); mockDb.query.mockResolvedValue({ rows: [mockProfile], rowCount: 1 }); const result = await adminRepo.updateUserRole('1', 'admin', mockLogger); expect(mockDb.query).toHaveBeenCalledWith( 'UPDATE public.profiles SET role = $1 WHERE user_id = $2 RETURNING *', ['admin', '1'], ); expect(result).toEqual(mockProfile); }); it('should throw an error if the user is not found (rowCount is 0)', async () => { mockDb.query.mockResolvedValue({ rowCount: 0, rows: [] }); await expect(adminRepo.updateUserRole('999', 'admin', mockLogger)).rejects.toThrow( 'User with ID 999 not found.', ); }); it('should re-throw a generic error if the database query fails for other reasons', async () => { const dbError = new Error('DB Error'); mockDb.query.mockRejectedValue(dbError); await expect(adminRepo.updateUserRole('1', 'admin', mockLogger)).rejects.toThrow('DB Error'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: '1', role: 'admin' }, 'Database error in updateUserRole', ); }); }); it('should throw ForeignKeyConstraintError if the user does not exist on update', async () => { const dbError = new Error('violates foreign key constraint'); // Create a more specific type for the error object to avoid using 'any' (dbError as Error & { code: string }).code = '23503'; mockDb.query.mockRejectedValue(dbError); await expect( adminRepo.updateUserRole('non-existent-user', 'admin', mockLogger), ).rejects.toThrow(ForeignKeyConstraintError); await expect( adminRepo.updateUserRole('non-existent-user', 'admin', mockLogger), ).rejects.toThrow('The specified user does not exist.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'non-existent-user', role: 'admin' }, 'Database error in updateUserRole', ); }); });