Refactor database tests to improve type safety and error handling
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled

- Updated mock implementations in various database service tests to use specific types (Pool, PoolClient) instead of 'any'.
- Enhanced error handling in tests by explicitly defining error types and codes, improving clarity and maintainability.
- Removed unnecessary eslint-disable comments related to 'any' usage.
- Cleaned up transaction mock implementations to ensure proper type casting.
- Adjusted error handling in the UserRepository to provide more specific error messages for foreign key constraints.
- Improved test assertions to ensure they are more robust and type-safe.
This commit is contained in:
2025-12-14 13:29:38 -08:00
parent 56f14f6342
commit edb0f8a38c
13 changed files with 130 additions and 123 deletions

View File

@@ -1,5 +1,5 @@
// src/services/backgroundJobService.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach, Mocked } from 'vitest';
import type { Logger } from 'pino';
// Use vi.hoisted to ensure the mock variable is available when vi.mock is executed.
@@ -28,6 +28,9 @@ vi.mock('../utils/dateUtils', () => ({
import { BackgroundJobService, startBackgroundJobs } from './backgroundJobService';
import type { Queue } from 'bullmq';
import type { PersonalizationRepository } from './db/personalization.db';
import type { NotificationRepository } from './db/notification.db';
import type { WatchedItemDeal } from '../types';
import { logger as globalMockLogger } from '../services/logger.server'; // Import the mocked logger
describe('Background Job Service', () => {
@@ -71,7 +74,12 @@ describe('Background Job Service', () => {
// Instantiate the service with mock dependencies for each test run
const mockServiceLogger = createMockLogger();
const service = new BackgroundJobService(mockPersonalizationRepo as any, mockNotificationRepo as any, mockEmailQueue as unknown as Queue<any>, mockServiceLogger as any);
const service = new BackgroundJobService(
mockPersonalizationRepo as unknown as PersonalizationRepository,
mockNotificationRepo as unknown as NotificationRepository,
mockEmailQueue as unknown as Queue,
mockServiceLogger
);
it('should do nothing if no deals are found for any user', async () => {
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
@@ -169,14 +177,14 @@ describe('Background Job Service', () => {
describe('startBackgroundJobs', () => {
const mockBackgroundJobService = {
runDailyDealCheck: vi.fn(),
} as unknown as BackgroundJobService;
} as unknown as Mocked<BackgroundJobService>;
const mockAnalyticsQueue = {
add: vi.fn(),
} as unknown as Queue;
} as unknown as Mocked<Queue>;
const mockWeeklyAnalyticsQueue = {
add: vi.fn(),
} as unknown as Queue;
} as unknown as Mocked<Queue>;
beforeEach(() => {
vi.clearAllMocks(); // Clear global mock logger calls too
@@ -187,7 +195,7 @@ describe('Background Job Service', () => {
});
it('should schedule three cron jobs with the correct schedules', () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
expect(mockCronSchedule).toHaveBeenCalledTimes(3);
expect(mockCronSchedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function));
@@ -196,7 +204,7 @@ describe('Background Job Service', () => {
});
it('should call runDailyDealCheck when the first cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
// Get the callback function for the first cron job
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
@@ -208,7 +216,7 @@ describe('Background Job Service', () => {
it('should log an error and release the lock if runDailyDealCheck fails', async () => {
const jobError = new Error('Cron job failed');
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockRejectedValue(jobError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
await dailyDealCheckCallback();
@@ -230,7 +238,7 @@ describe('Background Job Service', () => {
// Make the first call hang indefinitely
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(new Promise(() => {}));
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
// Trigger the job once, it will hang
@@ -245,7 +253,7 @@ describe('Background Job Service', () => {
});
it('should enqueue an analytics job when the second cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
await analyticsJobCallback();
@@ -256,7 +264,7 @@ describe('Background Job Service', () => {
it('should log an error if enqueuing the analytics job fails', async () => {
const queueError = new Error('Redis is down');
vi.mocked(mockAnalyticsQueue.add).mockRejectedValue(queueError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
await analyticsJobCallback();
@@ -266,7 +274,7 @@ describe('Background Job Service', () => {
});
it('should enqueue a weekly analytics job when the third cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
// The weekly job is the third one scheduled
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
@@ -282,7 +290,7 @@ describe('Background Job Service', () => {
it('should log an error if enqueuing the weekly analytics job fails', async () => {
const queueError = new Error('Redis is down for weekly job');
vi.mocked(mockWeeklyAnalyticsQueue.add).mockRejectedValue(queueError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger);
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
await weeklyAnalyticsJobCallback();

View File

@@ -1,5 +1,6 @@
// src/services/db/address.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Pool } from 'pg';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import { AddressRepository } from './address.db';
import type { Address } from '../../types';
@@ -19,7 +20,7 @@ describe('Address DB Service', () => {
beforeEach(() => {
vi.clearAllMocks();
addressRepo = new AddressRepository(mockPoolInstance as any);
addressRepo = new AddressRepository(mockPoolInstance as unknown as Pool);
});
describe('getAddressById', () => {
@@ -96,8 +97,8 @@ describe('Address DB Service', () => {
it('should throw UniqueConstraintError on duplicate address insert', async () => {
const newAddressData = { address_line_1: '123 Main St', city: 'Anytown' };
const dbError = new Error('duplicate key value violates unique constraint');
(dbError as any).code = '23505';
const dbError = new Error('duplicate key value violates unique constraint') as Error & { code: string };
dbError.code = '23505';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(addressRepo.upsertAddress(newAddressData, mockLogger)).rejects.toThrow(UniqueConstraintError);

View File

@@ -1,6 +1,7 @@
// src/services/db/admin.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Pool, PoolClient } from 'pg';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { AdminRepository } from './admin.db';
import type { SuggestedCorrection, AdminUserView, User } from '../../types';
@@ -36,10 +37,10 @@ describe('Admin DB Service', () => {
// Reset the withTransaction mock before each test
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
// Instantiate the repository with the mock pool for each test
adminRepo = new AdminRepository(mockPoolInstance as any);
adminRepo = new AdminRepository(mockPoolInstance as unknown as Pool);
});
describe('getSuggestedCorrections', () => {
@@ -282,13 +283,13 @@ describe('Admin DB Service', () => {
it('should execute a transaction to resolve an unmatched item', async () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id
.mockResolvedValueOnce({ rowCount: 1 }) // UPDATE flyer_items
.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE unmatched_flyer_items
return callback(mockClient as any);
(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
return callback(mockClient as unknown as PoolClient);
});
await adminRepo.resolveUnmatchedFlyerItem(1, 101, mockLogger);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
@@ -300,8 +301,8 @@ describe('Admin DB Service', () => {
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.mockResolvedValueOnce({ rowCount: 0, rows: [] }); // SELECT finds nothing
await expect(callback(mockClient as any)).rejects.toThrow(NotFoundError);
(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
});
@@ -313,10 +314,10 @@ describe('Admin DB Service', () => {
const dbError = new Error('DB Error');
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
(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 any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError; // Re-throw for the outer expect
});
@@ -481,7 +482,8 @@ describe('Admin DB Service', () => {
it('should throw ForeignKeyConstraintError if the user does not exist on update', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
// Create a more specific type for the error object to avoid using 'any'
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(adminRepo.updateUserRole('non-existent-user', 'admin', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);

View File

@@ -390,7 +390,6 @@ export class AdminRepository {
* Defines a type for JSON-compatible data structures, allowing for nested objects and arrays.
* This provides a safer alternative to `any` for objects intended for JSON serialization.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async logActivity(logData: {
userId?: string | null;
action: string;

View File

@@ -1,11 +1,12 @@
// src/services/db/budget.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { ForeignKeyConstraintError } from './errors.db';
// Un-mock the module we are testing to ensure we use the real implementation.
vi.unmock('./budget.db');
import { BudgetRepository } from './budget.db';
import type { Pool, PoolClient } from 'pg';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Budget, SpendingByCategory } from '../../types';
@@ -43,7 +44,7 @@ describe('Budget DB Service', () => {
beforeEach(() => {
vi.clearAllMocks();
// Instantiate the repository with the mock pool for each test
budgetRepo = new BudgetRepository(mockPoolInstance as any);
budgetRepo = new BudgetRepository(mockPoolInstance as unknown as Pool);
});
describe('getBudgetsForUser', () => {
@@ -76,19 +77,22 @@ describe('Budget DB Service', () => {
it('should execute an INSERT query and return the new budget', async () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
const mockCreatedBudget: Budget = { budget_id: 1, user_id: 'user-123', ...budgetData };
// Create a mock client that we can reference both inside and outside the transaction mock.
const mockClient = { query: vi.fn() };
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
// Configure the mock client's query behavior for this specific test.
(mockClient.query as Mock)
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // For the INSERT...RETURNING
.mockResolvedValueOnce({ rows: [] }); // For award_achievement
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
const result = await budgetRepo.createBudget('user-123', budgetData, mockLogger);
// Now we can assert directly on the mockClient we created.
const { GamificationRepository } = await import('./gamification.db');
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.budgets'), expect.any(Array));
expect(GamificationRepository.prototype.awardAchievement).toHaveBeenCalledWith('user-123', 'First Budget Created', mockLogger);
expect(result).toEqual(mockCreatedBudget);
@@ -98,12 +102,12 @@ describe('Budget DB Service', () => {
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query.mockRejectedValueOnce(dbError); // INSERT fails
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError; // Re-throw for the outer expect
});
@@ -120,7 +124,7 @@ describe('Budget DB Service', () => {
mockClient.query
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // INSERT...RETURNING
.mockRejectedValueOnce(achievementError); // award_achievement fails
await expect(callback(mockClient as any)).rejects.toThrow(achievementError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(achievementError);
throw achievementError; // Re-throw for the outer expect
});
@@ -134,7 +138,7 @@ describe('Budget DB Service', () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query.mockRejectedValueOnce(dbError); // INSERT fails
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError; // Re-throw for the outer expect
});
await expect(budgetRepo.createBudget('user-123', budgetData, mockLogger)).rejects.toThrow('Failed to create budget.');

View File

@@ -1,6 +1,7 @@
// src/services/db/flyer.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Pool, PoolClient } from 'pg';
import { createMockFlyer, createMockFlyerItem, createMockBrand } from '../../tests/utils/mockFactories';
// Un-mock the module we are testing to ensure we use the real implementation
@@ -32,10 +33,10 @@ describe('Flyer DB Service', () => {
// In a transaction, `pool.connect()` returns a client. That client has a `release` method.
// For these tests, we simulate this by having `connect` resolve to the pool instance itself,
// and we ensure the `release` method is mocked on that instance.
const mockClient = { ...mockPoolInstance, release: vi.fn() };
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
const mockClient = { ...mockPoolInstance, release: vi.fn() } as unknown as PoolClient;
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient);
flyerRepo = new FlyerRepository(mockPoolInstance as any);
flyerRepo = new FlyerRepository(mockPoolInstance as unknown as Pool);
});
describe('findOrCreateStore', () => {
@@ -57,7 +58,7 @@ describe('Flyer DB Service', () => {
it('should handle race condition where store is created between SELECT and INSERT', async () => {
const uniqueConstraintError = new Error('duplicate key value violates unique constraint');
(uniqueConstraintError as any).code = '23505';
(uniqueConstraintError as Error & { code: string }).code = '23505';
mockPoolInstance.query
.mockResolvedValueOnce({ rows: [] }) // First SELECT finds nothing
@@ -78,7 +79,7 @@ describe('Flyer DB Service', () => {
it('should throw an error if race condition recovery fails', async () => {
const uniqueConstraintError = new Error('duplicate key value violates unique constraint');
(uniqueConstraintError as any).code = '23505';
(uniqueConstraintError as Error & { code: string }).code = '23505';
mockPoolInstance.query
.mockResolvedValueOnce({ rows: [] }) // First SELECT
@@ -131,7 +132,7 @@ describe('Flyer DB Service', () => {
it('should throw UniqueConstraintError on duplicate checksum', async () => {
const flyerData: FlyerDbInsert = { checksum: 'duplicate-checksum' } as FlyerDbInsert;
const dbError = new Error('duplicate key value violates unique constraint "flyers_checksum_key"');
(dbError as any).code = '23505';
(dbError as Error & { code: string }).code = '23505';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(flyerRepo.insertFlyer(flyerData, mockLogger)).rejects.toThrow(UniqueConstraintError);
@@ -178,7 +179,7 @@ describe('Flyer DB Service', () => {
it('should throw ForeignKeyConstraintError if flyerId is invalid', async () => {
const itemsData: FlyerItemInsert[] = [{ item: 'Test', price_display: '$1', price_in_cents: 100, quantity: '1', category_name: 'Test', view_count: 0, click_count: 0 }];
const dbError = new Error('insert or update on table "flyer_items" violates foreign key constraint "flyer_items_flyer_id_fkey"');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(flyerRepo.insertFlyerItems(999, itemsData, mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
@@ -217,7 +218,7 @@ describe('Flyer DB Service', () => {
.mockResolvedValueOnce({ rows: [mockFlyer] }) // insertFlyer
.mockResolvedValueOnce({ rows: mockItems }); // insertFlyerItems
return callback(mockClient as any);
});
}); // Cast to any is acceptable here as we are mocking the implementation
const result = await createFlyerAndItems(flyerData, itemsData, mockLogger);
@@ -228,10 +229,10 @@ describe('Flyer DB Service', () => {
expect(withTransaction).toHaveBeenCalledTimes(1);
// Verify the individual functions were called with the client
const callback = vi.mocked(withTransaction).mock.calls[0][0];
const callback = (vi.mocked(withTransaction) as Mock).mock.calls[0][0];
const mockClient = { query: vi.fn() };
mockClient.query.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }).mockResolvedValueOnce({ rows: [mockFlyer] }).mockResolvedValueOnce({ rows: mockItems });
await callback(mockClient as any);
await callback(mockClient as unknown as PoolClient);
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('SELECT store_id FROM public.stores'), ['Transaction Store']);
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO flyers'), expect.any(Array));
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO flyer_items'), expect.any(Array));
@@ -249,7 +250,7 @@ describe('Flyer DB Service', () => {
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }) // findOrCreateStore
.mockRejectedValueOnce(dbError); // insertFlyer fails
// The withTransaction helper will catch this and roll back
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
// re-throw because withTransaction re-throws
throw dbError;
});
@@ -446,15 +447,16 @@ describe('Flyer DB Service', () => {
describe('deleteFlyer', () => {
it('should use withTransaction to delete a flyer', async () => {
// Create a mock client that we can reference both inside and outside the transaction mock.
const mockClient = { query: vi.fn().mockResolvedValue({ rowCount: 1 }) };
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockResolvedValue({ rowCount: 1 }) };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
await flyerRepo.deleteFlyer(42, mockLogger);
expect(withTransaction).toHaveBeenCalledTimes(1);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM public.flyers WHERE flyer_id = $1', [42]);
});
@@ -462,7 +464,7 @@ describe('Flyer DB Service', () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockResolvedValue({ rowCount: 0 }) };
// The callback will throw NotFoundError, and withTransaction will re-throw it.
await expect(callback(mockClient as any)).rejects.toThrow(NotFoundError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(NotFoundError);
throw new NotFoundError('Simulated re-throw');
});
@@ -474,7 +476,7 @@ describe('Flyer DB Service', () => {
const dbError = new Error('DB Error');
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});

View File

@@ -1,6 +1,7 @@
// src/services/db/personalization.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Pool, PoolClient } from 'pg';
import { withTransaction } from './connection.db';
import {
PersonalizationRepository} from './personalization.db';
@@ -37,10 +38,10 @@ describe('Personalization DB Service', () => {
// Reset the withTransaction mock before each test
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
// Instantiate the repository with the mock pool for each test
personalizationRepo = new PersonalizationRepository(mockPoolInstance as any);
personalizationRepo = new PersonalizationRepository(mockPoolInstance as unknown as Pool);
});
describe('getAllMasterItems', () => {
@@ -103,7 +104,7 @@ describe('Personalization DB Service', () => {
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
.mockResolvedValueOnce({ rows: [mockItem] }) // Find master item
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
await personalizationRepo.addWatchedItem('user-123', 'New Item', 'Produce', mockLogger);
@@ -124,7 +125,7 @@ describe('Personalization DB Service', () => {
.mockResolvedValueOnce({ rows: [] }) // Find master item (not found)
.mockResolvedValueOnce({ rows: [mockNewItem] }) // INSERT new master item
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
const result = await personalizationRepo.addWatchedItem('user-123', 'Brand New Item', 'Produce', mockLogger);
@@ -142,7 +143,7 @@ describe('Personalization DB Service', () => {
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
.mockResolvedValueOnce({ rows: [mockExistingItem] }) // Find master item
.mockResolvedValueOnce({ rows: [] }); // INSERT...ON CONFLICT
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
// The function should resolve successfully without throwing an error.
@@ -153,7 +154,7 @@ describe('Personalization DB Service', () => {
it('should throw an error if the category is not found', async () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockResolvedValue({ rows: [] }) };
await expect(callback(mockClient as any)).rejects.toThrow("Category 'Fake Category' not found.");
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow("Category 'Fake Category' not found.");
throw new Error("Category 'Fake Category' not found.");
});
@@ -166,7 +167,7 @@ describe('Personalization DB Service', () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }).mockRejectedValueOnce(dbError);
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});
@@ -176,7 +177,7 @@ describe('Personalization DB Service', () => {
it('should throw ForeignKeyConstraintError on invalid user or category', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
vi.mocked(withTransaction).mockRejectedValue(dbError);
await expect(personalizationRepo.addWatchedItem('non-existent-user', 'Some Item', 'Produce', mockLogger)).rejects.toThrow('The specified user or category does not exist.');
@@ -358,7 +359,7 @@ describe('Personalization DB Service', () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
await personalizationRepo.setUserDietaryRestrictions('user-123', [1, 2], mockLogger);
@@ -370,11 +371,11 @@ describe('Personalization DB Service', () => {
it('should throw ForeignKeyConstraintError if a restriction ID is invalid', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError); // DELETE ok, INSERT fail
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});
@@ -385,7 +386,7 @@ describe('Personalization DB Service', () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
await personalizationRepo.setUserDietaryRestrictions('user-123', [], mockLogger);
@@ -455,7 +456,7 @@ describe('Personalization DB Service', () => {
mockClientQuery
.mockResolvedValueOnce({ rows: [] }) // DELETE
.mockResolvedValueOnce({ rows: mockNewAppliances }); // INSERT
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
const result = await personalizationRepo.setUserAppliances('user-123', [1, 2], mockLogger);
@@ -468,11 +469,11 @@ describe('Personalization DB Service', () => {
it('should throw ForeignKeyConstraintError if an appliance ID is invalid', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError); // DELETE ok, INSERT fail
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});
@@ -483,7 +484,7 @@ describe('Personalization DB Service', () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
const result = await personalizationRepo.setUserAppliances('user-123', [], mockLogger);
@@ -499,7 +500,7 @@ describe('Personalization DB Service', () => {
const dbError = new Error('DB Error');
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});

View File

@@ -1,6 +1,7 @@
// src/services/db/recipe.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Pool } from 'pg';
import { RecipeRepository } from './recipe.db';
// Un-mock the module we are testing to ensure we use the real implementation.
@@ -27,7 +28,7 @@ describe('Recipe DB Service', () => {
beforeEach(() => {
vi.clearAllMocks();
// Instantiate the repository with the mock pool for each test
recipeRepo = new RecipeRepository(mockPoolInstance as any);
recipeRepo = new RecipeRepository(mockPoolInstance as unknown as Pool);
});
describe('getRecipesBySalePercentage', () => {
@@ -130,7 +131,7 @@ describe('Recipe DB Service', () => {
it('should throw ForeignKeyConstraintError if user or recipe does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockQuery.mockRejectedValue(dbError);
await expect(recipeRepo.addFavoriteRecipe('user-123', 999, mockLogger)).rejects.toThrow('The specified user or recipe does not exist.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', recipeId: 999 }, 'Database error in addFavoriteRecipe');
@@ -284,7 +285,7 @@ describe('Recipe DB Service', () => {
it('should throw ForeignKeyConstraintError if recipe, user, or parent comment does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockQuery.mockRejectedValue(dbError);
await expect(recipeRepo.addRecipeComment(999, 'user-123', 'Fail', mockLogger)).rejects.toThrow('The specified recipe, user, or parent comment does not exist.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, recipeId: 999, userId: 'user-123', parentCommentId: undefined }, 'Database error in addRecipeComment');
@@ -310,7 +311,7 @@ describe('Recipe DB Service', () => {
it('should re-throw the specific error message from the database function', async () => {
const dbError = new Error('Recipe is not public and cannot be forked.');
(dbError as any).code = 'P0001'; // raise_exception
(dbError as Error & { code: string }).code = 'P0001'; // raise_exception
mockQuery.mockRejectedValue(dbError);
await expect(recipeRepo.forkRecipe('user-123', 1, mockLogger)).rejects.toThrow('Recipe is not public and cannot be forked.');

View File

@@ -1,6 +1,7 @@
// src/services/db/shopping.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Pool, PoolClient } from 'pg';
import { withTransaction } from './connection.db';
import { createMockShoppingList, createMockShoppingListItem } from '../../tests/utils/mockFactories';
@@ -8,7 +9,7 @@ import { createMockShoppingList, createMockShoppingListItem } from '../../tests/
vi.unmock('./shopping.db');
import { ShoppingRepository } from './shopping.db';
import { ForeignKeyConstraintError, UniqueConstraintError, NotFoundError } from './errors.db';
import { ForeignKeyConstraintError, UniqueConstraintError } from './errors.db';
// Mock the logger to prevent console output during tests
vi.mock('../logger.server', () => ({
@@ -33,7 +34,7 @@ describe('Shopping DB Service', () => {
beforeEach(() => {
vi.clearAllMocks();
// Instantiate the repository with the mock pool for each test
shoppingRepo = new ShoppingRepository(mockPoolInstance as any);
shoppingRepo = new ShoppingRepository(mockPoolInstance as unknown as Pool);
});
describe('getShoppingLists', () => {
@@ -104,7 +105,7 @@ describe('Shopping DB Service', () => {
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const dbError = new Error('insert or update on table "shopping_lists" violates foreign key constraint "shopping_lists_user_id_fkey"');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createShoppingList('non-existent-user', 'Wont work', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
});
@@ -174,7 +175,7 @@ describe('Shopping DB Service', () => {
it('should throw ForeignKeyConstraintError if list or master item does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.addShoppingListItem(999, { masterItemId: 999 }, mockLogger)).rejects.toThrow('Referenced list or item does not exist.');
});
@@ -263,7 +264,7 @@ describe('Shopping DB Service', () => {
it('should throw ForeignKeyConstraintError if the shopping list does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.completeShoppingList(999, 'user-123', mockLogger)).rejects.toThrow('The specified shopping list does not exist.');
});
@@ -356,14 +357,14 @@ describe('Shopping DB Service', () => {
it('should throw UniqueConstraintError on duplicate name', async () => {
const dbError = new Error('duplicate key value violates unique constraint');
(dbError as any).code = '23505';
(dbError as Error & { code: string }).code = '23505';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createPantryLocation('user-1', 'Fridge', mockLogger)).rejects.toThrow(UniqueConstraintError);
});
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createPantryLocation('non-existent-user', 'Pantry', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
});
@@ -410,7 +411,7 @@ describe('Shopping DB Service', () => {
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createReceipt('non-existent-user', 'url', mockLogger)).rejects.toThrow('User not found');
});
@@ -451,7 +452,7 @@ describe('Shopping DB Service', () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
const items = [{ raw_item_description: 'Milk', price_paid_cents: 399 }];
@@ -470,7 +471,7 @@ describe('Shopping DB Service', () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
// The callback will throw, and withTransaction will catch and re-throw
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});

View File

@@ -73,7 +73,7 @@ export class ShoppingRepository {
return { ...res.rows[0], items: [] };
} catch (error) {
// The patch requested this specific error handling.
if ((error as any).code === '23503') {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('The specified user does not exist.');
}
logger.error({ err: error, userId, name }, 'Database error in createShoppingList');
@@ -161,7 +161,7 @@ export class ShoppingRepository {
return res.rows[0];
} catch (error) {
// The patch requested this specific error handling.
if ((error as any).code === '23503') {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('Referenced list or item does not exist.');
}
logger.error({ err: error, listId, item }, 'Database error in addShoppingListItem');
@@ -248,9 +248,9 @@ export class ShoppingRepository {
);
return res.rows[0];
} catch (error) {
if ((error as any).code === '23505') {
if (error instanceof Error && 'code' in error && error.code === '23505') {
throw new UniqueConstraintError('A pantry location with this name already exists.');
} else if ((error as any).code === '23503') {
} else if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('User not found');
}
logger.error({ err: error, userId, name }, 'Database error in createPantryLocation');
@@ -325,7 +325,7 @@ export class ShoppingRepository {
return res.rows[0].complete_shopping_list;
} catch (error) {
// The patch requested this specific error handling.
if ((error as any).code === '23503') {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('The specified shopping list does not exist.');
}
logger.error({ err: error, shoppingListId, userId }, 'Database error in completeShoppingList');
@@ -388,7 +388,7 @@ export class ShoppingRepository {
return res.rows[0];
} catch (error) {
// The patch requested this specific error handling.
if ((error as any).code === '23503') {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('User not found');
}
logger.error({ err: error, userId, receiptImageUrl }, 'Database error in createReceipt');

View File

@@ -1,11 +1,4 @@
// src/services/db/user.db.test.ts
// --- FIX REGISTRY ---
//
// 2025-12-09: Corrected transaction rollback tests to expect generic error messages.
// Updated `updateUserProfile` and `exportUserData` tests to use more robust
// mocking strategies for internal method calls (spying on prototypes).
//
// --- END FIX REGISTRY ---
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { PoolClient } from 'pg';
@@ -60,9 +53,9 @@ describe('User DB Service', () => {
beforeEach(() => {
vi.clearAllMocks();
userRepo = new UserRepository(mockPoolInstance as any);
userRepo = new UserRepository(mockPoolInstance as unknown as PoolClient);
// Provide a default mock implementation for withTransaction for all tests.
vi.mocked(withTransaction).mockImplementation(async (callback: (client: PoolClient) => Promise<any>) => callback(mockPoolInstance as any));
vi.mocked(withTransaction).mockImplementation(async (callback: (client: PoolClient) => Promise<unknown>) => callback(mockPoolInstance as unknown as PoolClient));
});
describe('findUserByEmail', () => {
@@ -113,7 +106,7 @@ describe('User DB Service', () => {
.mockResolvedValueOnce({ rows: [] }) // set_config
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
.mockResolvedValueOnce({ rows: [mockDbProfile] }); // SELECT profile
return callback(mockClient as any);
return callback(mockClient as unknown as PoolClient);
});
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' }, mockLogger);
@@ -127,7 +120,7 @@ describe('User DB Service', () => {
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query.mockRejectedValueOnce(dbError); // set_config or INSERT fails
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});
@@ -144,7 +137,7 @@ describe('User DB Service', () => {
.mockResolvedValueOnce({ rows: [] }) // set_config
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
.mockRejectedValueOnce(dbError); // SELECT profile fails
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
throw dbError;
});
@@ -154,7 +147,7 @@ describe('User DB Service', () => {
it('should throw UniqueConstraintError if the email already exists', async () => {
const dbError = new Error('duplicate key value violates unique constraint');
(dbError as any).code = '23505';
(dbError as Error & { code: string }).code = '23505';
vi.mocked(withTransaction).mockRejectedValue(dbError);
@@ -417,7 +410,7 @@ describe('User DB Service', () => {
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(userRepo.createPasswordResetToken('non-existent-user', 'hash', new Date(), mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
});
@@ -471,12 +464,12 @@ describe('User DB Service', () => {
const { ShoppingRepository } = await import('./shopping.db');
const { PersonalizationRepository } = await import('./personalization.db');
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById')
.mockResolvedValue({ user_id: '123' } as any);
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems')
.mockResolvedValue([]);
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists')
.mockResolvedValue([]);
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById');
findProfileSpy.mockResolvedValue({ user_id: '123' } as Profile);
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems');
getWatchedItemsSpy.mockResolvedValue([]);
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists');
getShoppingListsSpy.mockResolvedValue([]);
await exportUserData('123', mockLogger);
@@ -522,7 +515,7 @@ describe('User DB Service', () => {
it('should throw ForeignKeyConstraintError if a user does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(userRepo.followUser('follower-1', 'non-existent-user', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
});

View File

@@ -452,9 +452,6 @@ export class UserRepository {
[followerId, followingId]
);
} catch (error) {
if ((error as any).code === '23503') {
throw new ForeignKeyConstraintError('User not found.');
}
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('One or both users do not exist.');
}

View File

@@ -51,9 +51,7 @@ describe('parsePriceToCents', () => {
it('should return null for empty or invalid inputs', () => {
expect(parsePriceToCents('')).toBeNull();
expect(parsePriceToCents(' ')).toBeNull();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(parsePriceToCents(null as any)).toBeNull();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(parsePriceToCents(undefined as any)).toBeNull();
});