Refactor database tests to improve type safety and error handling
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
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:
@@ -1,5 +1,5 @@
|
|||||||
// src/services/backgroundJobService.test.ts
|
// 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';
|
import type { Logger } from 'pino';
|
||||||
|
|
||||||
// Use vi.hoisted to ensure the mock variable is available when vi.mock is executed.
|
// 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 { BackgroundJobService, startBackgroundJobs } from './backgroundJobService';
|
||||||
import type { Queue } from 'bullmq';
|
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
|
import { logger as globalMockLogger } from '../services/logger.server'; // Import the mocked logger
|
||||||
|
|
||||||
describe('Background Job Service', () => {
|
describe('Background Job Service', () => {
|
||||||
@@ -71,7 +74,12 @@ describe('Background Job Service', () => {
|
|||||||
|
|
||||||
// Instantiate the service with mock dependencies for each test run
|
// Instantiate the service with mock dependencies for each test run
|
||||||
const mockServiceLogger = createMockLogger();
|
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 () => {
|
it('should do nothing if no deals are found for any user', async () => {
|
||||||
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
|
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
|
||||||
@@ -169,14 +177,14 @@ describe('Background Job Service', () => {
|
|||||||
describe('startBackgroundJobs', () => {
|
describe('startBackgroundJobs', () => {
|
||||||
const mockBackgroundJobService = {
|
const mockBackgroundJobService = {
|
||||||
runDailyDealCheck: vi.fn(),
|
runDailyDealCheck: vi.fn(),
|
||||||
} as unknown as BackgroundJobService;
|
} as unknown as Mocked<BackgroundJobService>;
|
||||||
|
|
||||||
const mockAnalyticsQueue = {
|
const mockAnalyticsQueue = {
|
||||||
add: vi.fn(),
|
add: vi.fn(),
|
||||||
} as unknown as Queue;
|
} as unknown as Mocked<Queue>;
|
||||||
const mockWeeklyAnalyticsQueue = {
|
const mockWeeklyAnalyticsQueue = {
|
||||||
add: vi.fn(),
|
add: vi.fn(),
|
||||||
} as unknown as Queue;
|
} as unknown as Mocked<Queue>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks(); // Clear global mock logger calls too
|
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', () => {
|
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).toHaveBeenCalledTimes(3);
|
||||||
expect(mockCronSchedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function));
|
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 () => {
|
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
|
// Get the callback function for the first cron job
|
||||||
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
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 () => {
|
it('should log an error and release the lock if runDailyDealCheck fails', async () => {
|
||||||
const jobError = new Error('Cron job failed');
|
const jobError = new Error('Cron job failed');
|
||||||
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockRejectedValue(jobError);
|
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];
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
||||||
await dailyDealCheckCallback();
|
await dailyDealCheckCallback();
|
||||||
@@ -230,7 +238,7 @@ describe('Background Job Service', () => {
|
|||||||
// Make the first call hang indefinitely
|
// Make the first call hang indefinitely
|
||||||
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(new Promise(() => {}));
|
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];
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
||||||
|
|
||||||
// Trigger the job once, it will hang
|
// 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 () => {
|
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];
|
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
||||||
await analyticsJobCallback();
|
await analyticsJobCallback();
|
||||||
@@ -256,7 +264,7 @@ describe('Background Job Service', () => {
|
|||||||
it('should log an error if enqueuing the analytics job fails', async () => {
|
it('should log an error if enqueuing the analytics job fails', async () => {
|
||||||
const queueError = new Error('Redis is down');
|
const queueError = new Error('Redis is down');
|
||||||
vi.mocked(mockAnalyticsQueue.add).mockRejectedValue(queueError);
|
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];
|
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
||||||
await analyticsJobCallback();
|
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 () => {
|
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
|
// The weekly job is the third one scheduled
|
||||||
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
|
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 () => {
|
it('should log an error if enqueuing the weekly analytics job fails', async () => {
|
||||||
const queueError = new Error('Redis is down for weekly job');
|
const queueError = new Error('Redis is down for weekly job');
|
||||||
vi.mocked(mockWeeklyAnalyticsQueue.add).mockRejectedValue(queueError);
|
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];
|
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
|
||||||
await weeklyAnalyticsJobCallback();
|
await weeklyAnalyticsJobCallback();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/services/db/address.db.test.ts
|
// src/services/db/address.db.test.ts
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import type { Pool } from 'pg';
|
||||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
import { AddressRepository } from './address.db';
|
import { AddressRepository } from './address.db';
|
||||||
import type { Address } from '../../types';
|
import type { Address } from '../../types';
|
||||||
@@ -19,7 +20,7 @@ describe('Address DB Service', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
addressRepo = new AddressRepository(mockPoolInstance as any);
|
addressRepo = new AddressRepository(mockPoolInstance as unknown as Pool);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAddressById', () => {
|
describe('getAddressById', () => {
|
||||||
@@ -96,8 +97,8 @@ describe('Address DB Service', () => {
|
|||||||
|
|
||||||
it('should throw UniqueConstraintError on duplicate address insert', async () => {
|
it('should throw UniqueConstraintError on duplicate address insert', async () => {
|
||||||
const newAddressData = { address_line_1: '123 Main St', city: 'Anytown' };
|
const newAddressData = { address_line_1: '123 Main St', city: 'Anytown' };
|
||||||
const dbError = new Error('duplicate key value violates unique constraint');
|
const dbError = new Error('duplicate key value violates unique constraint') as Error & { code: string };
|
||||||
(dbError as any).code = '23505';
|
dbError.code = '23505';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
|
|
||||||
await expect(addressRepo.upsertAddress(newAddressData, mockLogger)).rejects.toThrow(UniqueConstraintError);
|
await expect(addressRepo.upsertAddress(newAddressData, mockLogger)).rejects.toThrow(UniqueConstraintError);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/services/db/admin.db.test.ts
|
// 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 { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
|
import type { Pool, PoolClient } from 'pg';
|
||||||
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
||||||
import { AdminRepository } from './admin.db';
|
import { AdminRepository } from './admin.db';
|
||||||
import type { SuggestedCorrection, AdminUserView, User } from '../../types';
|
import type { SuggestedCorrection, AdminUserView, User } from '../../types';
|
||||||
@@ -36,10 +37,10 @@ describe('Admin DB Service', () => {
|
|||||||
// Reset the withTransaction mock before each test
|
// Reset the withTransaction mock before each test
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
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
|
// 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', () => {
|
describe('getSuggestedCorrections', () => {
|
||||||
@@ -282,13 +283,13 @@ describe('Admin DB Service', () => {
|
|||||||
it('should execute a transaction to resolve an unmatched item', async () => {
|
it('should execute a transaction to resolve an unmatched item', async () => {
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query
|
(mockClient.query as Mock)
|
||||||
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id
|
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id from unmatched_flyer_items
|
||||||
.mockResolvedValueOnce({ rowCount: 1 }) // UPDATE flyer_items
|
.mockResolvedValueOnce({ rowCount: 1 }) // UPDATE flyer_items table
|
||||||
.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE unmatched_flyer_items
|
.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE unmatched_flyer_items table
|
||||||
return callback(mockClient as any);
|
return callback(mockClient as unknown as PoolClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
await adminRepo.resolveUnmatchedFlyerItem(1, 101, mockLogger);
|
await adminRepo.resolveUnmatchedFlyerItem(1, 101, mockLogger);
|
||||||
|
|
||||||
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
|
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 () => {
|
it('should throw NotFoundError if the unmatched item is not found', async () => {
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockResolvedValueOnce({ rowCount: 0, rows: [] }); // SELECT finds nothing
|
(mockClient.query as Mock).mockResolvedValueOnce({ rowCount: 0, rows: [] }); // SELECT finds nothing
|
||||||
await expect(callback(mockClient as any)).rejects.toThrow(NotFoundError);
|
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
|
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');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query
|
(mockClient.query as Mock)
|
||||||
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id
|
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id
|
||||||
.mockRejectedValueOnce(dbError); // UPDATE flyer_items fails
|
.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
|
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 () => {
|
it('should throw ForeignKeyConstraintError if the user does not exist on update', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
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);
|
mockPoolInstance.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(ForeignKeyConstraintError);
|
||||||
|
|||||||
@@ -390,7 +390,6 @@ export class AdminRepository {
|
|||||||
* Defines a type for JSON-compatible data structures, allowing for nested objects and arrays.
|
* 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.
|
* 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: {
|
async logActivity(logData: {
|
||||||
userId?: string | null;
|
userId?: string | null;
|
||||||
action: string;
|
action: string;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// src/services/db/budget.db.test.ts
|
// src/services/db/budget.db.test.ts
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||||
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
import { ForeignKeyConstraintError } from './errors.db';
|
||||||
|
|
||||||
// Un-mock the module we are testing to ensure we use the real implementation.
|
// Un-mock the module we are testing to ensure we use the real implementation.
|
||||||
vi.unmock('./budget.db');
|
vi.unmock('./budget.db');
|
||||||
|
|
||||||
import { BudgetRepository } from './budget.db';
|
import { BudgetRepository } from './budget.db';
|
||||||
|
import type { Pool, PoolClient } from 'pg';
|
||||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
import type { Budget, SpendingByCategory } from '../../types';
|
import type { Budget, SpendingByCategory } from '../../types';
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ describe('Budget DB Service', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Instantiate the repository with the mock pool for each test
|
// 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', () => {
|
describe('getBudgetsForUser', () => {
|
||||||
@@ -76,19 +77,22 @@ describe('Budget DB Service', () => {
|
|||||||
it('should execute an INSERT query and return the new budget', async () => {
|
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 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 };
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
// Configure the mock client's query behavior for this specific test.
|
||||||
mockClient.query
|
(mockClient.query as Mock)
|
||||||
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // For the INSERT...RETURNING
|
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // For the INSERT...RETURNING
|
||||||
.mockResolvedValueOnce({ rows: [] }); // For award_achievement
|
.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);
|
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 { 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(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.budgets'), expect.any(Array));
|
||||||
expect(GamificationRepository.prototype.awardAchievement).toHaveBeenCalledWith('user-123', 'First Budget Created', mockLogger);
|
expect(GamificationRepository.prototype.awardAchievement).toHaveBeenCalledWith('user-123', 'First Budget Created', mockLogger);
|
||||||
expect(result).toEqual(mockCreatedBudget);
|
expect(result).toEqual(mockCreatedBudget);
|
||||||
@@ -98,12 +102,12 @@ describe('Budget DB Service', () => {
|
|||||||
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
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 budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
|
||||||
const dbError = new Error('violates foreign key constraint');
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockRejectedValueOnce(dbError); // INSERT fails
|
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
|
throw dbError; // Re-throw for the outer expect
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +124,7 @@ describe('Budget DB Service', () => {
|
|||||||
mockClient.query
|
mockClient.query
|
||||||
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // INSERT...RETURNING
|
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // INSERT...RETURNING
|
||||||
.mockRejectedValueOnce(achievementError); // award_achievement fails
|
.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
|
throw achievementError; // Re-throw for the outer expect
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,7 +138,7 @@ describe('Budget DB Service', () => {
|
|||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockRejectedValueOnce(dbError); // INSERT fails
|
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
|
throw dbError; // Re-throw for the outer expect
|
||||||
});
|
});
|
||||||
await expect(budgetRepo.createBudget('user-123', budgetData, mockLogger)).rejects.toThrow('Failed to create budget.');
|
await expect(budgetRepo.createBudget('user-123', budgetData, mockLogger)).rejects.toThrow('Failed to create budget.');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/services/db/flyer.db.test.ts
|
// 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 { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
|
import type { Pool, PoolClient } from 'pg';
|
||||||
import { createMockFlyer, createMockFlyerItem, createMockBrand } from '../../tests/utils/mockFactories';
|
import { createMockFlyer, createMockFlyerItem, createMockBrand } from '../../tests/utils/mockFactories';
|
||||||
|
|
||||||
// Un-mock the module we are testing to ensure we use the real implementation
|
// 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.
|
// 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,
|
// 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.
|
// and we ensure the `release` method is mocked on that instance.
|
||||||
const mockClient = { ...mockPoolInstance, release: vi.fn() };
|
const mockClient = { ...mockPoolInstance, release: vi.fn() } as unknown as PoolClient;
|
||||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient);
|
||||||
|
|
||||||
flyerRepo = new FlyerRepository(mockPoolInstance as any);
|
flyerRepo = new FlyerRepository(mockPoolInstance as unknown as Pool);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findOrCreateStore', () => {
|
describe('findOrCreateStore', () => {
|
||||||
@@ -57,7 +58,7 @@ describe('Flyer DB Service', () => {
|
|||||||
|
|
||||||
it('should handle race condition where store is created between SELECT and INSERT', async () => {
|
it('should handle race condition where store is created between SELECT and INSERT', async () => {
|
||||||
const uniqueConstraintError = new Error('duplicate key value violates unique constraint');
|
const uniqueConstraintError = new Error('duplicate key value violates unique constraint');
|
||||||
(uniqueConstraintError as any).code = '23505';
|
(uniqueConstraintError as Error & { code: string }).code = '23505';
|
||||||
|
|
||||||
mockPoolInstance.query
|
mockPoolInstance.query
|
||||||
.mockResolvedValueOnce({ rows: [] }) // First SELECT finds nothing
|
.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 () => {
|
it('should throw an error if race condition recovery fails', async () => {
|
||||||
const uniqueConstraintError = new Error('duplicate key value violates unique constraint');
|
const uniqueConstraintError = new Error('duplicate key value violates unique constraint');
|
||||||
(uniqueConstraintError as any).code = '23505';
|
(uniqueConstraintError as Error & { code: string }).code = '23505';
|
||||||
|
|
||||||
mockPoolInstance.query
|
mockPoolInstance.query
|
||||||
.mockResolvedValueOnce({ rows: [] }) // First SELECT
|
.mockResolvedValueOnce({ rows: [] }) // First SELECT
|
||||||
@@ -131,7 +132,7 @@ describe('Flyer DB Service', () => {
|
|||||||
it('should throw UniqueConstraintError on duplicate checksum', async () => {
|
it('should throw UniqueConstraintError on duplicate checksum', async () => {
|
||||||
const flyerData: FlyerDbInsert = { checksum: 'duplicate-checksum' } as FlyerDbInsert;
|
const flyerData: FlyerDbInsert = { checksum: 'duplicate-checksum' } as FlyerDbInsert;
|
||||||
const dbError = new Error('duplicate key value violates unique constraint "flyers_checksum_key"');
|
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);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
|
|
||||||
await expect(flyerRepo.insertFlyer(flyerData, mockLogger)).rejects.toThrow(UniqueConstraintError);
|
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 () => {
|
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 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"');
|
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);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
|
|
||||||
await expect(flyerRepo.insertFlyerItems(999, itemsData, mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
await expect(flyerRepo.insertFlyerItems(999, itemsData, mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
||||||
@@ -217,7 +218,7 @@ describe('Flyer DB Service', () => {
|
|||||||
.mockResolvedValueOnce({ rows: [mockFlyer] }) // insertFlyer
|
.mockResolvedValueOnce({ rows: [mockFlyer] }) // insertFlyer
|
||||||
.mockResolvedValueOnce({ rows: mockItems }); // insertFlyerItems
|
.mockResolvedValueOnce({ rows: mockItems }); // insertFlyerItems
|
||||||
return callback(mockClient as any);
|
return callback(mockClient as any);
|
||||||
});
|
}); // Cast to any is acceptable here as we are mocking the implementation
|
||||||
|
|
||||||
const result = await createFlyerAndItems(flyerData, itemsData, mockLogger);
|
const result = await createFlyerAndItems(flyerData, itemsData, mockLogger);
|
||||||
|
|
||||||
@@ -228,10 +229,10 @@ describe('Flyer DB Service', () => {
|
|||||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// Verify the individual functions were called with the client
|
// 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() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }).mockResolvedValueOnce({ rows: [mockFlyer] }).mockResolvedValueOnce({ rows: mockItems });
|
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('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 flyers'), expect.any(Array));
|
||||||
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO flyer_items'), 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
|
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }) // findOrCreateStore
|
||||||
.mockRejectedValueOnce(dbError); // insertFlyer fails
|
.mockRejectedValueOnce(dbError); // insertFlyer fails
|
||||||
// The withTransaction helper will catch this and roll back
|
// 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
|
// re-throw because withTransaction re-throws
|
||||||
throw dbError;
|
throw dbError;
|
||||||
});
|
});
|
||||||
@@ -446,15 +447,16 @@ describe('Flyer DB Service', () => {
|
|||||||
|
|
||||||
describe('deleteFlyer', () => {
|
describe('deleteFlyer', () => {
|
||||||
it('should use withTransaction to delete a flyer', async () => {
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn().mockResolvedValue({ rowCount: 1 }) };
|
return callback(mockClient as unknown as PoolClient);
|
||||||
return callback(mockClient as any);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await flyerRepo.deleteFlyer(42, mockLogger);
|
await flyerRepo.deleteFlyer(42, mockLogger);
|
||||||
|
|
||||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
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]);
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn().mockResolvedValue({ rowCount: 0 }) };
|
const mockClient = { query: vi.fn().mockResolvedValue({ rowCount: 0 }) };
|
||||||
// The callback will throw NotFoundError, and withTransaction will re-throw it.
|
// 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');
|
throw new NotFoundError('Simulated re-throw');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -474,7 +476,7 @@ describe('Flyer DB Service', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
|
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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/services/db/personalization.db.test.ts
|
// src/services/db/personalization.db.test.ts
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
|
import type { Pool, PoolClient } from 'pg';
|
||||||
import { withTransaction } from './connection.db';
|
import { withTransaction } from './connection.db';
|
||||||
import {
|
import {
|
||||||
PersonalizationRepository} from './personalization.db';
|
PersonalizationRepository} from './personalization.db';
|
||||||
@@ -37,10 +38,10 @@ describe('Personalization DB Service', () => {
|
|||||||
// Reset the withTransaction mock before each test
|
// Reset the withTransaction mock before each test
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
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
|
// 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', () => {
|
describe('getAllMasterItems', () => {
|
||||||
@@ -103,7 +104,7 @@ describe('Personalization DB Service', () => {
|
|||||||
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
|
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
|
||||||
.mockResolvedValueOnce({ rows: [mockItem] }) // Find master item
|
.mockResolvedValueOnce({ rows: [mockItem] }) // Find master item
|
||||||
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
|
.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);
|
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: [] }) // Find master item (not found)
|
||||||
.mockResolvedValueOnce({ rows: [mockNewItem] }) // INSERT new master item
|
.mockResolvedValueOnce({ rows: [mockNewItem] }) // INSERT new master item
|
||||||
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
|
.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);
|
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: [{ category_id: 1 }] }) // Find category
|
||||||
.mockResolvedValueOnce({ rows: [mockExistingItem] }) // Find master item
|
.mockResolvedValueOnce({ rows: [mockExistingItem] }) // Find master item
|
||||||
.mockResolvedValueOnce({ rows: [] }); // INSERT...ON CONFLICT
|
.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.
|
// 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 () => {
|
it('should throw an error if the category is not found', async () => {
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn().mockResolvedValue({ rows: [] }) };
|
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.");
|
throw new Error("Category 'Fake Category' not found.");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ describe('Personalization DB Service', () => {
|
|||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }).mockRejectedValueOnce(dbError);
|
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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ describe('Personalization DB Service', () => {
|
|||||||
|
|
||||||
it('should throw ForeignKeyConstraintError on invalid user or category', async () => {
|
it('should throw ForeignKeyConstraintError on invalid user or category', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
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);
|
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.');
|
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: [] });
|
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: mockClientQuery };
|
const mockClient = { query: mockClientQuery };
|
||||||
return callback(mockClient as any);
|
return callback(mockClient as unknown as PoolClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
await personalizationRepo.setUserDietaryRestrictions('user-123', [1, 2], mockLogger);
|
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 () => {
|
it('should throw ForeignKeyConstraintError if a restriction ID is invalid', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError); // DELETE ok, INSERT fail
|
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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -385,7 +386,7 @@ describe('Personalization DB Service', () => {
|
|||||||
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
|
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: mockClientQuery };
|
const mockClient = { query: mockClientQuery };
|
||||||
return callback(mockClient as any);
|
return callback(mockClient as unknown as PoolClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
await personalizationRepo.setUserDietaryRestrictions('user-123', [], mockLogger);
|
await personalizationRepo.setUserDietaryRestrictions('user-123', [], mockLogger);
|
||||||
@@ -455,7 +456,7 @@ describe('Personalization DB Service', () => {
|
|||||||
mockClientQuery
|
mockClientQuery
|
||||||
.mockResolvedValueOnce({ rows: [] }) // DELETE
|
.mockResolvedValueOnce({ rows: [] }) // DELETE
|
||||||
.mockResolvedValueOnce({ rows: mockNewAppliances }); // INSERT
|
.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);
|
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 () => {
|
it('should throw ForeignKeyConstraintError if an appliance ID is invalid', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError); // DELETE ok, INSERT fail
|
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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -483,7 +484,7 @@ describe('Personalization DB Service', () => {
|
|||||||
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
|
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: mockClientQuery };
|
const mockClient = { query: mockClientQuery };
|
||||||
return callback(mockClient as any);
|
return callback(mockClient as unknown as PoolClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await personalizationRepo.setUserAppliances('user-123', [], mockLogger);
|
const result = await personalizationRepo.setUserAppliances('user-123', [], mockLogger);
|
||||||
@@ -499,7 +500,7 @@ describe('Personalization DB Service', () => {
|
|||||||
const dbError = new Error('DB Error');
|
const dbError = new Error('DB Error');
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
|
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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/services/db/recipe.db.test.ts
|
// src/services/db/recipe.db.test.ts
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
|
import type { Pool } from 'pg';
|
||||||
import { RecipeRepository } from './recipe.db';
|
import { RecipeRepository } from './recipe.db';
|
||||||
|
|
||||||
// Un-mock the module we are testing to ensure we use the real implementation.
|
// Un-mock the module we are testing to ensure we use the real implementation.
|
||||||
@@ -27,7 +28,7 @@ describe('Recipe DB Service', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Instantiate the repository with the mock pool for each test
|
// 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', () => {
|
describe('getRecipesBySalePercentage', () => {
|
||||||
@@ -130,7 +131,7 @@ describe('Recipe DB Service', () => {
|
|||||||
|
|
||||||
it('should throw ForeignKeyConstraintError if user or recipe does not exist', async () => {
|
it('should throw ForeignKeyConstraintError if user or recipe does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockQuery.mockRejectedValue(dbError);
|
mockQuery.mockRejectedValue(dbError);
|
||||||
await expect(recipeRepo.addFavoriteRecipe('user-123', 999, mockLogger)).rejects.toThrow('The specified user or recipe does not exist.');
|
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');
|
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 () => {
|
it('should throw ForeignKeyConstraintError if recipe, user, or parent comment does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockQuery.mockRejectedValue(dbError);
|
mockQuery.mockRejectedValue(dbError);
|
||||||
await expect(recipeRepo.addRecipeComment(999, 'user-123', 'Fail', mockLogger)).rejects.toThrow('The specified recipe, user, or parent comment does not exist.');
|
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');
|
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 () => {
|
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.');
|
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);
|
mockQuery.mockRejectedValue(dbError);
|
||||||
|
|
||||||
await expect(recipeRepo.forkRecipe('user-123', 1, mockLogger)).rejects.toThrow('Recipe is not public and cannot be forked.');
|
await expect(recipeRepo.forkRecipe('user-123', 1, mockLogger)).rejects.toThrow('Recipe is not public and cannot be forked.');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/services/db/shopping.db.test.ts
|
// src/services/db/shopping.db.test.ts
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||||
|
import type { Pool, PoolClient } from 'pg';
|
||||||
import { withTransaction } from './connection.db';
|
import { withTransaction } from './connection.db';
|
||||||
import { createMockShoppingList, createMockShoppingListItem } from '../../tests/utils/mockFactories';
|
import { createMockShoppingList, createMockShoppingListItem } from '../../tests/utils/mockFactories';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ import { createMockShoppingList, createMockShoppingListItem } from '../../tests/
|
|||||||
vi.unmock('./shopping.db');
|
vi.unmock('./shopping.db');
|
||||||
|
|
||||||
import { ShoppingRepository } from './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
|
// Mock the logger to prevent console output during tests
|
||||||
vi.mock('../logger.server', () => ({
|
vi.mock('../logger.server', () => ({
|
||||||
@@ -33,7 +34,7 @@ describe('Shopping DB Service', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Instantiate the repository with the mock pool for each test
|
// 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', () => {
|
describe('getShoppingLists', () => {
|
||||||
@@ -104,7 +105,7 @@ describe('Shopping DB Service', () => {
|
|||||||
|
|
||||||
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
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"');
|
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);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(shoppingRepo.createShoppingList('non-existent-user', 'Wont work', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
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 () => {
|
it('should throw ForeignKeyConstraintError if list or master item does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(shoppingRepo.addShoppingListItem(999, { masterItemId: 999 }, mockLogger)).rejects.toThrow('Referenced list or item does not exist.');
|
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 () => {
|
it('should throw ForeignKeyConstraintError if the shopping list does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(shoppingRepo.completeShoppingList(999, 'user-123', mockLogger)).rejects.toThrow('The specified shopping list does not exist.');
|
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 () => {
|
it('should throw UniqueConstraintError on duplicate name', async () => {
|
||||||
const dbError = new Error('duplicate key value violates unique constraint');
|
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);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(shoppingRepo.createPantryLocation('user-1', 'Fridge', mockLogger)).rejects.toThrow(UniqueConstraintError);
|
await expect(shoppingRepo.createPantryLocation('user-1', 'Fridge', mockLogger)).rejects.toThrow(UniqueConstraintError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(shoppingRepo.createPantryLocation('non-existent-user', 'Pantry', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
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 () => {
|
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(shoppingRepo.createReceipt('non-existent-user', 'url', mockLogger)).rejects.toThrow('User not found');
|
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: [] });
|
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: mockClientQuery };
|
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 }];
|
const items = [{ raw_item_description: 'Milk', price_paid_cents: 399 }];
|
||||||
@@ -470,7 +471,7 @@ describe('Shopping DB Service', () => {
|
|||||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
|
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
|
||||||
// The callback will throw, and withTransaction will catch and re-throw
|
// 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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class ShoppingRepository {
|
|||||||
return { ...res.rows[0], items: [] };
|
return { ...res.rows[0], items: [] };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The patch requested this specific error handling.
|
// 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.');
|
throw new ForeignKeyConstraintError('The specified user does not exist.');
|
||||||
}
|
}
|
||||||
logger.error({ err: error, userId, name }, 'Database error in createShoppingList');
|
logger.error({ err: error, userId, name }, 'Database error in createShoppingList');
|
||||||
@@ -161,7 +161,7 @@ export class ShoppingRepository {
|
|||||||
return res.rows[0];
|
return res.rows[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The patch requested this specific error handling.
|
// 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.');
|
throw new ForeignKeyConstraintError('Referenced list or item does not exist.');
|
||||||
}
|
}
|
||||||
logger.error({ err: error, listId, item }, 'Database error in addShoppingListItem');
|
logger.error({ err: error, listId, item }, 'Database error in addShoppingListItem');
|
||||||
@@ -248,9 +248,9 @@ export class ShoppingRepository {
|
|||||||
);
|
);
|
||||||
return res.rows[0];
|
return res.rows[0];
|
||||||
} catch (error) {
|
} 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.');
|
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');
|
throw new ForeignKeyConstraintError('User not found');
|
||||||
}
|
}
|
||||||
logger.error({ err: error, userId, name }, 'Database error in createPantryLocation');
|
logger.error({ err: error, userId, name }, 'Database error in createPantryLocation');
|
||||||
@@ -325,7 +325,7 @@ export class ShoppingRepository {
|
|||||||
return res.rows[0].complete_shopping_list;
|
return res.rows[0].complete_shopping_list;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The patch requested this specific error handling.
|
// 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.');
|
throw new ForeignKeyConstraintError('The specified shopping list does not exist.');
|
||||||
}
|
}
|
||||||
logger.error({ err: error, shoppingListId, userId }, 'Database error in completeShoppingList');
|
logger.error({ err: error, shoppingListId, userId }, 'Database error in completeShoppingList');
|
||||||
@@ -388,7 +388,7 @@ export class ShoppingRepository {
|
|||||||
return res.rows[0];
|
return res.rows[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The patch requested this specific error handling.
|
// 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');
|
throw new ForeignKeyConstraintError('User not found');
|
||||||
}
|
}
|
||||||
logger.error({ err: error, userId, receiptImageUrl }, 'Database error in createReceipt');
|
logger.error({ err: error, userId, receiptImageUrl }, 'Database error in createReceipt');
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
// src/services/db/user.db.test.ts
|
// 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 { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { PoolClient } from 'pg';
|
import { PoolClient } from 'pg';
|
||||||
|
|
||||||
@@ -60,9 +53,9 @@ describe('User DB Service', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
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.
|
// 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', () => {
|
describe('findUserByEmail', () => {
|
||||||
@@ -113,7 +106,7 @@ describe('User DB Service', () => {
|
|||||||
.mockResolvedValueOnce({ rows: [] }) // set_config
|
.mockResolvedValueOnce({ rows: [] }) // set_config
|
||||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||||
.mockResolvedValueOnce({ rows: [mockDbProfile] }); // SELECT profile
|
.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);
|
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) => {
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||||
const mockClient = { query: vi.fn() };
|
const mockClient = { query: vi.fn() };
|
||||||
mockClient.query.mockRejectedValueOnce(dbError); // set_config or INSERT fails
|
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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,7 +137,7 @@ describe('User DB Service', () => {
|
|||||||
.mockResolvedValueOnce({ rows: [] }) // set_config
|
.mockResolvedValueOnce({ rows: [] }) // set_config
|
||||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||||
.mockRejectedValueOnce(dbError); // SELECT profile fails
|
.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;
|
throw dbError;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,7 +147,7 @@ describe('User DB Service', () => {
|
|||||||
|
|
||||||
it('should throw UniqueConstraintError if the email already exists', async () => {
|
it('should throw UniqueConstraintError if the email already exists', async () => {
|
||||||
const dbError = new Error('duplicate key value violates unique constraint');
|
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);
|
vi.mocked(withTransaction).mockRejectedValue(dbError);
|
||||||
|
|
||||||
@@ -417,7 +410,7 @@ describe('User DB Service', () => {
|
|||||||
|
|
||||||
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(userRepo.createPasswordResetToken('non-existent-user', 'hash', new Date(), mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
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 { ShoppingRepository } = await import('./shopping.db');
|
||||||
const { PersonalizationRepository } = await import('./personalization.db');
|
const { PersonalizationRepository } = await import('./personalization.db');
|
||||||
|
|
||||||
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById')
|
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById');
|
||||||
.mockResolvedValue({ user_id: '123' } as any);
|
findProfileSpy.mockResolvedValue({ user_id: '123' } as Profile);
|
||||||
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems')
|
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems');
|
||||||
.mockResolvedValue([]);
|
getWatchedItemsSpy.mockResolvedValue([]);
|
||||||
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists')
|
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists');
|
||||||
.mockResolvedValue([]);
|
getShoppingListsSpy.mockResolvedValue([]);
|
||||||
|
|
||||||
await exportUserData('123', mockLogger);
|
await exportUserData('123', mockLogger);
|
||||||
|
|
||||||
@@ -522,7 +515,7 @@ describe('User DB Service', () => {
|
|||||||
|
|
||||||
it('should throw ForeignKeyConstraintError if a user does not exist', async () => {
|
it('should throw ForeignKeyConstraintError if a user does not exist', async () => {
|
||||||
const dbError = new Error('violates foreign key constraint');
|
const dbError = new Error('violates foreign key constraint');
|
||||||
(dbError as any).code = '23503';
|
(dbError as Error & { code: string }).code = '23503';
|
||||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||||
await expect(userRepo.followUser('follower-1', 'non-existent-user', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
await expect(userRepo.followUser('follower-1', 'non-existent-user', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -452,9 +452,6 @@ export class UserRepository {
|
|||||||
[followerId, followingId]
|
[followerId, followingId]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as any).code === '23503') {
|
|
||||||
throw new ForeignKeyConstraintError('User not found.');
|
|
||||||
}
|
|
||||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||||
throw new ForeignKeyConstraintError('One or both users do not exist.');
|
throw new ForeignKeyConstraintError('One or both users do not exist.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ describe('parsePriceToCents', () => {
|
|||||||
it('should return null for empty or invalid inputs', () => {
|
it('should return null for empty or invalid inputs', () => {
|
||||||
expect(parsePriceToCents('')).toBeNull();
|
expect(parsePriceToCents('')).toBeNull();
|
||||||
expect(parsePriceToCents(' ')).toBeNull();
|
expect(parsePriceToCents(' ')).toBeNull();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
expect(parsePriceToCents(null as any)).toBeNull();
|
expect(parsePriceToCents(null as any)).toBeNull();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
expect(parsePriceToCents(undefined as any)).toBeNull();
|
expect(parsePriceToCents(undefined as any)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user