doc updates and test fixin

This commit is contained in:
2026-01-22 11:17:06 -08:00
parent 9f7b821760
commit fac98f4c54
56 changed files with 11967 additions and 357 deletions

View File

@@ -12,6 +12,18 @@ import path from 'node:path';
import bcrypt from 'bcrypt';
import { logger } from '../services/logger.server';
// Determine base URL based on environment
// Dev container: https://127.0.0.1
// Test: https://flyer-crawler-test.projectium.com
// Production: https://flyer-crawler.projectium.com
const BASE_URL =
process.env.FLYER_BASE_URL ||
process.env.NODE_ENV === 'production'
? 'https://flyer-crawler.projectium.com'
: process.env.NODE_ENV === 'test'
? 'https://flyer-crawler-test.projectium.com'
: 'https://127.0.0.1';
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
@@ -101,7 +113,21 @@ async function main() {
const userId = userRes.rows[0].user_id;
logger.info('Seeded regular user (user@example.com / userpass)');
// 4. Seed a Flyer
// 4. Copy test images to flyer-images directory
logger.info('--- Copying test flyer images... ---');
const flyerImagesDir = path.resolve(process.cwd(), 'public/flyer-images');
await fs.mkdir(flyerImagesDir, { recursive: true });
const testImageSource = path.resolve(process.cwd(), 'src/tests/assets/test-flyer-image.jpg');
const testIconSource = path.resolve(process.cwd(), 'src/tests/assets/test-flyer-icon.png');
const testImageDest = path.join(flyerImagesDir, 'test-flyer-image.jpg');
const testIconDest = path.join(flyerImagesDir, 'test-flyer-icon.png');
await fs.copyFile(testImageSource, testImageDest);
await fs.copyFile(testIconSource, testIconDest);
logger.info(`Copied test images to ${flyerImagesDir}`);
// 5. Seed a Flyer
logger.info('--- Seeding a Sample Flyer... ---');
const today = new Date();
const validFrom = new Date(today);
@@ -111,7 +137,7 @@ async function main() {
const flyerQuery = `
INSERT INTO public.flyers (file_name, image_url, icon_url, checksum, store_id, valid_from, valid_to)
VALUES ('safeway-flyer.jpg', 'https://example.com/flyer-images/safeway-flyer.jpg', 'https://example.com/flyer-images/icons/safeway-flyer.jpg', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
VALUES ('test-flyer-image.jpg', '${BASE_URL}/flyer-images/test-flyer-image.jpg', '${BASE_URL}/flyer-images/test-flyer-icon.png', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
RETURNING flyer_id;
`;
const flyerRes = await client.query<{ flyer_id: number }>(flyerQuery, [
@@ -121,7 +147,7 @@ async function main() {
const flyerId = flyerRes.rows[0].flyer_id;
logger.info(`Seeded flyer for Safeway (ID: ${flyerId}).`);
// 5. Seed Flyer Items
// 6. Seed Flyer Items
logger.info('--- Seeding Flyer Items... ---');
const flyerItems = [
{
@@ -169,7 +195,7 @@ async function main() {
}
logger.info(`Seeded ${flyerItems.length} items for the Safeway flyer.`);
// 6. Seed Watched Items for the user
// 7. Seed Watched Items for the user
logger.info('--- Seeding Watched Items... ---');
const watchedItemIds = [
masterItemMap.get('chicken breast'),
@@ -186,7 +212,7 @@ async function main() {
}
logger.info(`Seeded ${watchedItemIds.length} watched items for Test User.`);
// 7. Seed a Shopping List
// 8. Seed a Shopping List
logger.info('--- Seeding a Shopping List... ---');
const listRes = await client.query<{ shopping_list_id: number }>(
'INSERT INTO public.shopping_lists (user_id, name) VALUES ($1, $2) RETURNING shopping_list_id',

View File

@@ -1,8 +1,9 @@
// src/hooks/mutations/useAuthMutations.ts
import { useMutation } from '@tanstack/react-query';
import * as apiClient from '../../services/apiClient';
import { notifyError } from '../../services/notificationService';
import { notifyError} from '../../services/notificationService';
import type { UserProfile } from '../../types';
import { logger } from '../../services/logger.client';
interface AuthResponse {
userprofile: UserProfile;
@@ -29,7 +30,9 @@ export const useLoginMutation = () => {
password: string;
rememberMe: boolean;
}): Promise<AuthResponse> => {
logger.info('[useLoginMutation] MUTATION STARTED', { email, rememberMe: rememberMe });
const response = await apiClient.loginUser(email, password, rememberMe);
logger.info('[useLoginMutation] Got response', { status: response.status });
if (!response.ok) {
const error = await response.json().catch(() => ({
@@ -38,7 +41,15 @@ export const useLoginMutation = () => {
throw new Error(error.message || 'Failed to login');
}
return response.json();
const result = await response.json();
// DEBUG: Log the actual response structure
logger.debug('[useLoginMutation] Raw API response', { result });
logger.debug('[useLoginMutation] result.data', { data: result.data });
logger.debug('[useLoginMutation] result.data.token', { token: result.data?.token });
// The API returns {success, data: {userprofile, token}}, so extract the data
const extracted = result.data;
logger.info('[useLoginMutation] Returning extracted data', { hasUserprofile: !!extracted?.userprofile, hasToken: !!extracted?.token });
return extracted;
},
onError: (error: Error) => {
notifyError(error.message || 'Failed to login');
@@ -75,7 +86,9 @@ export const useRegisterMutation = () => {
throw new Error(error.message || 'Failed to register');
}
return response.json();
const result = await response.json();
// The API returns {success, data: {userprofile, token}}, so extract the data
return result.data;
},
onError: (error: Error) => {
notifyError(error.message || 'Failed to register');

View File

@@ -3,6 +3,9 @@ import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { useDataExtraction } from './useDataExtraction';
import type { Flyer } from '../types';
import { getFlyerBaseUrl } from '../tests/utils/testHelpers';
const FLYER_BASE_URL = getFlyerBaseUrl();
// Create a mock flyer for testing
const createMockFlyer = (id: number, storeName: string = `Store ${id}`): Flyer => ({
@@ -14,8 +17,8 @@ const createMockFlyer = (id: number, storeName: string = `Store ${id}`): Flyer =
updated_at: '2024-01-01T00:00:00Z',
},
file_name: `flyer${id}.jpg`,
image_url: `https://example.com/flyer${id}.jpg`,
icon_url: `https://example.com/flyer${id}_icon.jpg`,
image_url: `${FLYER_BASE_URL}/flyer${id}.jpg`,
icon_url: `${FLYER_BASE_URL}/flyer${id}_icon.jpg`,
status: 'processed',
item_count: 0,
created_at: '2024-01-01T00:00:00Z',
@@ -76,7 +79,7 @@ describe('useDataExtraction Hook', () => {
expect(updatedFlyer.store?.name).toBe('New Store Name');
// Ensure other properties are preserved
expect(updatedFlyer.flyer_id).toBe(1);
expect(updatedFlyer.image_url).toBe('https://example.com/flyer1.jpg');
expect(updatedFlyer.image_url).toBe(`${FLYER_BASE_URL}/flyer1.jpg`);
});
it('should preserve store_id when updating store name', () => {

View File

@@ -71,6 +71,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const login = useCallback(
async (token: string, profileData?: UserProfile) => {
logger.info(`[AuthProvider-Login] Attempting login.`);
console.log('[AuthProvider-Login] Received token:', token);
console.log('[AuthProvider-Login] Received profileData:', profileData);
setToken(token);
if (profileData) {

View File

@@ -1,12 +1,15 @@
// src/schemas/flyer.schemas.test.ts
import { describe, it, expect } from 'vitest';
import { flyerInsertSchema, flyerDbInsertSchema } from './flyer.schemas';
import { getFlyerBaseUrl } from '../tests/utils/testHelpers';
const FLYER_BASE_URL = getFlyerBaseUrl();
describe('flyerInsertSchema', () => {
const validFlyer = {
file_name: 'flyer.jpg',
image_url: 'https://example.com/flyer.jpg',
icon_url: 'https://example.com/icon.jpg',
image_url: `${FLYER_BASE_URL}/flyer.jpg`,
icon_url: `${FLYER_BASE_URL}/icon.jpg`,
checksum: 'a'.repeat(64),
store_name: 'Test Store',
valid_from: '2023-01-01T00:00:00Z',
@@ -128,8 +131,8 @@ describe('flyerInsertSchema', () => {
describe('flyerDbInsertSchema', () => {
const validDbFlyer = {
file_name: 'flyer.jpg',
image_url: 'https://example.com/flyer.jpg',
icon_url: 'https://example.com/icon.jpg',
image_url: `${FLYER_BASE_URL}/flyer.jpg`,
icon_url: `${FLYER_BASE_URL}/icon.jpg`,
checksum: 'a'.repeat(64),
store_id: 1,
valid_from: '2023-01-01T00:00:00Z',

View File

@@ -62,16 +62,21 @@ const _performTokenRefresh = async (): Promise<string> => {
// This endpoint relies on the HttpOnly cookie, so no body is needed.
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
const result = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to refresh token.');
throw new Error(result.error?.message || result.message || 'Failed to refresh token.');
}
// The API returns {success, data: {token}}, so extract the token
const token = result.data?.token;
if (!token) {
throw new Error('No token received from refresh endpoint.');
}
// On successful refresh, store the new access token.
if (typeof window !== 'undefined') {
localStorage.setItem('authToken', data.token);
localStorage.setItem('authToken', token);
}
logger.info('Successfully refreshed access token.');
return data.token;
return token;
} catch (error) {
logger.error({ error }, 'Failed to refresh token. User session has expired.');
// Only perform browser-specific actions if in the browser environment.

View File

@@ -4,6 +4,9 @@ import { Job, UnrecoverableError } from 'bullmq';
import path from 'node:path';
import type { FlyerInsert } from '../types';
import type { CleanupJobData, FlyerJobData } from '../types/job-data';
import { getFlyerBaseUrl } from '../tests/utils/testHelpers';
const FLYER_BASE_URL = getFlyerBaseUrl();
// 1. Create hoisted mocks FIRST
const mocks = vi.hoisted(() => ({
@@ -840,8 +843,8 @@ describe('FlyerProcessingService', () => {
const job = createMockCleanupJob({ flyerId: 1, paths: [] }); // Empty paths
const mockFlyer = createMockFlyer({
image_url: 'https://example.com/flyer-images/flyer-abc.jpg',
icon_url: 'https://example.com/flyer-images/icons/icon-flyer-abc.webp',
image_url: `${FLYER_BASE_URL}/flyer-images/flyer-abc.jpg`,
icon_url: `${FLYER_BASE_URL}/flyer-images/icons/icon-flyer-abc.webp`,
});
// Mock DB call to return a flyer
vi.mocked(mockedDb.flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -7,7 +7,7 @@ import * as db from '../../services/db/index.db';
import { generateFileChecksum } from '../../utils/checksum';
import { logger } from '../../services/logger.server';
import type { UserProfile } from '../../types';
import { createAndLoginUser } from '../utils/testHelpers';
import { createAndLoginUser, getFlyerBaseUrl } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
import { poll } from '../utils/poll';
import { cleanupFiles } from '../utils/cleanupFiles';
@@ -15,6 +15,8 @@ import piexif from 'piexifjs';
import exifParser from 'exif-parser';
import sharp from 'sharp';
const FLYER_BASE_URL = getFlyerBaseUrl();
// NOTE: STORAGE_PATH is set via the CI environment (deploy-to-test.yml).
// This ensures multer and flyerProcessingService use the test runner's directory
// instead of the production path (/var/www/.../flyer-images).
@@ -93,7 +95,7 @@ vi.mock('../../services/storage/storageService', () => {
await fsModule.writeFile(destPath, content);
// Return a valid URL to satisfy the 'url_check' DB constraint
return `https://example.com/flyer-images/${name}`;
return `${FLYER_BASE_URL}/flyer-images/${name}`;
},
),
delete: vi.fn().mockResolvedValue(undefined),
@@ -464,7 +466,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
}
// Extract the actual processed filename from the saved flyer's image_url
// The URL format is: https://example.com/flyer-images/filename.ext
// The URL format is: ${FLYER_BASE_URL}/flyer-images/filename.ext
const imageUrlPath = new URL(savedFlyer!.image_url).pathname;
const processedFileName = path.basename(imageUrlPath);
const savedImagePath = path.join(uploadDir, processedFileName);
@@ -594,7 +596,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
}
// Extract the actual processed filename from the saved flyer's image_url
// The URL format is: https://example.com/flyer-images/filename.ext
// The URL format is: ${FLYER_BASE_URL}/flyer-images/filename.ext
const imageUrlPath = new URL(savedFlyer!.image_url).pathname;
const processedFileName = path.basename(imageUrlPath);
const savedImagePath = path.join(uploadDir, processedFileName);

View File

@@ -12,6 +12,36 @@ export const getTestBaseUrl = (): string => {
return url.endsWith('/') ? url.slice(0, -1) : url;
};
/**
* Get the flyer base URL for test data based on environment.
* Uses FLYER_BASE_URL if set, otherwise detects environment:
* - Dev container: http://127.0.0.1
* - Test: https://flyer-crawler-test.projectium.com
* - Production: https://flyer-crawler.projectium.com
* - Default: https://example.com (for unit tests)
*/
export const getFlyerBaseUrl = (): string => {
if (process.env.FLYER_BASE_URL) {
return process.env.FLYER_BASE_URL;
}
// Check if we're in dev container (DB_HOST=postgres is typical indicator)
if (process.env.DB_HOST === 'postgres' || process.env.DB_HOST === '127.0.0.1') {
return 'http://127.0.0.1';
}
if (process.env.NODE_ENV === 'production') {
return 'https://flyer-crawler.projectium.com';
}
if (process.env.NODE_ENV === 'test') {
return 'https://flyer-crawler-test.projectium.com';
}
// Default for unit tests
return 'https://example.com';
};
interface CreateUserOptions {
email?: string;
password?: string;