Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7d03951b9 | ||
| af8816e0af |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.21",
|
||||
"version": "0.9.22",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.21",
|
||||
"version": "0.9.22",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.9.21",
|
||||
"version": "0.9.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -25,6 +25,93 @@ import { useAppInitialization } from './hooks/useAppInitialization';
|
||||
|
||||
// Mock top-level components rendered by App's routes
|
||||
|
||||
vi.mock('./components/Header', () => ({
|
||||
Header: ({ onOpenProfile, onOpenVoiceAssistant }: any) => (
|
||||
<div data-testid="header-mock">
|
||||
<button onClick={onOpenProfile}>Open Profile</button>
|
||||
<button onClick={onOpenVoiceAssistant}>Open Voice Assistant</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./components/Footer', () => ({
|
||||
Footer: () => <div data-testid="footer-mock">Mock Footer</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./layouts/MainLayout', async () => {
|
||||
const { Outlet } = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||
return {
|
||||
MainLayout: () => (
|
||||
<div data-testid="main-layout-mock">
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./pages/HomePage', () => ({
|
||||
HomePage: ({ selectedFlyer, onOpenCorrectionTool }: any) => (
|
||||
<div data-testid="home-page-mock" data-selected-flyer-id={selectedFlyer?.flyer_id}>
|
||||
<button onClick={onOpenCorrectionTool}>Open Correction Tool</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./pages/admin/AdminPage', () => ({
|
||||
AdminPage: () => <div data-testid="admin-page-mock">AdminPage</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./pages/admin/CorrectionsPage', () => ({
|
||||
CorrectionsPage: () => <div data-testid="corrections-page-mock">CorrectionsPage</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./pages/admin/AdminStatsPage', () => ({
|
||||
AdminStatsPage: () => <div data-testid="admin-stats-page-mock">AdminStatsPage</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./pages/admin/FlyerReviewPage', () => ({
|
||||
FlyerReviewPage: () => <div data-testid="flyer-review-page-mock">FlyerReviewPage</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./pages/VoiceLabPage', () => ({
|
||||
VoiceLabPage: () => <div data-testid="voice-lab-page-mock">VoiceLabPage</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./pages/ResetPasswordPage', () => ({
|
||||
ResetPasswordPage: () => <div data-testid="reset-password-page-mock">ResetPasswordPage</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./pages/admin/components/ProfileManager', () => ({
|
||||
ProfileManager: ({ isOpen, onClose, onProfileUpdate, onLoginSuccess }: any) =>
|
||||
isOpen ? (
|
||||
<div data-testid="profile-manager-mock">
|
||||
<button onClick={onClose}>Close Profile</button>
|
||||
<button onClick={() => onProfileUpdate({ full_name: 'Updated' })}>Update Profile</button>
|
||||
<button onClick={() => onLoginSuccess({}, 'token', false)}>Login</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock('./features/voice-assistant/VoiceAssistant', () => ({
|
||||
VoiceAssistant: ({ isOpen, onClose }: any) =>
|
||||
isOpen ? (
|
||||
<div data-testid="voice-assistant-mock">
|
||||
<button onClick={onClose}>Close Voice Assistant</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock('./components/FlyerCorrectionTool', () => ({
|
||||
FlyerCorrectionTool: ({ isOpen, onClose, onDataExtracted }: any) =>
|
||||
isOpen ? (
|
||||
<div data-testid="flyer-correction-tool-mock">
|
||||
<button onClick={onClose}>Close Correction</button>
|
||||
<button onClick={() => onDataExtracted('store_name', 'New Store')}>Extract Store</button>
|
||||
<button onClick={() => onDataExtracted('dates', 'New Dates')}>Extract Dates</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
|
||||
// This must be done in any test file that imports App.tsx.
|
||||
vi.mock('pdfjs-dist', () => ({
|
||||
@@ -128,6 +215,7 @@ describe('App Component', () => {
|
||||
mockUseUserData.mockReturnValue({
|
||||
watchedItems: [],
|
||||
shoppingLists: [],
|
||||
isLoadingShoppingLists: false,
|
||||
setWatchedItems: vi.fn(),
|
||||
setShoppingLists: vi.fn(),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { UserProfile } from '../types';
|
||||
import type * as jsonwebtoken from 'jsonwebtoken';
|
||||
import { DatabaseError } from './processingErrors';
|
||||
|
||||
const { transactionalUserRepoMocks, transactionalAdminRepoMocks } = vi.hoisted(() => {
|
||||
return {
|
||||
@@ -33,6 +32,7 @@ describe('AuthService', () => {
|
||||
let adminRepo: typeof import('./db/index.db').adminRepo;
|
||||
let logger: typeof import('./logger.server').logger;
|
||||
let sendPasswordResetEmail: typeof import('./emailService.server').sendPasswordResetEmail;
|
||||
let DatabaseError: typeof import('./processingErrors').DatabaseError;
|
||||
let UniqueConstraintError: typeof import('./db/errors.db').UniqueConstraintError;
|
||||
let RepositoryError: typeof import('./db/errors.db').RepositoryError;
|
||||
let withTransaction: typeof import('./db/index.db').withTransaction;
|
||||
@@ -106,6 +106,7 @@ describe('AuthService', () => {
|
||||
const { validatePasswordStrength } = await import('../utils/authUtils');
|
||||
vi.mocked(validatePasswordStrength).mockReturnValue({ isValid: true, feedback: '' });
|
||||
sendPasswordResetEmail = (await import('./emailService.server')).sendPasswordResetEmail;
|
||||
DatabaseError = (await import('./processingErrors')).DatabaseError;
|
||||
UniqueConstraintError = (await import('./db/errors.db')).UniqueConstraintError;
|
||||
RepositoryError = (await import('./db/errors.db')).RepositoryError;
|
||||
});
|
||||
|
||||
@@ -106,7 +106,13 @@ describe('Address DB Service', () => {
|
||||
'An identical address already exists.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError, address: addressData },
|
||||
{
|
||||
err: dbError,
|
||||
address: addressData,
|
||||
code: '23505',
|
||||
constraint: undefined,
|
||||
detail: undefined,
|
||||
},
|
||||
'Database error in upsertAddress',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -108,25 +108,32 @@ export class FlyerProcessingService {
|
||||
stages[3].status = 'in-progress';
|
||||
await job.updateProgress({ stages });
|
||||
|
||||
const { flyer } = await db.withTransaction(async (client) => {
|
||||
// This assumes createFlyerAndItems is refactored to accept a transactional client.
|
||||
const { flyer: newFlyer } = await createFlyerAndItems(flyerData, itemsForDb, logger, client);
|
||||
let flyerId: number;
|
||||
try {
|
||||
const { flyer } = await db.withTransaction(async (client) => {
|
||||
// This assumes createFlyerAndItems is refactored to accept a transactional client.
|
||||
const { flyer: newFlyer } = await createFlyerAndItems(flyerData, itemsForDb, logger, client);
|
||||
|
||||
// Instantiate a new AdminRepository with the transactional client to ensure
|
||||
// the activity log is part of the same transaction.
|
||||
const transactionalAdminRepo = new AdminRepository(client);
|
||||
await transactionalAdminRepo.logActivity(
|
||||
{
|
||||
action: 'flyer_processed',
|
||||
displayText: `Processed flyer for ${flyerData.store_name}`,
|
||||
details: { flyer_id: newFlyer.flyer_id, store_name: flyerData.store_name },
|
||||
userId: job.data.userId,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
// Instantiate a new AdminRepository with the transactional client to ensure
|
||||
// the activity log is part of the same transaction.
|
||||
const transactionalAdminRepo = new AdminRepository(client);
|
||||
await transactionalAdminRepo.logActivity(
|
||||
{
|
||||
action: 'flyer_processed',
|
||||
displayText: `Processed flyer for ${flyerData.store_name}`,
|
||||
details: { flyer_id: newFlyer.flyer_id, store_name: flyerData.store_name },
|
||||
userId: job.data.userId,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
|
||||
return { flyer: newFlyer };
|
||||
});
|
||||
return { flyer: newFlyer };
|
||||
});
|
||||
flyerId = flyer.flyer_id;
|
||||
} catch (error) {
|
||||
if (error instanceof FlyerProcessingError) throw error;
|
||||
throw new DatabaseError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
stages[3].status = 'completed';
|
||||
await job.updateProgress({ stages });
|
||||
@@ -134,12 +141,12 @@ export class FlyerProcessingService {
|
||||
// Enqueue a job to clean up the original and any generated files.
|
||||
await this.cleanupQueue.add(
|
||||
'cleanup-flyer-files',
|
||||
{ flyerId: flyer.flyer_id, paths: allFilePaths },
|
||||
{ flyerId, paths: allFilePaths },
|
||||
{ removeOnComplete: true },
|
||||
);
|
||||
logger.info(`Successfully processed job and enqueued cleanup for flyer ID: ${flyer.flyer_id}`);
|
||||
logger.info(`Successfully processed job and enqueued cleanup for flyer ID: ${flyerId}`);
|
||||
|
||||
return { flyerId: flyer.flyer_id };
|
||||
return { flyerId };
|
||||
} catch (error) {
|
||||
logger.warn('Job failed. Temporary files will NOT be cleaned up to allow for manual inspection.');
|
||||
// Add detailed logging of the raw error object
|
||||
|
||||
Reference in New Issue
Block a user