large mock refector hopefully done + no errors?
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h19m21s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h19m21s
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -532,7 +532,6 @@ export class AIService {
|
||||
logger.error({ err: apiError }, "Google GenAI API call failed in planTripWithMaps");
|
||||
throw apiError;
|
||||
}
|
||||
/* eslint-enable no-unreachable */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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':
|
||||
|
||||
30
src/types.ts
30
src/types.ts
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user