large mock refector hopefully done + no errors?
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h19m21s

This commit is contained in:
2025-12-21 12:38:53 -08:00
parent 9d5fea19b2
commit 0cf4ca02b7
25 changed files with 156 additions and 120 deletions

View File

@@ -7,7 +7,7 @@ import App from './App';
import * as aiApiClient from './services/aiApiClient'; // Import aiApiClient
import * as apiClient from './services/apiClient';
import { AppProviders } from './providers/AppProviders';
import type { Flyer, User, UserProfile} from './types';
import type { Flyer, UserProfile} from './types';
import { createMockFlyer, createMockUserProfile, createMockUser } from './tests/utils/mockFactories';
import { mockUseAuth, mockUseFlyers, mockUseMasterItems, mockUseUserData, mockUseFlyerItems } from './tests/setup/mockHooks';

View File

@@ -1,12 +1,12 @@
// src/App.tsx
import React, { useState, useCallback, useEffect, useRef } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import * as pdfjsLib from 'pdfjs-dist';
import { Footer } from './components/Footer'; // Assuming this is where your Footer component will live
import { Header } from './components/Header';
import { logger } from './services/logger.client';
import type { Flyer, Profile, User, UserProfile } from './types';
import type { Flyer, Profile, UserProfile } from './types';
import { ProfileManager } from './pages/admin/components/ProfileManager';
import { VoiceAssistant } from './features/voice-assistant/VoiceAssistant';
import { AdminPage } from './pages/admin/AdminPage';

View File

@@ -3,7 +3,7 @@ import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useActiveDeals } from './useActiveDeals';
import * as apiClient from '../services/apiClient';
import type { Flyer, MasterGroceryItem, FlyerItem, DealItem } from '../types';
import type { Flyer, MasterGroceryItem, FlyerItem } from '../types';
import { createMockFlyer, createMockFlyerItem, createMockMasterGroceryItem, createMockDealItem } from '../tests/utils/mockFactories';
import { mockUseFlyers, mockUseUserData } from '../tests/setup/mockHooks';

View File

@@ -125,9 +125,9 @@ export const useAiAnalysis = ({ flyerItems, selectedFlyer, watchedItems, service
break;
}
}
} catch (e: any) {
logger.error(`runAnalysis failed for type ${analysisType}`, { error: e });
const message = e.message || 'An unexpected error occurred.';
} catch (err: unknown) {
logger.error(`runAnalysis failed for type ${analysisType}`, { error: err });
const message = err instanceof Error ? err.message : 'An unexpected error occurred.';
dispatch({ type: 'FETCH_ERROR', payload: { error: message } });
}
}, [service, flyerItems, watchedItems, selectedFlyer]);
@@ -142,9 +142,9 @@ export const useAiAnalysis = ({ flyerItems, selectedFlyer, watchedItems, service
try {
const data = await service.generateImageFromText(mealPlanText);
dispatch({ type: 'FETCH_SUCCESS_IMAGE', payload: { data } });
} catch (e: any) {
logger.error('generateImage failed', { error: e });
const message = e.message || 'An unexpected error occurred during image generation.';
} catch (err: unknown) {
logger.error('generateImage failed', { error: err });
const message = err instanceof Error ? err.message : 'An unexpected error occurred during image generation.';
dispatch({ type: 'FETCH_ERROR', payload: { error: message } });
}
}, [service, state.results]);

View File

@@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useAuth } from './useAuth';
import { AuthProvider } from '../providers/AuthProvider';
import * as apiClient from '../services/apiClient';
import type { User, UserProfile } from '../types';
import type { UserProfile } from '../types';
import { createMockUserProfile } from '../tests/utils/mockFactories';
// Mock the dependencies
@@ -32,7 +32,6 @@ const mockProfile: UserProfile = createMockUserProfile({
role: 'user',
user: { user_id: 'user-abc-123', email: 'test@example.com' },
});
const mockUser: User = mockProfile.user;
// Reusable wrapper for rendering the hook within the provider
const wrapper = ({ children }: { children: ReactNode }) => <AuthProvider>{children}</AuthProvider>;

View File

