Files
flyer-crawler.projectium.com/src/services/flyerDataTransformer.test.ts
Torben Sorensen 2e72ee81dd
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 41s
maybe a few too many fixes
2025-12-28 21:38:31 -08:00

244 lines
6.9 KiB
TypeScript

// src/services/flyerDataTransformer.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FlyerDataTransformer } from './flyerDataTransformer';
import { logger as mockLogger } from './logger.server';
import { generateFlyerIcon } from '../utils/imageProcessor';
import type { AiProcessorResult } from './flyerAiProcessor.server';
import type { FlyerItemInsert } from '../types';
// Mock the dependencies
vi.mock('../utils/imageProcessor', () => ({
generateFlyerIcon: vi.fn(),
}));
vi.mock('./logger.server', () => ({
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
}));
describe('FlyerDataTransformer', () => {
let transformer: FlyerDataTransformer;
beforeEach(() => {
vi.clearAllMocks();
transformer = new FlyerDataTransformer();
// Provide a default mock implementation for generateFlyerIcon
vi.mocked(generateFlyerIcon).mockResolvedValue('icon-flyer-page-1.webp');
});
it('should transform AI data into database-ready format with a user ID', async () => {
// Arrange
const aiResult: AiProcessorResult = {
data: {
store_name: 'Test Store',
valid_from: '2024-01-01',
valid_to: '2024-01-07',
store_address: '123 Test St',
items: [
{
item: 'Milk',
price_display: '$3.99',
price_in_cents: 399,
quantity: '1L',
category_name: 'Dairy',
master_item_id: 10,
},
{
item: 'Bread',
price_display: '$2.49',
price_in_cents: 249,
quantity: '1 loaf',
category_name: 'Bakery',
master_item_id: null,
},
],
},
needsReview: false,
};
const imagePaths = [{ path: '/uploads/flyer-page-1.jpg', mimetype: 'image/jpeg' }];
const originalFileName = 'my-flyer.pdf';
const checksum = 'checksum-abc-123';
const userId = 'user-xyz-456';
// Act
const { flyerData, itemsForDb } = await transformer.transform(
aiResult,
imagePaths,
originalFileName,
checksum,
userId,
mockLogger,
);
// Assert
// 0. Check logging
expect(mockLogger.info).toHaveBeenCalledWith(
'Starting data transformation from AI output to database format.',
);
expect(mockLogger.info).toHaveBeenCalledWith(
{ itemCount: 2, storeName: 'Test Store' },
'Data transformation complete.',
);
// 1. Check flyer data
expect(flyerData).toEqual({
file_name: originalFileName,
image_url: '/flyer-images/flyer-page-1.jpg',
icon_url: '/flyer-images/icons/icon-flyer-page-1.webp',
checksum,
store_name: 'Test Store',
valid_from: '2024-01-01',
valid_to: '2024-01-07',
store_address: '123 Test St',
item_count: 2,
status: 'processed',
uploaded_by: userId,
});
// 2. Check item data
expect(itemsForDb).toHaveLength(2);
expect(itemsForDb[0]).toEqual(
expect.objectContaining({
item: 'Milk',
master_item_id: 10, // Should be passed through
view_count: 0,
click_count: 0,
}),
);
expect(itemsForDb[1]).toEqual(
expect.objectContaining({
item: 'Bread',
master_item_id: undefined, // null should be converted to undefined
view_count: 0,
click_count: 0,
}),
);
// 3. Check that generateFlyerIcon was called correctly
expect(generateFlyerIcon).toHaveBeenCalledWith(
'/uploads/flyer-page-1.jpg',
'/uploads/icons',
mockLogger,
);
});
it('should handle missing optional data gracefully', async () => {
// Arrange
const aiResult: AiProcessorResult = {
data: {
store_name: '', // Empty store name
valid_from: null,
valid_to: null,
store_address: null,
items: [], // No items
},
needsReview: true,
};
const imagePaths = [{ path: '/uploads/another.png', mimetype: 'image/png' }];
const originalFileName = 'another.png';
const checksum = 'checksum-def-456';
// No userId provided
vi.mocked(generateFlyerIcon).mockResolvedValue('icon-another.webp');
// Act
const { flyerData, itemsForDb } = await transformer.transform(
aiResult,
imagePaths,
originalFileName,
checksum,
undefined,
mockLogger,
);
// Assert
// 0. Check logging
expect(mockLogger.info).toHaveBeenCalledWith(
'Starting data transformation from AI output to database format.',
);
expect(mockLogger.warn).toHaveBeenCalledWith(
'AI did not return a store name. Using fallback "Unknown Store (auto)".',
);
expect(mockLogger.info).toHaveBeenCalledWith(
{ itemCount: 0, storeName: 'Unknown Store (auto)' },
'Data transformation complete.',
);
expect(itemsForDb).toHaveLength(0);
expect(flyerData).toEqual({
file_name: originalFileName,
image_url: '/flyer-images/another.png',
icon_url: '/flyer-images/icons/icon-another.webp',
checksum,
store_name: 'Unknown Store (auto)', // Should use fallback
valid_from: null,
valid_to: null,
store_address: null,
item_count: 0,
status: 'needs_review',
uploaded_by: undefined, // Should be undefined
});
});
it('should correctly normalize item fields with null, undefined, or empty values', async () => {
// Arrange
const aiResult: AiProcessorResult = {
data: {
store_name: 'Test Store',
valid_from: '2024-01-01',
valid_to: '2024-01-07',
store_address: '123 Test St',
items: [
// Case 1: All fields are null or undefined
{
item: null,
price_display: null,
price_in_cents: null,
quantity: null,
category_name: null,
master_item_id: null,
},
// Case 2: Fields are empty strings
{
item: ' ', // whitespace only
price_display: '',
price_in_cents: 200,
quantity: '',
category_name: '',
master_item_id: 20,
},
],
},
needsReview: false,
};
const imagePaths = [{ path: '/uploads/flyer-page-1.jpg', mimetype: 'image/jpeg' }];
// Act
const { itemsForDb } = await transformer.transform(
aiResult,
imagePaths,
'file.pdf',
'checksum',
'user-1',
mockLogger,
);
// Assert
expect(itemsForDb).toHaveLength(2);
// Check Case 1 (null/undefined values)
expect(itemsForDb[0]).toEqual(
expect.objectContaining({
item: 'Unknown Item', price_display: '', quantity: '', category_name: 'Other/Miscellaneous', master_item_id: undefined,
}),
);
// Check Case 2 (empty string values)
expect(itemsForDb[1]).toEqual(
expect.objectContaining({
item: 'Unknown Item', price_display: '', quantity: '', category_name: 'Other/Miscellaneous', master_item_id: 20,
}),
);
});
});