Files
flyer-crawler.projectium.com/src/services/db/flyer.db.test.ts
Torben Sorensen 66a2585efc
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 6m41s
moar unit test !
2025-12-08 08:53:17 -08:00

257 lines
11 KiB
TypeScript

// 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, createMockFlyerItem, createMockBrand } from '../../tests/utils/mockFactories';
// Un-mock the module we are testing to ensure we use the real implementation
vi.unmock('./flyer.db');
import {
insertFlyer,
insertFlyerItems,
createFlyerAndItems,
getAllBrands,
getFlyerById,
getFlyers,
getFlyerItems,
getFlyerItemsForFlyers,
countFlyerItemsForFlyers,
findFlyerByChecksum,
} from './flyer.db';
import type { FlyerInsert, FlyerItemInsert, Brand, Flyer, FlyerItem } from '../../types';
// Mock dependencies
vi.mock('../logger.server', () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));
describe('Flyer DB Service', () => {
beforeEach(() => {
// In a transaction, `pool.connect()` returns a client. That client has a `release` method.
// For these tests, we simulate this by having `connect` resolve to the pool instance itself,
// and we ensure the `release` method is mocked on that instance.
const mockClient = { ...mockPoolInstance, release: vi.fn() };
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
vi.clearAllMocks();
});
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('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 });
const result = await insertFlyerItems(1, itemsData, mockPoolInstance as any);
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 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',
price_in_cents: 199,
quantity: 'each',
view_count: 0,
click_count: 0,
} 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);
// Use `objectContaining` to make the test more resilient to changes
// in the returned object structure (e.g., new columns added to the DB).
// This ensures the core data is correct without being overly brittle.
expect(result).toEqual({
flyer: expect.objectContaining(mockFlyer),
items: expect.arrayContaining([
expect.objectContaining(mockItems[0])
])
});
// Verify transaction control
expect(mockPoolInstance.connect).toHaveBeenCalled();
expect(mockPoolInstance.query).toHaveBeenCalledWith('BEGIN');
expect(mockPoolInstance.query).toHaveBeenCalledWith('COMMIT');
expect(mockPoolInstance.query).not.toHaveBeenCalledWith('ROLLBACK');
// 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(vi.mocked(mockPoolInstance.connect).mock.results[0].value.release).toHaveBeenCalled();
});
});
describe('getAllBrands', () => {
it('should execute the correct SELECT query and return brands', async () => {
const mockBrands: Brand[] = [createMockBrand({ brand_id: 1, name: 'Test Brand' })];
mockPoolInstance.query.mockResolvedValue({ rows: mockBrands });
const result = await getAllBrands();
expect(result).toEqual(mockBrands);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.stores s'));
});
});
describe('getFlyerById', () => {
it('should return a flyer if found', async () => {
const mockFlyer = createMockFlyer({ flyer_id: 123 });
mockPoolInstance.query.mockResolvedValue({ rows: [mockFlyer] });
const result = await getFlyerById(123);
expect(result).toEqual(mockFlyer);
expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT * FROM public.flyers WHERE flyer_id = $1', [123]);
});
});
describe('getFlyers', () => {
it('should use default limit and offset when none are provided', async () => {
const mockFlyers: Flyer[] = [createMockFlyer({ flyer_id: 1 })];
mockPoolInstance.query.mockResolvedValue({ rows: mockFlyers });
await getFlyers();
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'SELECT * FROM public.flyers ORDER BY created_at DESC LIMIT $1 OFFSET $2',
[20, 0] // Default values
);
});
it('should use provided limit and offset values', async () => {
const mockFlyers: Flyer[] = [createMockFlyer({ flyer_id: 1 })];
mockPoolInstance.query.mockResolvedValue({ rows: mockFlyers });
await getFlyers(10, 5);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'SELECT * FROM public.flyers ORDER BY created_at DESC LIMIT $1 OFFSET $2',
[10, 5] // Provided values
);
});
});
describe('getFlyerItems', () => {
it('should return items for a specific flyer', async () => {
const mockItems: FlyerItem[] = [createMockFlyerItem({ flyer_id: 456 })];
mockPoolInstance.query.mockResolvedValue({ rows: mockItems });
const result = await getFlyerItems(456);
expect(result).toEqual(mockItems);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE flyer_id = $1'), [456]);
});
});
describe('getFlyerItemsForFlyers', () => {
it('should return items for multiple flyers using ANY', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await getFlyerItemsForFlyers([1, 2, 3]);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('flyer_id = ANY($1::int[])'), [[1, 2, 3]]);
});
});
describe('countFlyerItemsForFlyers', () => {
it('should return the total count of items', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{ count: '42' }] });
const result = await countFlyerItemsForFlyers([1, 2]);
expect(result).toBe(42);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('SELECT COUNT(*)'), [[1, 2]]);
});
});
describe('findFlyerByChecksum', () => {
it('should return a flyer for a given checksum', async () => {
const mockFlyer = createMockFlyer({ checksum: 'abc' });
mockPoolInstance.query.mockResolvedValue({ rows: [mockFlyer] });
const result = await findFlyerByChecksum('abc');
expect(result).toEqual(mockFlyer);
expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT * FROM public.flyers WHERE checksum = $1', ['abc']);
});
});
});