@@ -33,7 +33,6 @@ const mockedUseUserData = vi.mocked(useUserData);
// Create a mock User object by extracting it from a mock UserProfile
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }) });
const mockUser: User = mockUserProfile.user;
describe('useShoppingLists Hook', () => {
// Create a mock setter function that we can spy on

View File

@@ -28,21 +28,25 @@ const mockLogs: ActivityLogItem[] = [
user_id: 'user-123',
action: 'flyer_processed',
display_text: 'Processed a new flyer for Walmart.',
details: { flyer_id: 1, store_name: 'Walmart', user_avatar_url: 'http://example.com/avatar.png', user_full_name: 'Test User' },
user_avatar_url: 'http://example.com/avatar.png',
user_full_name: 'Test User',
details: { flyer_id: 1, store_name: 'Walmart' },
}),
createMockActivityLogItem({
activity_log_id: 2,
user_id: 'user-456',
action: 'recipe_created',
display_text: 'Jane Doe added a new recipe: Pasta Carbonara',
details: { recipe_id: 1, recipe_name: 'Pasta Carbonara', user_full_name: 'Jane Doe' },
user_full_name: 'Jane Doe',
details: { recipe_id: 1, recipe_name: 'Pasta Carbonara' },
}),
createMockActivityLogItem({
activity_log_id: 3,
user_id: 'user-789',
action: 'list_shared',
display_text: 'John Smith shared a list.',
details: { list_name: 'Weekly Groceries', shopping_list_id: 10, user_full_name: 'John Smith', shared_with_name: 'Test User' },
user_full_name: 'John Smith',
details: { list_name: 'Weekly Groceries', shopping_list_id: 10, shared_with_name: 'Test User' },
}),
createMockActivityLogItem({
activity_log_id: 4,
@@ -56,7 +60,9 @@ const mockLogs: ActivityLogItem[] = [
user_id: 'user-102',
action: 'recipe_favorited',
display_text: 'User favorited a recipe',
details: { recipe_name: 'Best Pizza', user_full_name: 'Pizza Lover', user_avatar_url: 'http://example.com/pizza.png' },
user_full_name: 'Pizza Lover',
user_avatar_url: 'http://example.com/pizza.png',
details: { recipe_name: 'Best Pizza' },
}),
createMockActivityLogItem({
activity_log_id: 6,

View File

@@ -14,7 +14,7 @@ interface ActivityLogProps {
const renderLogDetails = (log: ActivityLogItem, onLogClick?: ActivityLogClickHandler) => {
// With discriminated unions, we can safely access properties based on the 'action' type.
const userName = 'user_full_name' in log.details ? log.details.user_full_name : 'A user';
const userName = log.user_full_name || 'A user';
const isClickable = onLogClick !== undefined;
switch (log.action) {
case 'flyer_processed':
@@ -115,11 +115,11 @@ export const ActivityLog: React.FC<ActivityLogProps> = ({ userProfile, onLogClic
{logs.map((log) => (
<li key={log.activity_log_id} className="flex items-start space-x-3">
<div className="shrink-0">
{log.details?.user_avatar_url ? (
{log.user_avatar_url ? (
(() => {
const altText = log.details.user_full_name || 'User Avatar';
const altText = log.user_full_name || 'User Avatar';
console.log(`[ActivityLog] Rendering avatar for log ${log.activity_log_id}. Alt: "${altText}"`);
return <img className="h-8 w-8 rounded-full" src={log.details.user_avatar_url} alt={altText} />;
return <img className="h-8 w-8 rounded-full" src={log.user_avatar_url} alt={altText} />;
})()
) : (
<span className="h-8 w-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center">

View File

@@ -7,7 +7,7 @@ import * as apiClient from '../../../services/apiClient';
import { notifySuccess, notifyError } from '../../../services/notificationService';
import toast from 'react-hot-toast';
import * as logger from '../../../services/logger.client';
import { createMockProfile, createMockAddress, createMockUser, createMockUserProfile } from '../../../tests/utils/mockFactories';
import { createMockAddress, createMockUser, createMockUserProfile } from '../../../tests/utils/mockFactories';
// Unmock the component to test the real implementation
vi.unmock('./ProfileManager');

View File

@@ -1,7 +1,7 @@
// src/providers/AuthProvider.tsx
import React, { useState, useEffect, useCallback, ReactNode, useMemo } from 'react';
import { AuthContext, AuthContextType } from '../contexts/AuthContext';
import type { User, UserProfile } from '../types';
import type { UserProfile } from '../types';
import * as apiClient from '../services/apiClient';
import { useApi } from '../hooks/useApi';
import { logger } from '../services/logger.client';

View File

@@ -295,11 +295,16 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
extractedData = {};
}
// Ensure items is an array (DB function handles zero-items case)
const itemsArray = Array.isArray(extractedData.items) ? extractedData.items : [];
if (!Array.isArray(extractedData.items)) {
logger.warn('extractedData.items is missing or not an array; proceeding with empty items array.');
}
// Transform the extracted items into the format required for database insertion.
// This adds default values for fields like `view_count` and `click_count`
// and makes this legacy endpoint consistent with the newer FlyerDataTransformer service.
const itemsForDb = (extractedData.items ?? []).map(item => ({
...item,
master_item_id: item.master_item_id === null ? undefined : item.master_item_id,
view_count: 0,
click_count: 0,
updated_at: new Date().toISOString(),
}));
// Ensure we have a valid store name; the DB requires a non-null store name.
const storeName = extractedData.store_name && String(extractedData.store_name).trim().length > 0
@@ -337,7 +342,7 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
};
// 3. Create flyer and its items in a transaction
const { flyer: newFlyer, items: newItems } = await createFlyerAndItems(flyerData, itemsArray, req.log);
const { flyer: newFlyer, items: newItems } = await createFlyerAndItems(flyerData, itemsForDb, req.log);
logger.info(`Successfully processed and saved new flyer: ${newFlyer.file_name} (ID: ${newFlyer.flyer_id}) with ${newItems.length} items.`);

View File

@@ -1,4 +1,4 @@
// src/routes/passport.test.ts
// src/routes/passport.routes.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import * as bcrypt from 'bcrypt';
import { Request, Response, NextFunction } from 'express';
@@ -105,15 +105,21 @@ describe('Passport Configuration', () => {
it('should call done(null, user) on successful authentication', async () => {
// Arrange
const mockUser = {
const mockAuthableProfile = {
...createMockUserProfile({
user_id: 'user-123',
user: { user_id: 'user-123', email: 'test@test.com' },
points: 0,
role: 'user' as const,
}),
...createMockUserWithPasswordHash({
user_id: 'user-123',
email: 'test@test.com',
password_hash: 'hashed_password',
}),
points: 0,
role: 'user' as const,
refresh_token: 'mock-refresh-token',
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockAuthableProfile);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
// Act
@@ -125,17 +131,8 @@ describe('Passport Configuration', () => {
expect(mockedDb.userRepo.findUserWithProfileByEmail).toHaveBeenCalledWith('test@test.com', logger);
expect(bcrypt.compare).toHaveBeenCalledWith('password', 'hashed_password');
expect(mockedDb.adminRepo.resetFailedLoginAttempts).toHaveBeenCalledWith('user-123', '127.0.0.1', logger);
// The strategy transforms the flat DB user into a nested UserProfile structure.
// We need to construct the expected object based on that transformation logic.
const { password_hash, email, ...profileData } = mockUser;
const expectedUserProfile = {
...profileData,
user: {
user_id: mockUser.user_id,
email: mockUser.email,
}
};
// The strategy now just strips auth fields.
const { password_hash, failed_login_attempts, last_failed_login, created_at, updated_at, last_login_ip, refresh_token, email, ...expectedUserProfile } = mockAuthableProfile;
expect(done).toHaveBeenCalledWith(null, expectedUserProfile);
});
@@ -151,13 +148,18 @@ describe('Passport Configuration', () => {
it('should call done(null, false) and increment failed attempts on password mismatch', async () => {
const mockUser = {
...createMockUserProfile({
user_id: 'user-123',
user: { user_id: 'user-123', email: 'test@test.com' },
points: 0,
role: 'user' as const,
}),
...createMockUserWithPasswordHash({
user_id: 'user-123',
email: 'test@test.com',
failed_login_attempts: 1,
}),
points: 0,
role: 'user' as const,
refresh_token: 'mock-refresh-token',
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
@@ -178,13 +180,18 @@ describe('Passport Configuration', () => {
it('should return a lockout message immediately if the final attempt fails', async () => {
const mockUser = {
...createMockUserProfile({
user_id: 'user-123',
user: { user_id: 'user-123', email: 'test@test.com' },
points: 0,
role: 'user' as const,
}),
...createMockUserWithPasswordHash({
user_id: 'user-123',
email: 'test@test.com',
failed_login_attempts: 4, // This is the 4th failed attempt
}),
points: 0,
role: 'user' as const,
refresh_token: 'mock-refresh-token',
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
@@ -202,13 +209,18 @@ describe('Passport Configuration', () => {
it('should call done(null, false) for an OAuth user (no password hash)', async () => {
const mockUser = {
...createMockUserProfile({
user_id: 'oauth-user',
user: { user_id: 'oauth-user', email: 'oauth@test.com' },
points: 0,
role: 'user' as const,
}),
...createMockUserWithPasswordHash({
user_id: 'oauth-user',
email: 'oauth@test.com',
password_hash: null,
}),
points: 0,
role: 'user' as const,
refresh_token: 'mock-refresh-token',
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
@@ -221,14 +233,19 @@ describe('Passport Configuration', () => {
it('should call done(null, false) if account is locked', async () => {
const mockUser = {
...createMockUserProfile({
user_id: 'locked-user',
user: { user_id: 'locked-user', email: 'locked@test.com' },
points: 0,
role: 'user' as const,
}),
...createMockUserWithPasswordHash({
user_id: 'locked-user',
email: 'locked@test.com',
failed_login_attempts: 5,
last_failed_login: new Date().toISOString(),
}),
points: 0,
role: 'user' as const,
refresh_token: 'mock-refresh-token',
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
@@ -241,14 +258,19 @@ describe('Passport Configuration', () => {
it('should allow login if lockout period has expired', async () => {
const mockUser = {
...createMockUserProfile({
user_id: 'expired-lock-user',
user: { user_id: 'expired-lock-user', email: 'expired@test.com' },
points: 0,
role: 'user' as const,
}),
...createMockUserWithPasswordHash({
user_id: 'expired-lock-user',
email: 'expired@test.com',
failed_login_attempts: 5,
last_failed_login: new Date(Date.now() - 20 * 60 * 1000).toISOString(),
}),
points: 0,
role: 'user' as const,
refresh_token: 'mock-refresh-token',
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Correct password

View File

@@ -98,18 +98,11 @@ passport.use(new LocalStrategy(
logger.info(`User successfully authenticated: ${email}`);
// The `user` object from `findUserWithProfileByEmail` is a flat combination of User and Profile.
// We transform it into the nested `UserProfile` structure used by the rest of the application
// to ensure a consistent object shape.
const { password_hash, email: userEmail, ...profileData } = user;
const userProfile: UserProfile = {
...profileData,
user: {
user_id: user.user_id,
email: userEmail,
},
};
// The `user` object from `findUserWithProfileByEmail` is now a fully formed
// UserProfile object with additional authentication fields. We must strip these
// sensitive fields before passing the profile to the session.
// The `...userProfile` rest parameter will contain the clean UserProfile object.
const { password_hash, failed_login_attempts, last_failed_login, refresh_token, email: _, ...userProfile } = user;
return done(null, userProfile);
} catch (err: unknown) {
req.log.error({ error: err }, 'Error during local authentication strategy:');

View File

@@ -2,7 +2,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as aiApiClient from './aiApiClient';
import { AiAnalysisService } from './aiAnalysisService';
import { logger } from './logger.client';
// Mock the dependencies
vi.mock('./aiApiClient');

View File

@@ -20,7 +20,6 @@ vi.mock('./logger.client', () => ({
// 2. Mock ./apiClient to simply pass calls through to the global fetch.
vi.mock('./apiClient', async (importOriginal) => {
const actual = await importOriginal<typeof import('./apiClient')>();
return {
apiFetch: (url: string, options: RequestInit = {}, apiOptions: import('./apiClient').ApiOptions = {}) => {
const fullUrl = url.startsWith('/') ? `http://localhost/api${url}` : url;

View File

@@ -5,7 +5,7 @@
* call the Google AI services. This ensures no API keys are exposed on the client.
*/
import type { FlyerItem, Store, MasterGroceryItem } from '../types';
import { logger } from './logger.client'; // Corrected import path for client-side logger
import { logger } from './logger.client';
import { apiFetch } from './apiClient';
/**
@@ -157,7 +157,7 @@ export const startVoiceSession = (callbacks: {
onmessage: (message: import('@google/genai').LiveServerMessage) => void;
onerror?: (error: ErrorEvent) => void;
onclose?: () => void;
}) : Promise<any> => {
}) : Promise<unknown> => {
logger.debug('Stub: startVoiceSession called.', { callbacks });
// In a real implementation, this would connect to a WebSocket endpoint on your server,
// which would then proxy the connection to the Google AI Live API.

View File

@@ -532,7 +532,6 @@ export class AIService {
logger.error({ err: apiError }, "Google GenAI API call failed in planTripWithMaps");
throw apiError;
}
/* eslint-enable no-unreachable */
}
}

View File

@@ -243,7 +243,7 @@ export function startBackgroundJobs(
// Instantiate the service with its real dependencies for use in the application.
import { personalizationRepo, notificationRepo } from './db/index.db';
import { logger } from './logger.server';
import { emailQueue, tokenCleanupQueue } from './queueService.server';
import { emailQueue } from './queueService.server';
export const backgroundJobService = new BackgroundJobService(
personalizationRepo,

View File

@@ -8,7 +8,7 @@ import { RecipeRepository } from './recipe.db';
vi.unmock('./recipe.db'); // This line is correct.
const mockQuery = mockPoolInstance.query;
import type { Recipe, FavoriteRecipe, RecipeComment } from '../../types';
import type { FavoriteRecipe, RecipeComment } from '../../types';
import { createMockRecipe } from '../../tests/utils/mockFactories';
// Mock the logger to prevent console output during tests. This is a server-side DB test.

View File

@@ -127,17 +127,48 @@ export class UserRepository {
* @param email The email of the user to find.
* @returns A promise that resolves to the combined user and profile object or undefined if not found.
*/
async findUserWithProfileByEmail(email: string, logger: Logger): Promise<(DbUser & Profile) | undefined> {
async findUserWithProfileByEmail(email: string, logger: Logger): Promise<(UserProfile & DbUser) | undefined> {
logger.debug({ email }, `[DB findUserWithProfileByEmail] Searching for user.`);
try {
const query = `
SELECT u.*, p.full_name, p.avatar_url, p.role, p.points, p.preferences, p.address_id
SELECT
u.user_id, u.email, u.password_hash, u.refresh_token, u.failed_login_attempts, u.last_failed_login,
p.full_name, p.avatar_url, p.role, p.points, p.preferences, p.address_id,
p.created_at, p.updated_at
FROM public.users u
JOIN public.profiles p ON u.user_id = p.user_id
WHERE u.email = $1;
`;
const res = await this.db.query<(DbUser & Profile)>(query, [email]);
return res.rows[0];
const res = await this.db.query<any>(query, [email]);
const flatUser = res.rows[0];
if (!flatUser) {
return undefined;
}
// Manually construct the nested UserProfile object and add auth fields
const authableProfile: UserProfile & DbUser = {
user_id: flatUser.user_id,
full_name: flatUser.full_name,
avatar_url: flatUser.avatar_url,
role: flatUser.role,
points: flatUser.points,
preferences: flatUser.preferences,
address_id: flatUser.address_id,
created_at: flatUser.created_at,
updated_at: flatUser.updated_at,
user: {
user_id: flatUser.user_id,
email: flatUser.email,
},
email: flatUser.email,
password_hash: flatUser.password_hash,
failed_login_attempts: flatUser.failed_login_attempts,
last_failed_login: flatUser.last_failed_login,
refresh_token: flatUser.refresh_token,
};
return authableProfile;
} catch (error) {
logger.error({ err: error, email }, 'Database error in findUserWithProfileByEmail');
throw new Error('Failed to retrieve user with profile from database.');

View File

@@ -1,6 +1,7 @@
// src/tests/utils/componentMocks.tsx
import React from 'react';
import { Outlet } from 'react-router-dom';
import { createMockProfile, createMockUser, createMockUserProfile } from './mockFactories';
import { createMockProfile, createMockUserProfile } from './mockFactories';
import type { SuggestedCorrection } from '../../types';
import type { HeaderProps } from '../../components/Header';
import type { ProfileManagerProps } from '../../pages/admin/components/ProfileManager';

View File

@@ -466,7 +466,8 @@ export const createMockActivityLogItem = (overrides: Partial<ActivityLogItem> =
action: 'recipe_favorited',
display_text: 'User favorited a recipe.',
icon: 'heart',
details: { recipe_name: 'Mock Recipe', user_full_name: 'Mock User' },
user_full_name: 'Mock User',
details: { recipe_name: 'Mock Recipe' },
};
break;
case 'flyer_processed':

View File

@@ -87,7 +87,7 @@ export interface MasterGroceryItem {
category_id?: number | null;
category_name?: string | null;
is_allergen?: boolean;
allergy_info?: any | null; // JSONB
allergy_info?: unknown | null; // JSONB
created_by?: string | null;
}
@@ -504,6 +504,9 @@ interface ActivityLogItemBase {
created_at: string;
updated_at: string;
icon?: string | null;
// Joined data for display in feeds
user_full_name?: string;
user_avatar_url?: string;
}
// --- Discriminated Union for Activity Log Details ---
@@ -513,8 +516,6 @@ interface FlyerProcessedLog extends ActivityLogItemBase {
details: {
flyer_id: number;
store_name: string;
user_avatar_url?: string;
user_full_name?: string;
};
}
@@ -523,8 +524,6 @@ interface RecipeCreatedLog extends ActivityLogItemBase {
details: {
recipe_id: number;
recipe_name: string;
user_avatar_url?: string;
user_full_name?: string;
};
}
@@ -532,8 +531,6 @@ interface UserRegisteredLog extends ActivityLogItemBase {
action: 'user_registered';
details: {
full_name: string;
user_avatar_url?: string;
user_full_name?: string;
};
}
@@ -541,8 +538,6 @@ interface RecipeFavoritedLog extends ActivityLogItemBase {
action: 'recipe_favorited';
details: {
recipe_name: string;
user_avatar_url?: string;
user_full_name?: string;
};
}
@@ -552,8 +547,6 @@ interface ListSharedLog extends ActivityLogItemBase {
list_name: string;
shopping_list_id: number;
shared_with_name: string;
user_avatar_url?: string;
user_full_name?: string;
};
}
@@ -762,7 +755,7 @@ export interface ExtractedCoreData {
valid_from: string | null;
valid_to: string | null;
store_address: string | null;
items: Omit<FlyerItem, 'flyer_item_id' | 'created_at' | 'flyer_id'>[];
items: ExtractedFlyerItem[];
}
/**
@@ -787,19 +780,6 @@ export interface ExtractedLogoData {
store_logo_base_64: string | null;
}
/**
* Represents the raw structure of a flyer item as returned by the AI model,
* before it's processed into the final `FlyerItem` shape.
*/
export interface RawFlyerItem {
item: string;
price: string;
quantity: string;
category: string;
master_item_id: number | null;
unit_price?: UnitPrice | null;
}
/**
* Represents the data extracted from a receipt image by the AI service.
*/

View File

@@ -102,20 +102,18 @@ describe('generateFileChecksum', () => {
Object.defineProperty(file, 'arrayBuffer', { value: undefined });
// Mock FileReader to simulate an error
const MockErrorReader = vi.fn(function(this: FileReader) {
interface MockFileReader {
readAsArrayBuffer: () => void;
onerror: (() => void) | null;
error: { message: string };
}
const MockErrorReader = vi.fn(function(this: MockFileReader) {
this.readAsArrayBuffer = () => {
if (this.onerror) {
// Simulate an error event
// We cast to any to create a mock event object that satisfies the type checker.
// The `currentTarget` is what the handler's `this` context will be, and what the type checker
// uses to validate the event's target property.
this.onerror({ currentTarget: this } as any);
this.onerror();
}
};
// Define the error property that the function will read
Object.defineProperty(this, 'error', {
get: () => ({ message: 'Simulated read error' }),
});
this.error = { message: 'Simulated read error' };
});
vi.stubGlobal('FileReader', MockErrorReader);
@@ -128,12 +126,16 @@ describe('generateFileChecksum', () => {
Object.defineProperty(file, 'arrayBuffer', { value: undefined });
// Mock FileReader to return a string
const MockBadResultReader = vi.fn(function(this: FileReader) {
interface MockFileReader {
readAsArrayBuffer: () => void;
onload: (() => void) | null;
result: string | ArrayBuffer;
}
const MockBadResultReader = vi.fn(function(this: MockFileReader) {
this.result = 'this is not an array buffer';
this.readAsArrayBuffer = () => {
// Simulate the onload event with a mock event object.
if (this.onload) this.onload({ currentTarget: this } as any);
if (this.onload) this.onload();
};
Object.defineProperty(this, 'result', { get: () => 'this is not an array buffer' });
});
vi.stubGlobal('FileReader', MockBadResultReader);

View File

@@ -17,7 +17,7 @@ export function omit<T extends object, K extends keyof T>(obj: T, keysToOmit: K[
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && !keysToOmitSet.has(key as keyof T as K)) {
// If the key is not in the omit set, add it to the new object.
(newObj as any)[key] = obj[key];
(newObj as Record<string, unknown>)[key] = obj[key];
}
}