moar unit test !
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 6m4s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 6m4s
This commit is contained in:
@@ -1,123 +1,143 @@
|
||||
// src/services/db/flyer.db.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||
import { createMockFlyer } from '../../tests/utils/mockFactories';
|
||||
import { createMockFlyer, createMockFlyerItem } from '../../tests/utils/mockFactories';
|
||||
|
||||
// Un-mock the module we are testing
|
||||
// Un-mock the module we are testing to ensure we use the real implementation
|
||||
vi.unmock('./flyer.db');
|
||||
|
||||
import {
|
||||
getFlyers,
|
||||
findFlyerByChecksum,
|
||||
createFlyerAndItems,
|
||||
trackFlyerItemInteraction,
|
||||
getHistoricalPriceDataForItems,
|
||||
} from './flyer.db';
|
||||
import { insertFlyer, insertFlyerItems, createFlyerAndItems } from './flyer.db';
|
||||
import type { FlyerInsert, FlyerItemInsert } from '../../types';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../logger.server', () => ({
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
}));
|
||||
vi.mock('../geocodingService.server', () => ({
|
||||
geocodeAddress: vi.fn().mockResolvedValue({ lat: 48, lng: -123 }),
|
||||
}));
|
||||
|
||||
describe('Flyer DB Service', () => {
|
||||
beforeEach(() => {
|
||||
// To correctly mock a transaction, the `connect` method should return an object
|
||||
// that has `query` and `release` methods. Here, we make `connect` return the
|
||||
// pool instance itself, and ensure the `release` method is present on it.
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockPoolInstance as any);
|
||||
(mockPoolInstance as any).release = vi.fn(); // Add the missing release method
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getFlyers', () => {
|
||||
it('should throw an error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Connection Error'));
|
||||
await expect(getFlyers()).rejects.toThrow('Failed to retrieve flyers from database.');
|
||||
describe('insertFlyer', () => {
|
||||
it('should execute an INSERT query and return the new flyer', async () => {
|
||||
const flyerData: FlyerInsert = {
|
||||
file_name: 'test.jpg',
|
||||
image_url: '/images/test.jpg',
|
||||
icon_url: '/images/icons/test.jpg',
|
||||
checksum: 'checksum123',
|
||||
store_name: 'Test Store',
|
||||
valid_from: '2024-01-01',
|
||||
valid_to: '2024-01-07',
|
||||
store_address: '123 Test St',
|
||||
item_count: 10,
|
||||
uploaded_by: 'user-1',
|
||||
};
|
||||
const mockFlyer = createMockFlyer({ ...flyerData, flyer_id: 1 });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockFlyer] });
|
||||
|
||||
const result = await insertFlyer(flyerData, mockPoolInstance as any);
|
||||
|
||||
expect(result).toEqual(mockFlyer);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledTimes(1);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO flyers'),
|
||||
[
|
||||
'test.jpg',
|
||||
'/images/test.jpg',
|
||||
'/images/icons/test.jpg',
|
||||
'checksum123',
|
||||
'Test Store',
|
||||
'2024-01-01',
|
||||
'2024-01-07',
|
||||
'123 Test St',
|
||||
10,
|
||||
'user-1',
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findFlyerByChecksum', () => {
|
||||
it('should return undefined if no flyer is found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await findFlyerByChecksum('non-existent-checksum');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('insertFlyerItems', () => {
|
||||
it('should build a bulk INSERT query and return the new items', async () => {
|
||||
const itemsData: FlyerItemInsert[] = [
|
||||
{ item: 'Milk', price_display: '$3.99', price_in_cents: 399, quantity: '1L', category_name: 'Dairy', view_count: 0, click_count: 0 },
|
||||
{ item: 'Bread', price_display: '$2.49', price_in_cents: 249, quantity: '1 loaf', category_name: 'Bakery', view_count: 0, click_count: 0 },
|
||||
];
|
||||
const mockItems = itemsData.map((item, i) => createMockFlyerItem({ ...item, flyer_item_id: i + 1, flyer_id: 1 }));
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockItems });
|
||||
|
||||
describe('createFlyerAndItems', () => {
|
||||
const mockFlyerData = {
|
||||
file_name: 'test.jpg',
|
||||
image_url: 'test.jpg',
|
||||
checksum: 'test-checksum',
|
||||
store_name: 'Test Store',
|
||||
valid_from: '2024-01-01',
|
||||
valid_to: '2024-01-07',
|
||||
store_address: '123 Test St',
|
||||
uploaded_by: 'user-1',
|
||||
item_count: 0, // This was missing and is required by the type
|
||||
};
|
||||
const result = await insertFlyerItems(1, itemsData, mockPoolInstance as any);
|
||||
|
||||
it('should rollback the transaction if an item insert fails', async () => {
|
||||
const mockFlyer = createMockFlyer();
|
||||
const mockItems = [{ item: 'Good Item' }, { item: 'Bad Item' }];
|
||||
|
||||
// Setup a failing query on the second item insert
|
||||
mockPoolInstance.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }) // Find store
|
||||
.mockResolvedValueOnce({ rows: [{ address_id: 1 }] }) // Insert/Select address
|
||||
.mockResolvedValueOnce({ rows: [{ store_location_id: 1 }] }) // Upsert store_location
|
||||
.mockResolvedValueOnce({ rows: [mockFlyer] }) // INSERT flyer
|
||||
.mockResolvedValueOnce({ rows: [] }) // Link flyer_location
|
||||
.mockResolvedValueOnce({ rows: [] }) // INSERT first item (success)
|
||||
.mockRejectedValueOnce(new Error('Item insert failed')) // INSERT second item (fail)
|
||||
.mockResolvedValueOnce({ rows: [] }); // ROLLBACK
|
||||
|
||||
await expect(createFlyerAndItems(mockFlyerData, mockItems as any)).rejects.toThrow('Failed to save flyer and its items.');
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockPoolInstance.query).not.toHaveBeenCalledWith('COMMIT');
|
||||
expect(result).toEqual(mockItems);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledTimes(1);
|
||||
// Check that the query string has two value placeholders
|
||||
expect(mockPoolInstance.query.mock.calls[0][0]).toContain('VALUES ($1, $2, $3, $4, $5, $6, $7, $8), ($9, $10, $11, $12, $13, $14, $15, $16)');
|
||||
// Check that the values array is correctly flattened
|
||||
expect(mockPoolInstance.query.mock.calls[0][1]).toEqual([
|
||||
1, 'Milk', '$3.99', 399, '1L', 'Dairy', 0, 0,
|
||||
1, 'Bread', '$2.49', 249, '1 loaf', 'Bakery', 0, 0,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle an empty items array gracefully', async () => {
|
||||
const mockFlyer = createMockFlyer();
|
||||
mockPoolInstance.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] })
|
||||
.mockResolvedValueOnce({ rows: [{ address_id: 1 }] })
|
||||
.mockResolvedValueOnce({ rows: [{ store_location_id: 1 }] })
|
||||
.mockResolvedValueOnce({ rows: [mockFlyer] })
|
||||
.mockResolvedValueOnce({ rows: [] }) // Link flyer_location
|
||||
.mockResolvedValueOnce({ rows: [] }); // COMMIT
|
||||
|
||||
// Mock geocodeAddress to handle the null address case gracefully
|
||||
const { geocodeAddress } = await import('../geocodingService.server');
|
||||
vi.mocked(geocodeAddress).mockResolvedValue(null);
|
||||
|
||||
await createFlyerAndItems(mockFlyerData, []);
|
||||
|
||||
// Also test the case where store_address is null
|
||||
const mockFlyerDataNoAddress = { ...mockFlyerData, store_address: null };
|
||||
await createFlyerAndItems(mockFlyerDataNoAddress, []);
|
||||
|
||||
// Verify it doesn't try to insert items but still commits the flyer
|
||||
expect(mockPoolInstance.query).not.toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.flyer_items'));
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackFlyerItemInteraction', () => {
|
||||
it('should not throw an error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Connection Error'));
|
||||
// The function is designed to swallow errors, so we expect it to resolve to undefined
|
||||
await expect(trackFlyerItemInteraction(1, 'click')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistoricalPriceDataForItems', () => {
|
||||
it('should return an empty array if no masterItemIds are provided', async () => {
|
||||
const result = await getHistoricalPriceDataForItems([]);
|
||||
it('should return an empty array and not query the DB if items array is empty', async () => {
|
||||
const result = await insertFlyerItems(1, [], mockPoolInstance as any);
|
||||
expect(result).toEqual([]);
|
||||
expect(mockPoolInstance.query).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFlyerAndItems', () => {
|
||||
it('should execute a transaction with BEGIN, INSERTs, and COMMIT', async () => {
|
||||
const flyerData: FlyerInsert = { file_name: 'transact.jpg', store_name: 'Transaction Store' } as FlyerInsert;
|
||||
const itemsData: FlyerItemInsert[] = [{ item: 'Transactional Item' } as FlyerItemInsert];
|
||||
const mockFlyer = createMockFlyer({ ...flyerData, flyer_id: 99 });
|
||||
const mockItems = [createMockFlyerItem({ ...itemsData[0], flyer_id: 99, flyer_item_id: 101 })];
|
||||
|
||||
// Mock the sequence of calls within the transaction
|
||||
mockPoolInstance.query
|
||||
.mockResolvedValueOnce({ rows: [mockFlyer] }) // insertFlyer
|
||||
.mockResolvedValueOnce({ rows: mockItems }); // insertFlyerItems
|
||||
|
||||
const result = await createFlyerAndItems(flyerData, itemsData);
|
||||
|
||||
expect(result).toEqual({ flyer: mockFlyer, items: mockItems });
|
||||
|
||||
// Verify transaction control
|
||||
expect(mockPoolInstance.connect).toHaveBeenCalled();
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('COMMIT');
|
||||
expect(mockPoolInstance.query).not.toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockPoolInstance.release).toHaveBeenCalled();
|
||||
|
||||
// Verify the individual functions were called with the client
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO flyers'), expect.any(Array));
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO flyer_items'), expect.any(Array));
|
||||
});
|
||||
|
||||
it('should ROLLBACK the transaction if an error occurs', async () => {
|
||||
const flyerData: FlyerInsert = { file_name: 'fail.jpg', store_name: 'Fail Store' } as FlyerInsert;
|
||||
const itemsData: FlyerItemInsert[] = [{ item: 'Failing Item' } as FlyerItemInsert];
|
||||
const dbError = new Error('DB connection lost');
|
||||
|
||||
// Mock insertFlyer to succeed, but insertFlyerItems to fail
|
||||
mockPoolInstance.query
|
||||
.mockResolvedValueOnce({ rows: [createMockFlyer()] }) // insertFlyer
|
||||
.mockRejectedValueOnce(dbError); // insertFlyerItems fails
|
||||
|
||||
await expect(createFlyerAndItems(flyerData, itemsData)).rejects.toThrow(dbError);
|
||||
|
||||
// Verify transaction control
|
||||
expect(mockPoolInstance.connect).toHaveBeenCalled();
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockPoolInstance.query).not.toHaveBeenCalledWith('COMMIT');
|
||||
expect(mockPoolInstance.release).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
// src/services/db/index.db.ts
|
||||
export * from './connection.db';
|
||||
export * from './errors.db';
|
||||
export * from './user.db';
|
||||
export * from './flyer.db';
|
||||
export * from './shopping.db';
|
||||
export * from './personalization.db';
|
||||
@@ -9,5 +8,6 @@ export * from './recipe.db';
|
||||
export * from './admin.db';
|
||||
export * from './notification.db';
|
||||
export * from './gamification.db';
|
||||
export * from './budget.db';
|
||||
export * from './address.db'; // Add the new address service exports
|
||||
export * from './budget.db';
|
||||
export * from './address.db';
|
||||
export * from './user.db';
|
||||
@@ -14,6 +14,21 @@ import {
|
||||
Recipe,
|
||||
} from '../../types';
|
||||
|
||||
/**
|
||||
* Retrieves all master grocery items from the database.
|
||||
* This is used to provide a list of known items to the AI for better matching.
|
||||
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
||||
*/
|
||||
export async function getAllMasterItems(): Promise<MasterGroceryItem[]> {
|
||||
try {
|
||||
const res = await getPool().query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items ORDER BY name ASC');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAllMasterItems:', { error });
|
||||
throw new Error('Failed to retrieve master grocery items.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all watched master items for a specific user.
|
||||
* @param userId The UUID of the user.
|
||||
|
||||
Reference in New Issue
Block a user