doc updates and test fixin
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
BIN
src/tests/assets/test-flyer-icon.png
Normal file
BIN
src/tests/assets/test-flyer-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user