Compare commits

..

17 Commits

Author SHA1 Message Date
Gitea Actions
f49f3a75fb ci: Bump version to 0.9.44 [skip ci] 2026-01-06 13:41:43 +05:00
8f14044ae6 debugging the flyer integration issue
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m27s
2026-01-06 00:41:03 -08:00
Gitea Actions
55e1e425f4 ci: Bump version to 0.9.43 [skip ci] 2026-01-06 12:56:47 +05:00
68b16ad2e8 fix the dang integration tests
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m2s
2026-01-05 23:53:54 -08:00
Gitea Actions
6a28934692 ci: Bump version to 0.9.42 [skip ci] 2026-01-06 12:25:08 +05:00
78c4a5fee6 fix the dang integration tests
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
2026-01-05 23:20:56 -08:00
Gitea Actions
1ce5f481a8 ci: Bump version to 0.9.41 [skip ci] 2026-01-06 11:39:28 +05:00
Gitea Actions
e0120d38fd ci: Bump version to 0.9.39 [skip ci] 2026-01-06 11:39:27 +05:00
6b2079ef2c fix the dang integration tests
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 32m44s
2026-01-05 22:38:21 -08:00
Gitea Actions
0478e176d5 ci: Bump version to 0.9.38 [skip ci] 2026-01-06 10:23:22 +05:00
47f7f97cd9 fuck database contraints - seems buggy
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 32m10s
2026-01-05 21:16:08 -08:00
Gitea Actions
b0719d1e39 ci: Bump version to 0.9.37 [skip ci] 2026-01-06 10:11:19 +05:00
0039ac3752 fuck database contraints - seems buggy
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 37s
2026-01-05 21:08:16 -08:00
Gitea Actions
3c8316f4f7 ci: Bump version to 0.9.36 [skip ci] 2026-01-06 09:03:20 +05:00
2564df1c64 get rid of localhost in tests - not a qualified URL - we'll see
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 33m19s
2026-01-05 20:02:44 -08:00
Gitea Actions
696c547238 ci: Bump version to 0.9.35 [skip ci] 2026-01-06 08:11:42 +05:00
38165bdb9a get rid of localhost in tests - not a qualified URL - we'll see
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m14s
2026-01-05 19:10:46 -08:00
56 changed files with 268 additions and 185 deletions

View File

@@ -113,7 +113,7 @@ jobs:
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD_TEST }} REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD_TEST }}
# --- Integration test specific variables --- # --- Integration test specific variables ---
FRONTEND_URL: 'http://localhost:3000' FRONTEND_URL: 'https://example.com'
VITE_API_BASE_URL: 'http://localhost:3001/api' VITE_API_BASE_URL: 'http://localhost:3001/api'
GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY }} GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY }}
@@ -389,7 +389,7 @@ jobs:
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD_TEST }} REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD_TEST }}
# Application Secrets # Application Secrets
FRONTEND_URL: 'https://flyer-crawler-test.projectium.com' FRONTEND_URL: 'https://example.com'
JWT_SECRET: ${{ secrets.JWT_SECRET }} JWT_SECRET: ${{ secrets.JWT_SECRET }}
GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY_TEST }} GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY_TEST }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "flyer-crawler", "name": "flyer-crawler",
"version": "0.9.34", "version": "0.9.44",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "flyer-crawler", "name": "flyer-crawler",
"version": "0.9.34", "version": "0.9.44",
"dependencies": { "dependencies": {
"@bull-board/api": "^6.14.2", "@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2", "@bull-board/express": "^6.14.2",

View File

@@ -1,7 +1,7 @@
{ {
"name": "flyer-crawler", "name": "flyer-crawler",
"private": true, "private": true,
"version": "0.9.34", "version": "0.9.44",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"", "dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -90,10 +90,10 @@ CREATE TABLE IF NOT EXISTS public.profiles (
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT profiles_full_name_check CHECK (full_name IS NULL OR TRIM(full_name) <> ''), CONSTRAINT profiles_full_name_check CHECK (full_name IS NULL OR TRIM(full_name) <> ''),
CONSTRAINT profiles_avatar_url_check CHECK (avatar_url IS NULL OR avatar_url ~* '^https://?.*'),
created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL, created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL,
updated_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL updated_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL
); );
-- CONSTRAINT profiles_avatar_url_check CHECK (avatar_url IS NULL OR avatar_url ~* '^https://?.*'),
COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.'; COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.';
COMMENT ON COLUMN public.profiles.address_id IS 'A foreign key to the user''s primary address in the `addresses` table.'; COMMENT ON COLUMN public.profiles.address_id IS 'A foreign key to the user''s primary address in the `addresses` table.';
-- This index is crucial for the gamification leaderboard feature. -- This index is crucial for the gamification leaderboard feature.
@@ -108,9 +108,9 @@ CREATE TABLE IF NOT EXISTS public.stores (
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT stores_name_check CHECK (TRIM(name) <> ''), CONSTRAINT stores_name_check CHECK (TRIM(name) <> ''),
CONSTRAINT stores_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https://?.*'),
created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL
); );
-- CONSTRAINT stores_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https://?.*'),
COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).'; COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).';
-- 5. The 'categories' table for normalized category data. -- 5. The 'categories' table for normalized category data.
@@ -141,10 +141,10 @@ CREATE TABLE IF NOT EXISTS public.flyers (
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT flyers_valid_dates_check CHECK (valid_to >= valid_from), CONSTRAINT flyers_valid_dates_check CHECK (valid_to >= valid_from),
CONSTRAINT flyers_file_name_check CHECK (TRIM(file_name) <> ''), CONSTRAINT flyers_file_name_check CHECK (TRIM(file_name) <> ''),
CONSTRAINT flyers_image_url_check CHECK (image_url ~* '^https://?.*'),
CONSTRAINT flyers_icon_url_check CHECK (icon_url IS NULL OR icon_url ~* '^https://?.*'),
CONSTRAINT flyers_checksum_check CHECK (checksum IS NULL OR length(checksum) = 64) CONSTRAINT flyers_checksum_check CHECK (checksum IS NULL OR length(checksum) = 64)
); );
-- CONSTRAINT flyers_image_url_check CHECK (image_url ~* '^https://?.*'),
-- CONSTRAINT flyers_icon_url_check CHECK (icon_url IS NULL OR icon_url ~* '^https://?.*'),
COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.'; COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.';
CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id); CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id);
COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").'; COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").';
@@ -198,9 +198,9 @@ CREATE TABLE IF NOT EXISTS public.brands (
store_id BIGINT REFERENCES public.stores(store_id) ON DELETE SET NULL, store_id BIGINT REFERENCES public.stores(store_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT brands_name_check CHECK (TRIM(name) <> ''), CONSTRAINT brands_name_check CHECK (TRIM(name) <> '')
CONSTRAINT brands_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https://?.*')
); );
-- CONSTRAINT brands_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https://?.*')
COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".'; COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".';
COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.'; COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.';
@@ -464,9 +464,9 @@ CREATE TABLE IF NOT EXISTS public.user_submitted_prices (
upvotes INTEGER DEFAULT 0 NOT NULL CHECK (upvotes >= 0), upvotes INTEGER DEFAULT 0 NOT NULL CHECK (upvotes >= 0),
downvotes INTEGER DEFAULT 0 NOT NULL CHECK (downvotes >= 0), downvotes INTEGER DEFAULT 0 NOT NULL CHECK (downvotes >= 0),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
CONSTRAINT user_submitted_prices_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https://?.*')
); );
-- CONSTRAINT user_submitted_prices_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https://?.*')
COMMENT ON TABLE public.user_submitted_prices IS 'Stores item prices submitted by users directly from physical stores.'; COMMENT ON TABLE public.user_submitted_prices IS 'Stores item prices submitted by users directly from physical stores.';
COMMENT ON COLUMN public.user_submitted_prices.photo_url IS 'URL to user-submitted photo evidence of the price.'; COMMENT ON COLUMN public.user_submitted_prices.photo_url IS 'URL to user-submitted photo evidence of the price.';
COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.'; COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.';
@@ -521,9 +521,9 @@ CREATE TABLE IF NOT EXISTS public.recipes (
fork_count INTEGER DEFAULT 0 NOT NULL CHECK (fork_count >= 0), fork_count INTEGER DEFAULT 0 NOT NULL CHECK (fork_count >= 0),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT recipes_name_check CHECK (TRIM(name) <> ''), CONSTRAINT recipes_name_check CHECK (TRIM(name) <> '')
CONSTRAINT recipes_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https://?.*')
); );
-- CONSTRAINT recipes_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https://?.*')
COMMENT ON TABLE public.recipes IS 'Stores recipes that can be used to generate shopping lists.'; COMMENT ON TABLE public.recipes IS 'Stores recipes that can be used to generate shopping lists.';
COMMENT ON COLUMN public.recipes.servings IS 'The number of servings this recipe yields.'; COMMENT ON COLUMN public.recipes.servings IS 'The number of servings this recipe yields.';
COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.'; COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.';
@@ -920,9 +920,9 @@ CREATE TABLE IF NOT EXISTS public.receipts (
raw_text TEXT, raw_text TEXT,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
processed_at TIMESTAMPTZ, processed_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
CONSTRAINT receipts_receipt_image_url_check CHECK (receipt_image_url ~* '^https://?.*')
); );
-- CONSTRAINT receipts_receipt_image_url_check CHECK (receipt_image_url ~* '^https://?.*')
COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase tracking and analysis.'; COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase tracking and analysis.';
CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id); CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id);
CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id); CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id);

View File

@@ -106,10 +106,10 @@ CREATE TABLE IF NOT EXISTS public.profiles (
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT profiles_full_name_check CHECK (full_name IS NULL OR TRIM(full_name) <> ''), CONSTRAINT profiles_full_name_check CHECK (full_name IS NULL OR TRIM(full_name) <> ''),
CONSTRAINT profiles_avatar_url_check CHECK (avatar_url IS NULL OR avatar_url ~* '^https?://.*'),
created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL, created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL,
updated_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL updated_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL
); );
-- CONSTRAINT profiles_avatar_url_check CHECK (avatar_url IS NULL OR avatar_url ~* '^https?://.*'),
COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.'; COMMENT ON TABLE public.profiles IS 'Stores public-facing user data, linked to the public.users table.';
COMMENT ON COLUMN public.profiles.address_id IS 'A foreign key to the user''s primary address in the `addresses` table.'; COMMENT ON COLUMN public.profiles.address_id IS 'A foreign key to the user''s primary address in the `addresses` table.';
-- This index is crucial for the gamification leaderboard feature. -- This index is crucial for the gamification leaderboard feature.
@@ -124,9 +124,9 @@ CREATE TABLE IF NOT EXISTS public.stores (
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT stores_name_check CHECK (TRIM(name) <> ''), CONSTRAINT stores_name_check CHECK (TRIM(name) <> ''),
CONSTRAINT stores_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https?://.*'),
created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL created_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL
); );
-- CONSTRAINT stores_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https?://.*'),
COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).'; COMMENT ON TABLE public.stores IS 'Stores metadata for grocery store chains (e.g., Safeway, Kroger).';
-- 5. The 'categories' table for normalized category data. -- 5. The 'categories' table for normalized category data.
@@ -157,10 +157,10 @@ CREATE TABLE IF NOT EXISTS public.flyers (
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT flyers_valid_dates_check CHECK (valid_to >= valid_from), CONSTRAINT flyers_valid_dates_check CHECK (valid_to >= valid_from),
CONSTRAINT flyers_file_name_check CHECK (TRIM(file_name) <> ''), CONSTRAINT flyers_file_name_check CHECK (TRIM(file_name) <> ''),
CONSTRAINT flyers_image_url_check CHECK (image_url ~* '^https?://.*'),
CONSTRAINT flyers_icon_url_check CHECK (icon_url ~* '^https?://.*'),
CONSTRAINT flyers_checksum_check CHECK (checksum IS NULL OR length(checksum) = 64) CONSTRAINT flyers_checksum_check CHECK (checksum IS NULL OR length(checksum) = 64)
); );
-- CONSTRAINT flyers_image_url_check CHECK (image_url ~* '^https?://.*'),
-- CONSTRAINT flyers_icon_url_check CHECK (icon_url ~* '^https?://.*'),
COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.'; COMMENT ON TABLE public.flyers IS 'Stores metadata for each processed flyer, linking it to a store and its validity period.';
CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id); CREATE INDEX IF NOT EXISTS idx_flyers_store_id ON public.flyers(store_id);
COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").'; COMMENT ON COLUMN public.flyers.file_name IS 'The original name of the uploaded flyer file (e.g., "flyer_week_1.pdf").';
@@ -214,9 +214,9 @@ CREATE TABLE IF NOT EXISTS public.brands (
store_id BIGINT REFERENCES public.stores(store_id) ON DELETE SET NULL, store_id BIGINT REFERENCES public.stores(store_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT brands_name_check CHECK (TRIM(name) <> ''), CONSTRAINT brands_name_check CHECK (TRIM(name) <> '')
CONSTRAINT brands_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https?://.*')
); );
-- CONSTRAINT brands_logo_url_check CHECK (logo_url IS NULL OR logo_url ~* '^https?://.*')
COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".'; COMMENT ON TABLE public.brands IS 'Stores brand names like "Coca-Cola", "Maple Leaf", or "Kraft".';
COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.'; COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (e.g., President''s Choice), this links to the parent store.';
@@ -481,9 +481,9 @@ CREATE TABLE IF NOT EXISTS public.user_submitted_prices (
upvotes INTEGER DEFAULT 0 NOT NULL CHECK (upvotes >= 0), upvotes INTEGER DEFAULT 0 NOT NULL CHECK (upvotes >= 0),
downvotes INTEGER DEFAULT 0 NOT NULL CHECK (downvotes >= 0), downvotes INTEGER DEFAULT 0 NOT NULL CHECK (downvotes >= 0),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
CONSTRAINT user_submitted_prices_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https?://.*')
); );
-- CONSTRAINT user_submitted_prices_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https?://.*')
COMMENT ON TABLE public.user_submitted_prices IS 'Stores item prices submitted by users directly from physical stores.'; COMMENT ON TABLE public.user_submitted_prices IS 'Stores item prices submitted by users directly from physical stores.';
COMMENT ON COLUMN public.user_submitted_prices.photo_url IS 'URL to user-submitted photo evidence of the price.'; COMMENT ON COLUMN public.user_submitted_prices.photo_url IS 'URL to user-submitted photo evidence of the price.';
COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.'; COMMENT ON COLUMN public.user_submitted_prices.upvotes IS 'Community validation score indicating accuracy.';
@@ -538,9 +538,9 @@ CREATE TABLE IF NOT EXISTS public.recipes (
fork_count INTEGER DEFAULT 0 NOT NULL CHECK (fork_count >= 0), fork_count INTEGER DEFAULT 0 NOT NULL CHECK (fork_count >= 0),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL, updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
CONSTRAINT recipes_name_check CHECK (TRIM(name) <> ''), CONSTRAINT recipes_name_check CHECK (TRIM(name) <> '')
CONSTRAINT recipes_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https?://.*')
); );
-- CONSTRAINT recipes_photo_url_check CHECK (photo_url IS NULL OR photo_url ~* '^https?://.*')
COMMENT ON TABLE public.recipes IS 'Stores recipes that can be used to generate shopping lists.'; COMMENT ON TABLE public.recipes IS 'Stores recipes that can be used to generate shopping lists.';
COMMENT ON COLUMN public.recipes.servings IS 'The number of servings this recipe yields.'; COMMENT ON COLUMN public.recipes.servings IS 'The number of servings this recipe yields.';
COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.'; COMMENT ON COLUMN public.recipes.original_recipe_id IS 'If this recipe is a variation of another, this points to the original.';
@@ -940,9 +940,9 @@ CREATE TABLE IF NOT EXISTS public.receipts (
raw_text TEXT, raw_text TEXT,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
processed_at TIMESTAMPTZ, processed_at TIMESTAMPTZ,
CONSTRAINT receipts_receipt_image_url_check CHECK (receipt_image_url ~* '^https?://.*'),
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
); );
-- CONSTRAINT receipts_receipt_image_url_check CHECK (receipt_image_url ~* '^https?://.*'),
COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase tracking and analysis.'; COMMENT ON TABLE public.receipts IS 'Stores uploaded user receipts for purchase tracking and analysis.';
CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id); CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON public.receipts(user_id);
CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id); CREATE INDEX IF NOT EXISTS idx_receipts_store_id ON public.receipts(store_id);

View File

@@ -628,7 +628,7 @@ describe('App Component', () => {
app: { app: {
version: '2.0.0', version: '2.0.0',
commitMessage: 'A new version!', commitMessage: 'A new version!',
commitUrl: 'http://example.com/commit/2.0.0', commitUrl: 'https://example.com/commit/2.0.0',
}, },
}, },
})); }));
@@ -638,7 +638,7 @@ describe('App Component', () => {
renderApp(); renderApp();
const versionLink = screen.getByText(`Version: 2.0.0`); const versionLink = screen.getByText(`Version: 2.0.0`);
expect(versionLink).toBeInTheDocument(); expect(versionLink).toBeInTheDocument();
expect(versionLink).toHaveAttribute('href', 'http://example.com/commit/2.0.0'); expect(versionLink).toHaveAttribute('href', 'https://example.com/commit/2.0.0');
}); });
it('should open the "What\'s New" modal when the question mark icon is clicked', async () => { it('should open the "What\'s New" modal when the question mark icon is clicked', async () => {

View File

@@ -19,7 +19,7 @@ const mockedNotifyError = notifyError as Mocked<typeof notifyError>;
const defaultProps = { const defaultProps = {
isOpen: true, isOpen: true,
onClose: vi.fn(), onClose: vi.fn(),
imageUrl: 'http://example.com/flyer.jpg', imageUrl: 'https://example.com/flyer.jpg',
onDataExtracted: vi.fn(), onDataExtracted: vi.fn(),
}; };

View File

@@ -25,7 +25,7 @@ const mockLeaderboardData: LeaderboardUser[] = [
createMockLeaderboardUser({ createMockLeaderboardUser({
user_id: 'user-2', user_id: 'user-2',
full_name: 'Bob', full_name: 'Bob',
avatar_url: 'http://example.com/bob.jpg', avatar_url: 'https://example.com/bob.jpg',
points: 950, points: 950,
rank: '2', rank: '2',
}), }),
@@ -95,7 +95,7 @@ describe('Leaderboard', () => {
// Check for correct avatar URLs // Check for correct avatar URLs
const bobAvatar = screen.getByAltText('Bob') as HTMLImageElement; const bobAvatar = screen.getByAltText('Bob') as HTMLImageElement;
expect(bobAvatar.src).toBe('http://example.com/bob.jpg'); expect(bobAvatar.src).toBe('https://example.com/bob.jpg');
const aliceAvatar = screen.getByAltText('Alice') as HTMLImageElement; const aliceAvatar = screen.getByAltText('Alice') as HTMLImageElement;
expect(aliceAvatar.src).toContain('api.dicebear.com'); // Check for fallback avatar expect(aliceAvatar.src).toContain('api.dicebear.com'); // Check for fallback avatar

View File

@@ -160,9 +160,9 @@ describe('AnalysisPanel', () => {
results: { WEB_SEARCH: 'Search results text.' }, results: { WEB_SEARCH: 'Search results text.' },
sources: { sources: {
WEB_SEARCH: [ WEB_SEARCH: [
{ title: 'Valid Source', uri: 'http://example.com/source1' }, { title: 'Valid Source', uri: 'https://example.com/source1' },
{ title: 'Source without URI', uri: null }, { title: 'Source without URI', uri: null },
{ title: 'Another Valid Source', uri: 'http://example.com/source2' }, { title: 'Another Valid Source', uri: 'https://example.com/source2' },
], ],
}, },
loadingAnalysis: null, loadingAnalysis: null,
@@ -178,7 +178,7 @@ describe('AnalysisPanel', () => {
expect(screen.getByText('Sources:')).toBeInTheDocument(); expect(screen.getByText('Sources:')).toBeInTheDocument();
const source1 = screen.getByText('Valid Source'); const source1 = screen.getByText('Valid Source');
expect(source1).toBeInTheDocument(); expect(source1).toBeInTheDocument();
expect(source1.closest('a')).toHaveAttribute('href', 'http://example.com/source1'); expect(source1.closest('a')).toHaveAttribute('href', 'https://example.com/source1');
expect(screen.queryByText('Source without URI')).not.toBeInTheDocument(); expect(screen.queryByText('Source without URI')).not.toBeInTheDocument();
expect(screen.getByText('Another Valid Source')).toBeInTheDocument(); expect(screen.getByText('Another Valid Source')).toBeInTheDocument();
}); });
@@ -278,13 +278,13 @@ describe('AnalysisPanel', () => {
loadingAnalysis: null, loadingAnalysis: null,
error: null, error: null,
runAnalysis: mockRunAnalysis, runAnalysis: mockRunAnalysis,
generatedImageUrl: 'http://example.com/meal.jpg', generatedImageUrl: 'https://example.com/meal.jpg',
generateImage: mockGenerateImage, generateImage: mockGenerateImage,
}); });
rerender(<AnalysisPanel selectedFlyer={mockFlyer} />); rerender(<AnalysisPanel selectedFlyer={mockFlyer} />);
const image = screen.getByAltText('AI generated meal plan'); const image = screen.getByAltText('AI generated meal plan');
expect(image).toBeInTheDocument(); expect(image).toBeInTheDocument();
expect(image).toHaveAttribute('src', 'http://example.com/meal.jpg'); expect(image).toHaveAttribute('src', 'https://example.com/meal.jpg');
}); });
it('should not show sources for non-search analysis types', () => { it('should not show sources for non-search analysis types', () => {

View File

@@ -8,13 +8,13 @@ import { createMockStore } from '../../tests/utils/mockFactories';
const mockStore = createMockStore({ const mockStore = createMockStore({
store_id: 1, store_id: 1,
name: 'SuperMart', name: 'SuperMart',
logo_url: 'http://example.com/logo.png', logo_url: 'https://example.com/logo.png',
}); });
const mockOnOpenCorrectionTool = vi.fn(); const mockOnOpenCorrectionTool = vi.fn();
const defaultProps = { const defaultProps = {
imageUrl: 'http://example.com/flyer.jpg', imageUrl: 'https://example.com/flyer.jpg',
store: mockStore, store: mockStore,
validFrom: '2023-10-26', validFrom: '2023-10-26',
validTo: '2023-11-01', validTo: '2023-11-01',

View File

@@ -19,7 +19,7 @@ const mockFlyers: Flyer[] = [
flyer_id: 1, flyer_id: 1,
file_name: 'metro_flyer_oct_1.pdf', file_name: 'metro_flyer_oct_1.pdf',
item_count: 50, item_count: 50,
image_url: 'http://example.com/flyer1.jpg', image_url: 'https://example.com/flyer1.jpg',
store: { store_id: 101, name: 'Metro' }, store: { store_id: 101, name: 'Metro' },
valid_from: '2023-10-05', valid_from: '2023-10-05',
valid_to: '2023-10-11', valid_to: '2023-10-11',
@@ -29,7 +29,7 @@ const mockFlyers: Flyer[] = [
flyer_id: 2, flyer_id: 2,
file_name: 'walmart_flyer.pdf', file_name: 'walmart_flyer.pdf',
item_count: 75, item_count: 75,
image_url: 'http://example.com/flyer2.jpg', image_url: 'https://example.com/flyer2.jpg',
store: { store_id: 102, name: 'Walmart' }, store: { store_id: 102, name: 'Walmart' },
valid_from: '2023-10-06', valid_from: '2023-10-06',
valid_to: '2023-10-06', // Same day valid_to: '2023-10-06', // Same day
@@ -40,8 +40,8 @@ const mockFlyers: Flyer[] = [
flyer_id: 3, flyer_id: 3,
file_name: 'no-store-flyer.pdf', file_name: 'no-store-flyer.pdf',
item_count: 10, item_count: 10,
image_url: 'http://example.com/flyer3.jpg', image_url: 'https://example.com/flyer3.jpg',
icon_url: 'http://example.com/icon3.png', icon_url: 'https://example.com/icon3.png',
valid_from: '2023-10-07', valid_from: '2023-10-07',
valid_to: '2023-10-08', valid_to: '2023-10-08',
store_address: '456 Side St, Ottawa', store_address: '456 Side St, Ottawa',
@@ -53,7 +53,7 @@ const mockFlyers: Flyer[] = [
flyer_id: 4, flyer_id: 4,
file_name: 'bad-date-flyer.pdf', file_name: 'bad-date-flyer.pdf',
item_count: 5, item_count: 5,
image_url: 'http://example.com/flyer4.jpg', image_url: 'https://example.com/flyer4.jpg',
store: { store_id: 103, name: 'Date Store' }, store: { store_id: 103, name: 'Date Store' },
created_at: 'invalid-date', created_at: 'invalid-date',
valid_from: 'invalid-from', valid_from: 'invalid-from',
@@ -163,7 +163,7 @@ describe('FlyerList', () => {
const flyerWithIcon = screen.getByText('Unknown Store').closest('li'); // Flyer ID 3 const flyerWithIcon = screen.getByText('Unknown Store').closest('li'); // Flyer ID 3
const iconImage = flyerWithIcon?.querySelector('img'); const iconImage = flyerWithIcon?.querySelector('img');
expect(iconImage).toBeInTheDocument(); expect(iconImage).toBeInTheDocument();
expect(iconImage).toHaveAttribute('src', 'http://example.com/icon3.png'); expect(iconImage).toHaveAttribute('src', 'https://example.com/icon3.png');
}); });
it('should render a document icon when icon_url is not present', () => { it('should render a document icon when icon_url is not present', () => {

View File

@@ -15,8 +15,8 @@ describe('useFlyerItems Hook', () => {
const mockFlyer = createMockFlyer({ const mockFlyer = createMockFlyer({
flyer_id: 123, flyer_id: 123,
file_name: 'test-flyer.jpg', file_name: 'test-flyer.jpg',
image_url: 'http://example.com/test.jpg', image_url: 'https://example.com/test.jpg',
icon_url: 'http://example.com/icon.jpg', icon_url: 'https://example.com/icon.jpg',
checksum: 'abc', checksum: 'abc',
valid_from: '2024-01-01', valid_from: '2024-01-01',
valid_to: '2024-01-07', valid_to: '2024-01-07',

View File

@@ -72,7 +72,7 @@ describe('useFlyers Hook and FlyersProvider', () => {
createMockFlyer({ createMockFlyer({
flyer_id: 1, flyer_id: 1,
file_name: 'flyer1.jpg', file_name: 'flyer1.jpg',
image_url: 'http://example.com/flyer1.jpg', image_url: 'https://example.com/flyer1.jpg',
item_count: 5, item_count: 5,
created_at: '2024-01-01', created_at: '2024-01-01',
}), }),

View File

@@ -79,7 +79,7 @@ describe('HomePage Component', () => {
describe('when a flyer is selected', () => { describe('when a flyer is selected', () => {
const mockFlyer: Flyer = createMockFlyer({ const mockFlyer: Flyer = createMockFlyer({
flyer_id: 1, flyer_id: 1,
image_url: 'http://example.com/flyer.jpg', image_url: 'https://example.com/flyer.jpg',
}); });
it('should render FlyerDisplay but not data tables if there are no flyer items', () => { it('should render FlyerDisplay but not data tables if there are no flyer items', () => {

View File

@@ -26,7 +26,7 @@ const mockedApiClient = vi.mocked(apiClient);
const mockProfile: UserProfile = createMockUserProfile({ const mockProfile: UserProfile = createMockUserProfile({
user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }), user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }),
full_name: 'Test User', full_name: 'Test User',
avatar_url: 'http://example.com/avatar.jpg', avatar_url: 'https://example.com/avatar.jpg',
points: 150, points: 150,
role: 'user', role: 'user',
}); });
@@ -359,7 +359,7 @@ describe('UserProfilePage', () => {
}); });
it('should upload a new avatar and update the image source', async () => { it('should upload a new avatar and update the image source', async () => {
const updatedProfile = { ...mockProfile, avatar_url: 'http://example.com/new-avatar.png' }; const updatedProfile = { ...mockProfile, avatar_url: 'https://example.com/new-avatar.png' };
// Log when the mock is called // Log when the mock is called
mockedApiClient.uploadAvatar.mockImplementation((file) => { mockedApiClient.uploadAvatar.mockImplementation((file) => {

View File

@@ -30,7 +30,7 @@ const mockLogs: ActivityLogItem[] = [
user_id: 'user-123', user_id: 'user-123',
action: 'flyer_processed', action: 'flyer_processed',
display_text: 'Processed a new flyer for Walmart.', display_text: 'Processed a new flyer for Walmart.',
user_avatar_url: 'http://example.com/avatar.png', user_avatar_url: 'https://example.com/avatar.png',
user_full_name: 'Test User', user_full_name: 'Test User',
details: { flyer_id: 1, store_name: 'Walmart' }, details: { flyer_id: 1, store_name: 'Walmart' },
}), }),
@@ -63,7 +63,7 @@ const mockLogs: ActivityLogItem[] = [
action: 'recipe_favorited', action: 'recipe_favorited',
display_text: 'User favorited a recipe', display_text: 'User favorited a recipe',
user_full_name: 'Pizza Lover', user_full_name: 'Pizza Lover',
user_avatar_url: 'http://example.com/pizza.png', user_avatar_url: 'https://example.com/pizza.png',
details: { recipe_name: 'Best Pizza' }, details: { recipe_name: 'Best Pizza' },
}), }),
createMockActivityLogItem({ createMockActivityLogItem({
@@ -136,7 +136,7 @@ describe('ActivityLog', () => {
// Check for avatar // Check for avatar
const avatar = screen.getByAltText('Test User'); const avatar = screen.getByAltText('Test User');
expect(avatar).toBeInTheDocument(); expect(avatar).toBeInTheDocument();
expect(avatar).toHaveAttribute('src', 'http://example.com/avatar.png'); expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.png');
// Check for fallback avatar (Newbie User has no avatar) // Check for fallback avatar (Newbie User has no avatar)
// The fallback is an SVG inside a span. We can check for the span's class or the SVG. // The fallback is an SVG inside a span. We can check for the span's class or the SVG.

View File

@@ -59,14 +59,14 @@ describe('FlyerReviewPage', () => {
file_name: 'flyer1.jpg', file_name: 'flyer1.jpg',
created_at: '2023-01-01T00:00:00Z', created_at: '2023-01-01T00:00:00Z',
store: { name: 'Store A' }, store: { name: 'Store A' },
icon_url: 'http://example.com/icon1.jpg', icon_url: 'https://example.com/icon1.jpg',
}, },
{ {
flyer_id: 2, flyer_id: 2,
file_name: 'flyer2.jpg', file_name: 'flyer2.jpg',
created_at: '2023-01-02T00:00:00Z', created_at: '2023-01-02T00:00:00Z',
store: { name: 'Store B' }, store: { name: 'Store B' },
icon_url: 'http://example.com/icon2.jpg', icon_url: 'https://example.com/icon2.jpg',
}, },
{ {
flyer_id: 3, flyer_id: 3,

View File

@@ -19,7 +19,7 @@ const mockBrands = [
brand_id: 2, brand_id: 2,
name: 'Compliments', name: 'Compliments',
store_name: 'Sobeys', store_name: 'Sobeys',
logo_url: 'http://example.com/compliments.png', logo_url: 'https://example.com/compliments.png',
}), }),
]; ];
@@ -92,7 +92,7 @@ describe('AdminBrandManager', () => {
); );
mockedApiClient.uploadBrandLogo.mockImplementation( mockedApiClient.uploadBrandLogo.mockImplementation(
async () => async () =>
new Response(JSON.stringify({ logoUrl: 'http://example.com/new-logo.png' }), { new Response(JSON.stringify({ logoUrl: 'https://example.com/new-logo.png' }), {
status: 200, status: 200,
}), }),
); );
@@ -120,7 +120,7 @@ describe('AdminBrandManager', () => {
// Check if the UI updates with the new logo // Check if the UI updates with the new logo
expect(screen.getByAltText('No Frills logo')).toHaveAttribute( expect(screen.getByAltText('No Frills logo')).toHaveAttribute(
'src', 'src',
'http://example.com/new-logo.png', 'https://example.com/new-logo.png',
); );
console.log('TEST SUCCESS: All assertions for successful upload passed.'); console.log('TEST SUCCESS: All assertions for successful upload passed.');
}); });
@@ -350,7 +350,7 @@ describe('AdminBrandManager', () => {
// Brand 2 should still have original logo // Brand 2 should still have original logo
expect(screen.getByAltText('Compliments logo')).toHaveAttribute( expect(screen.getByAltText('Compliments logo')).toHaveAttribute(
'src', 'src',
'http://example.com/compliments.png', 'https://example.com/compliments.png',
); );
}); });
}); });

View File

@@ -35,7 +35,7 @@ const authenticatedUser = createMockUser({ user_id: 'auth-user-123', email: 'tes
const mockAddressId = 123; const mockAddressId = 123;
const authenticatedProfile = createMockUserProfile({ const authenticatedProfile = createMockUserProfile({
full_name: 'Test User', full_name: 'Test User',
avatar_url: 'http://example.com/avatar.png', avatar_url: 'https://example.com/avatar.png',
role: 'user', role: 'user',
points: 100, points: 100,
preferences: { preferences: {

View File

@@ -40,7 +40,7 @@ describe('Personalization Routes (/api/personalization)', () => {
const mockItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })]; const mockItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })];
vi.mocked(db.personalizationRepo.getAllMasterItems).mockResolvedValue(mockItems); vi.mocked(db.personalizationRepo.getAllMasterItems).mockResolvedValue(mockItems);
const response = await supertest(app).get('/api/personalization/master-items'); const response = await supertest(app).get('/api/personalization/master-items').set('x-test-rate-limit-enable', 'true');
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual(mockItems); expect(response.body).toEqual(mockItems);
@@ -49,7 +49,7 @@ describe('Personalization Routes (/api/personalization)', () => {
it('should return 500 if the database call fails', async () => { it('should return 500 if the database call fails', async () => {
const dbError = new Error('DB Error'); const dbError = new Error('DB Error');
vi.mocked(db.personalizationRepo.getAllMasterItems).mockRejectedValue(dbError); vi.mocked(db.personalizationRepo.getAllMasterItems).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/personalization/master-items'); const response = await supertest(app).get('/api/personalization/master-items').set('x-test-rate-limit-enable', 'true');
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error'); expect(response.body.message).toBe('DB Error');
expect(mockLogger.error).toHaveBeenCalledWith( expect(mockLogger.error).toHaveBeenCalledWith(

View File

@@ -1030,7 +1030,7 @@ describe('User Routes (/api/users)', () => {
it('should upload an avatar and update the user profile', async () => { it('should upload an avatar and update the user profile', async () => {
const mockUpdatedProfile = createMockUserProfile({ const mockUpdatedProfile = createMockUserProfile({
...mockUserProfile, ...mockUserProfile,
avatar_url: 'http://localhost:3001/uploads/avatars/new-avatar.png', avatar_url: 'https://example.com/uploads/avatars/new-avatar.png',
}); });
vi.mocked(userService.updateUserAvatar).mockResolvedValue(mockUpdatedProfile); vi.mocked(userService.updateUserAvatar).mockResolvedValue(mockUpdatedProfile);
@@ -1042,7 +1042,7 @@ describe('User Routes (/api/users)', () => {
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath); .attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.avatar_url).toContain('http://localhost:3001/uploads/avatars/'); expect(response.body.avatar_url).toContain('https://example.com/uploads/avatars/');
expect(userService.updateUserAvatar).toHaveBeenCalledWith( expect(userService.updateUserAvatar).toHaveBeenCalledWith(
mockUserProfile.user.user_id, mockUserProfile.user.user_id,
expect.any(Object), expect.any(Object),
@@ -1299,32 +1299,32 @@ describe('User Routes (/api/users)', () => {
expect(response.headers).toHaveProperty('ratelimit-limit'); expect(response.headers).toHaveProperty('ratelimit-limit');
expect(parseInt(response.headers['ratelimit-limit'])).toBe(20); expect(parseInt(response.headers['ratelimit-limit'])).toBe(20);
}); });
});
it('should apply userSensitiveUpdateLimiter to DELETE /account and block after limit', async () => { it('should apply userSensitiveUpdateLimiter to DELETE /account and block after limit', async () => {
// Explicitly advance time to ensure the rate limiter window has reset from previous tests // Explicitly advance time to ensure the rate limiter window has reset from previous tests
vi.advanceTimersByTime(60 * 60 * 1000 + 5000); vi.advanceTimersByTime(60 * 60 * 1000 + 5000);
const limit = 5; const limit = 5;
vi.mocked(userService.deleteUserAccount).mockResolvedValue(undefined); vi.mocked(userService.deleteUserAccount).mockResolvedValue(undefined);
// Consume the limit // Consume the limit
for (let i = 0; i < limit; i++) { for (let i = 0; i < limit; i++) {
const response = await supertest(app)
.delete('/api/users/account')
.set('X-Test-Rate-Limit-Enable', 'true')
.send({ password: 'correct-password' });
expect(response.status).toBe(200);
}
// Next request should be blocked
const response = await supertest(app) const response = await supertest(app)
.delete('/api/users/account') .delete('/api/users/account')
.set('X-Test-Rate-Limit-Enable', 'true') .set('X-Test-Rate-Limit-Enable', 'true')
.send({ password: 'correct-password' }); .send({ password: 'correct-password' });
expect(response.status).toBe(200);
}
// Next request should be blocked expect(response.status).toBe(429);
const response = await supertest(app) expect(response.text).toContain('Too many sensitive requests');
.delete('/api/users/account') });
.set('X-Test-Rate-Limit-Enable', 'true')
.send({ password: 'correct-password' });
expect(response.status).toBe(429);
expect(response.text).toContain('Too many sensitive requests');
}); });
}); });
}); });

View File

@@ -116,7 +116,7 @@ interface MockFlyer {
updated_at: string; updated_at: string;
} }
const baseUrl = 'http://localhost:3001'; const baseUrl = 'https://example.com';
describe('AI Service (Server)', () => { describe('AI Service (Server)', () => {
// Create mock dependencies that will be injected into the service // Create mock dependencies that will be injected into the service
@@ -1015,7 +1015,7 @@ describe('AI Service (Server)', () => {
userId: 'user123', userId: 'user123',
submitterIp: '127.0.0.1', submitterIp: '127.0.0.1',
userProfileAddress: '123 St, City, Country', // Partial address match based on filter(Boolean) userProfileAddress: '123 St, City, Country', // Partial address match based on filter(Boolean)
baseUrl: 'http://localhost:3000', baseUrl: 'https://example.com',
}); });
expect(result.id).toBe('job123'); expect(result.id).toBe('job123');
}); });
@@ -1037,7 +1037,7 @@ describe('AI Service (Server)', () => {
expect.objectContaining({ expect.objectContaining({
userId: undefined, userId: undefined,
userProfileAddress: undefined, userProfileAddress: undefined,
baseUrl: 'http://localhost:3000', baseUrl: 'https://example.com',
}), }),
); );
}); });

View File

@@ -59,7 +59,7 @@ describe('AuthService', () => {
// Set environment variables before any modules are imported // Set environment variables before any modules are imported
vi.stubEnv('JWT_SECRET', 'test-secret'); vi.stubEnv('JWT_SECRET', 'test-secret');
vi.stubEnv('FRONTEND_URL', 'http://localhost:3000'); vi.stubEnv('FRONTEND_URL', 'https://example.com');
// Mock all dependencies before dynamically importing the service // Mock all dependencies before dynamically importing the service
// Core modules like bcrypt, jsonwebtoken, and crypto are now mocked globally in tests-setup-unit.ts // Core modules like bcrypt, jsonwebtoken, and crypto are now mocked globally in tests-setup-unit.ts

View File

@@ -132,8 +132,8 @@ describe('Flyer DB Service', () => {
it('should execute an INSERT query and return the new flyer', async () => { it('should execute an INSERT query and return the new flyer', async () => {
const flyerData: FlyerDbInsert = { const flyerData: FlyerDbInsert = {
file_name: 'test.jpg', file_name: 'test.jpg',
image_url: 'http://localhost:3001/images/test.jpg', image_url: 'https://example.com/images/test.jpg',
icon_url: 'http://localhost:3001/images/icons/test.jpg', icon_url: 'https://example.com/images/icons/test.jpg',
checksum: 'checksum123', checksum: 'checksum123',
store_id: 1, store_id: 1,
valid_from: '2024-01-01', valid_from: '2024-01-01',
@@ -155,8 +155,8 @@ describe('Flyer DB Service', () => {
expect.stringContaining('INSERT INTO flyers'), expect.stringContaining('INSERT INTO flyers'),
[ [
'test.jpg', 'test.jpg',
'http://localhost:3001/images/test.jpg', 'https://example.com/images/test.jpg',
'http://localhost:3001/images/icons/test.jpg', 'https://example.com/images/icons/test.jpg',
'checksum123', 'checksum123',
1, 1,
'2024-01-01', '2024-01-01',

View File

@@ -63,6 +63,7 @@ export class FlyerRepository {
* @returns The newly created flyer record with its ID. * @returns The newly created flyer record with its ID.
*/ */
async insertFlyer(flyerData: FlyerDbInsert, logger: Logger): Promise<Flyer> { async insertFlyer(flyerData: FlyerDbInsert, logger: Logger): Promise<Flyer> {
console.log('[DEBUG] FlyerRepository.insertFlyer called with:', JSON.stringify(flyerData, null, 2));
try { try {
const query = ` const query = `
INSERT INTO flyers ( INSERT INTO flyers (

View File

@@ -596,7 +596,7 @@ describe('Shopping DB Service', () => {
const mockReceipt = { const mockReceipt = {
receipt_id: 1, receipt_id: 1,
user_id: 'user-1', user_id: 'user-1',
receipt_image_url: 'http://example.com/receipt.jpg', receipt_image_url: 'https://example.com/receipt.jpg',
status: 'pending', status: 'pending',
}; };
mockPoolInstance.query.mockResolvedValue({ rows: [mockReceipt] }); mockPoolInstance.query.mockResolvedValue({ rows: [mockReceipt] });

View File

@@ -21,7 +21,7 @@ const createMockJobData = (data: Partial<FlyerJobData>): FlyerJobData => ({
filePath: '/tmp/flyer.jpg', filePath: '/tmp/flyer.jpg',
originalFileName: 'flyer.jpg', originalFileName: 'flyer.jpg',
checksum: 'checksum-123', checksum: 'checksum-123',
baseUrl: 'http://localhost:3000', baseUrl: 'https://example.com',
...data, ...data,
}); });

View File

@@ -28,13 +28,13 @@ describe('FlyerDataTransformer', () => {
transformer = new FlyerDataTransformer(); transformer = new FlyerDataTransformer();
// Stub environment variables to ensure consistency and predictability. // Stub environment variables to ensure consistency and predictability.
// Prioritize FRONTEND_URL to match the updated service logic. // Prioritize FRONTEND_URL to match the updated service logic.
vi.stubEnv('FRONTEND_URL', 'http://localhost:3000'); vi.stubEnv('FRONTEND_URL', 'https://example.com');
vi.stubEnv('BASE_URL', ''); // Ensure this is not used to confirm priority logic vi.stubEnv('BASE_URL', ''); // Ensure this is not used to confirm priority logic
vi.stubEnv('PORT', ''); // Ensure this is not used vi.stubEnv('PORT', ''); // Ensure this is not used
// Provide a default mock implementation for generateFlyerIcon // Provide a default mock implementation for generateFlyerIcon
vi.mocked(generateFlyerIcon).mockResolvedValue('icon-flyer-page-1.webp'); vi.mocked(generateFlyerIcon).mockResolvedValue('icon-flyer-page-1.webp');
vi.mocked(getBaseUrl).mockReturnValue('http://localhost:3000'); vi.mocked(getBaseUrl).mockReturnValue('https://example.com');
}); });
it('should transform AI data into database-ready format with a user ID', async () => { it('should transform AI data into database-ready format with a user ID', async () => {
@@ -69,7 +69,7 @@ describe('FlyerDataTransformer', () => {
const originalFileName = 'my-flyer.pdf'; const originalFileName = 'my-flyer.pdf';
const checksum = 'checksum-abc-123'; const checksum = 'checksum-abc-123';
const userId = 'user-xyz-456'; const userId = 'user-xyz-456';
const baseUrl = 'http://test.host'; const baseUrl = 'https://example.com';
// Act // Act
const { flyerData, itemsForDb } = await transformer.transform( const { flyerData, itemsForDb } = await transformer.transform(
@@ -262,7 +262,7 @@ describe('FlyerDataTransformer', () => {
}, },
needsReview: false, needsReview: false,
}; };
const baseUrl = undefined; // Explicitly pass undefined for this test const baseUrl = ''; // Explicitly pass '' for this test
const expectedFallbackUrl = 'http://fallback-url.com'; const expectedFallbackUrl = 'http://fallback-url.com';
vi.mocked(getBaseUrl).mockReturnValue(expectedFallbackUrl); vi.mocked(getBaseUrl).mockReturnValue(expectedFallbackUrl);

View File

@@ -59,13 +59,16 @@ export class FlyerDataTransformer {
private _buildUrls( private _buildUrls(
imageFileName: string, imageFileName: string,
iconFileName: string, iconFileName: string,
baseUrl: string | undefined, baseUrl: string,
logger: Logger, logger: Logger,
): { imageUrl: string; iconUrl: string } { ): { imageUrl: string; iconUrl: string } {
console.log('[DEBUG] FlyerDataTransformer._buildUrls inputs:', { imageFileName, iconFileName, baseUrl });
logger.debug({ imageFileName, iconFileName, baseUrl }, 'Building URLs'); logger.debug({ imageFileName, iconFileName, baseUrl }, 'Building URLs');
const finalBaseUrl = baseUrl || getBaseUrl(logger); const finalBaseUrl = baseUrl || getBaseUrl(logger);
console.log('[DEBUG] FlyerDataTransformer._buildUrls finalBaseUrl resolved to:', finalBaseUrl);
const imageUrl = `${finalBaseUrl}/flyer-images/${imageFileName}`; const imageUrl = `${finalBaseUrl}/flyer-images/${imageFileName}`;
const iconUrl = `${finalBaseUrl}/flyer-images/icons/${iconFileName}`; const iconUrl = `${finalBaseUrl}/flyer-images/icons/${iconFileName}`;
console.log('[DEBUG] FlyerDataTransformer._buildUrls constructed:', { imageUrl, iconUrl });
logger.debug({ imageUrl, iconUrl }, 'Constructed URLs'); logger.debug({ imageUrl, iconUrl }, 'Constructed URLs');
return { imageUrl, iconUrl }; return { imageUrl, iconUrl };
} }
@@ -88,8 +91,9 @@ export class FlyerDataTransformer {
checksum: string, checksum: string,
userId: string | undefined, userId: string | undefined,
logger: Logger, logger: Logger,
baseUrl?: string, baseUrl: string,
): Promise<{ flyerData: FlyerInsert; itemsForDb: FlyerItemInsert[] }> { ): Promise<{ flyerData: FlyerInsert; itemsForDb: FlyerItemInsert[] }> {
console.log('[DEBUG] FlyerDataTransformer.transform called with baseUrl:', baseUrl);
logger.info('Starting data transformation from AI output to database format.'); logger.info('Starting data transformation from AI output to database format.');
try { try {

View File

@@ -104,8 +104,8 @@ describe('FlyerProcessingService', () => {
vi.spyOn(FlyerDataTransformer.prototype, 'transform').mockResolvedValue({ vi.spyOn(FlyerDataTransformer.prototype, 'transform').mockResolvedValue({
flyerData: { flyerData: {
file_name: 'test.jpg', file_name: 'test.jpg',
image_url: 'http://example.com/test.jpg', image_url: 'https://example.com/test.jpg',
icon_url: 'http://example.com/icon.webp', icon_url: 'https://example.com/icon.webp',
store_name: 'Mock Store', store_name: 'Mock Store',
// Add required fields for FlyerInsert type // Add required fields for FlyerInsert type
status: 'processed', status: 'processed',
@@ -169,7 +169,7 @@ describe('FlyerProcessingService', () => {
flyer: createMockFlyer({ flyer: createMockFlyer({
flyer_id: 1, flyer_id: 1,
file_name: 'test.jpg', file_name: 'test.jpg',
image_url: 'http://example.com/test.jpg', image_url: 'https://example.com/test.jpg',
item_count: 1, item_count: 1,
}), }),
items: [], items: [],
@@ -189,7 +189,7 @@ describe('FlyerProcessingService', () => {
filePath: '/tmp/flyer.jpg', filePath: '/tmp/flyer.jpg',
originalFileName: 'flyer.jpg', originalFileName: 'flyer.jpg',
checksum: 'checksum-123', checksum: 'checksum-123',
baseUrl: 'http://localhost:3000', baseUrl: 'https://example.com',
...data, ...data,
}, },
updateProgress: vi.fn(), updateProgress: vi.fn(),
@@ -241,7 +241,7 @@ describe('FlyerProcessingService', () => {
'checksum-123', // checksum 'checksum-123', // checksum
undefined, // userId undefined, // userId
expect.any(Object), // logger expect.any(Object), // logger
'http://localhost:3000', // baseUrl 'https://example.com', // baseUrl
); );
// 5. DB transaction was initiated // 5. DB transaction was initiated
@@ -695,8 +695,8 @@ describe('FlyerProcessingService', () => {
it('should derive paths from DB and delete files if job paths are empty', async () => { it('should derive paths from DB and delete files if job paths are empty', async () => {
const job = createMockCleanupJob({ flyerId: 1, paths: [] }); // Empty paths const job = createMockCleanupJob({ flyerId: 1, paths: [] }); // Empty paths
const mockFlyer = createMockFlyer({ const mockFlyer = createMockFlyer({
image_url: 'http://localhost:3000/flyer-images/flyer-abc.jpg', image_url: 'https://example.com/flyer-images/flyer-abc.jpg',
icon_url: 'http://localhost:3000/flyer-images/icons/icon-flyer-abc.webp', icon_url: 'https://example.com/flyer-images/icons/icon-flyer-abc.webp',
}); });
// Mock DB call to return a flyer // Mock DB call to return a flyer
vi.mocked(mockedDb.flyerRepo.getFlyerById).mockResolvedValue(mockFlyer); vi.mocked(mockedDb.flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);

View File

@@ -103,6 +103,8 @@ export class FlyerProcessingService {
// The main processed image path is already in `allFilePaths` via `createdImagePaths`. // The main processed image path is already in `allFilePaths` via `createdImagePaths`.
allFilePaths.push(path.join(iconsDir, iconFileName)); allFilePaths.push(path.join(iconsDir, iconFileName));
console.log('[DEBUG] FlyerProcessingService calling transformer with:', { originalFileName: job.data.originalFileName, imageFileName, iconFileName, checksum: job.data.checksum, baseUrl: job.data.baseUrl });
const { flyerData, itemsForDb } = await this.transformer.transform( const { flyerData, itemsForDb } = await this.transformer.transform(
aiResult, aiResult,
job.data.originalFileName, job.data.originalFileName,

View File

@@ -241,7 +241,7 @@ describe('UserService', () => {
describe('updateUserAvatar', () => { describe('updateUserAvatar', () => {
it('should construct avatar URL and update profile', async () => { it('should construct avatar URL and update profile', async () => {
const { logger } = await import('./logger.server'); const { logger } = await import('./logger.server');
const testBaseUrl = getTestBaseUrl(3001); const testBaseUrl = getTestBaseUrl();
vi.stubEnv('FRONTEND_URL', testBaseUrl); vi.stubEnv('FRONTEND_URL', testBaseUrl);
const userId = 'user-123'; const userId = 'user-123';

View File

@@ -1,7 +1,6 @@
// src/tests/integration/admin.integration.test.ts // src/tests/integration/admin.integration.test.ts
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import { getPool } from '../../services/db/connection.db'; import { getPool } from '../../services/db/connection.db';
import type { UserProfile } from '../../types'; import type { UserProfile } from '../../types';
import { createAndLoginUser, TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers'; import { createAndLoginUser, TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
@@ -10,9 +9,9 @@ import { cleanupDb } from '../utils/cleanup';
/** /**
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('Admin API Routes Integration Tests', () => { describe('Admin API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let adminToken: string; let adminToken: string;
let adminUser: UserProfile; let adminUser: UserProfile;
let regularUser: UserProfile; let regularUser: UserProfile;
@@ -21,6 +20,10 @@ describe('Admin API Routes Integration Tests', () => {
const createdStoreIds: number[] = []; const createdStoreIds: number[] = [];
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Create a fresh admin user and a regular user for this test suite // Create a fresh admin user and a regular user for this test suite
// Using unique emails to prevent test pollution from other integration test files. // Using unique emails to prevent test pollution from other integration test files.
({ user: adminUser, token: adminToken } = await createAndLoginUser({ ({ user: adminUser, token: adminToken } = await createAndLoginUser({
@@ -40,6 +43,7 @@ describe('Admin API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
await cleanupDb({ await cleanupDb({
userIds: createdUserIds, userIds: createdUserIds,
storeIds: createdStoreIds, storeIds: createdStoreIds,

View File

@@ -1,7 +1,6 @@
// src/tests/integration/ai.integration.test.ts // src/tests/integration/ai.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'path'; import path from 'path';
import { createAndLoginUser } from '../utils/testHelpers'; import { createAndLoginUser } from '../utils/testHelpers';
@@ -12,8 +11,6 @@ import { cleanupFiles } from '../utils/cleanupFiles';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
interface TestGeolocationCoordinates { interface TestGeolocationCoordinates {
latitude: number; latitude: number;
longitude: number; longitude: number;
@@ -26,10 +23,15 @@ interface TestGeolocationCoordinates {
} }
describe('AI API Routes Integration Tests', () => { describe('AI API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let authToken: string; let authToken: string;
let testUserId: string; let testUserId: string;
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Create and log in as a new user for authenticated tests. // Create and log in as a new user for authenticated tests.
const { token, user } = await createAndLoginUser({ fullName: 'AI Tester', request }); const { token, user } = await createAndLoginUser({ fullName: 'AI Tester', request });
authToken = token; authToken = token;
@@ -37,6 +39,7 @@ describe('AI API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
// 1. Clean up database records // 1. Clean up database records
await cleanupDb({ userIds: [testUserId] }); await cleanupDb({ userIds: [testUserId] });

View File

@@ -1,7 +1,6 @@
// src/tests/integration/auth.integration.test.ts // src/tests/integration/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers'; import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup'; import { cleanupDb } from '../utils/cleanup';
import type { UserProfile } from '../../types'; import type { UserProfile } from '../../types';
@@ -10,8 +9,6 @@ import type { UserProfile } from '../../types';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
/** /**
* These are integration tests that verify the authentication flow against a running backend server. * These are integration tests that verify the authentication flow against a running backend server.
* Make sure your Express server is running before executing these tests. * Make sure your Express server is running before executing these tests.
@@ -19,11 +16,16 @@ const request = supertest(app);
* To run only these tests: `vitest run src/tests/auth.integration.test.ts` * To run only these tests: `vitest run src/tests/auth.integration.test.ts`
*/ */
describe('Authentication API Integration', () => { describe('Authentication API Integration', () => {
let request: ReturnType<typeof supertest>;
let testUserEmail: string; let testUserEmail: string;
let testUser: UserProfile; let testUser: UserProfile;
const createdUserIds: string[] = []; const createdUserIds: string[] = [];
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Use a unique email for this test suite to prevent collisions with other tests. // Use a unique email for this test suite to prevent collisions with other tests.
const email = `auth-integration-test-${Date.now()}@example.com`; const email = `auth-integration-test-${Date.now()}@example.com`;
({ user: testUser } = await createAndLoginUser({ email, fullName: 'Auth Test User', request })); ({ user: testUser } = await createAndLoginUser({ email, fullName: 'Auth Test User', request }));
@@ -32,6 +34,7 @@ describe('Authentication API Integration', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
await cleanupDb({ userIds: createdUserIds }); await cleanupDb({ userIds: createdUserIds });
}); });

View File

@@ -1,7 +1,6 @@
// src/tests/integration/budget.integration.test.ts // src/tests/integration/budget.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import { createAndLoginUser } from '../utils/testHelpers'; import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup'; import { cleanupDb } from '../utils/cleanup';
import type { UserProfile, Budget } from '../../types'; import type { UserProfile, Budget } from '../../types';
@@ -11,9 +10,8 @@ import { getPool } from '../../services/db/connection.db';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('Budget API Routes Integration Tests', () => { describe('Budget API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let testUser: UserProfile; let testUser: UserProfile;
let authToken: string; let authToken: string;
let testBudget: Budget; let testBudget: Budget;
@@ -21,6 +19,10 @@ describe('Budget API Routes Integration Tests', () => {
const createdBudgetIds: number[] = []; const createdBudgetIds: number[] = [];
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// 1. Create a user for the tests // 1. Create a user for the tests
const { user, token } = await createAndLoginUser({ const { user, token } = await createAndLoginUser({
email: `budget-user-${Date.now()}@example.com`, email: `budget-user-${Date.now()}@example.com`,
@@ -50,6 +52,7 @@ describe('Budget API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
// Clean up all created resources // Clean up all created resources
await cleanupDb({ await cleanupDb({
userIds: createdUserIds, userIds: createdUserIds,

View File

@@ -1,7 +1,6 @@
// src/tests/integration/flyer-processing.integration.test.ts // src/tests/integration/flyer-processing.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'path'; import path from 'path';
import * as db from '../../services/db/index.db'; import * as db from '../../services/db/index.db';
@@ -22,8 +21,6 @@ import sharp from 'sharp';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
const { mockExtractCoreData } = vi.hoisted(() => ({ const { mockExtractCoreData } = vi.hoisted(() => ({
mockExtractCoreData: vi.fn(), mockExtractCoreData: vi.fn(),
})); }));
@@ -50,6 +47,7 @@ vi.mock('../../services/db/index.db', async (importOriginal) => {
}); });
describe('Flyer Processing Background Job Integration Test', () => { describe('Flyer Processing Background Job Integration Test', () => {
let request: ReturnType<typeof supertest>;
const createdUserIds: string[] = []; const createdUserIds: string[] = [];
const createdFlyerIds: number[] = []; const createdFlyerIds: number[] = [];
const createdFilePaths: string[] = []; const createdFilePaths: string[] = [];
@@ -57,12 +55,19 @@ describe('Flyer Processing Background Job Integration Test', () => {
beforeAll(async () => { beforeAll(async () => {
// FIX: Stub FRONTEND_URL to ensure valid absolute URLs (http://...) are generated // FIX: Stub FRONTEND_URL to ensure valid absolute URLs (http://...) are generated
// for the database, satisfying the 'url_check' constraint. // for the database, satisfying the 'url_check' constraint.
vi.stubEnv('FRONTEND_URL', 'http://localhost:3000'); // IMPORTANT: This must run BEFORE the app is imported so workers inherit the env var.
vi.stubEnv('FRONTEND_URL', 'https://example.com');
console.log('[TEST SETUP] FRONTEND_URL stubbed to:', process.env.FRONTEND_URL);
const appModule = await import('../../../server');
const app = appModule.default;
request = supertest(app);
}); });
// FIX: Reset mocks before each test to ensure isolation. // FIX: Reset mocks before each test to ensure isolation.
// This prevents "happy path" mocks from leaking into error handling tests and vice versa. // This prevents "happy path" mocks from leaking into error handling tests and vice versa.
beforeEach(async () => { beforeEach(async () => {
console.log('[TEST SETUP] Resetting mocks before test execution');
// 1. Reset AI Service Mock to default success state // 1. Reset AI Service Mock to default success state
mockExtractCoreData.mockReset(); mockExtractCoreData.mockReset();
mockExtractCoreData.mockResolvedValue({ mockExtractCoreData.mockResolvedValue({
@@ -107,6 +112,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
* It uploads a file, polls for completion, and verifies the result in the database. * It uploads a file, polls for completion, and verifies the result in the database.
*/ */
const runBackgroundProcessingTest = async (user?: UserProfile, token?: string) => { const runBackgroundProcessingTest = async (user?: UserProfile, token?: string) => {
console.log(`[TEST START] runBackgroundProcessingTest. User: ${user?.user.email ?? 'ANONYMOUS'}`);
// Arrange: Load a mock flyer PDF. // Arrange: Load a mock flyer PDF.
const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg'); const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
const imageBuffer = await fs.readFile(imagePath); const imageBuffer = await fs.readFile(imagePath);
@@ -116,6 +122,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
const uniqueFileName = `test-flyer-image-${Date.now()}.jpg`; const uniqueFileName = `test-flyer-image-${Date.now()}.jpg`;
const mockImageFile = new File([new Uint8Array(uniqueContent)], uniqueFileName, { type: 'image/jpeg' }); const mockImageFile = new File([new Uint8Array(uniqueContent)], uniqueFileName, { type: 'image/jpeg' });
const checksum = await generateFileChecksum(mockImageFile); const checksum = await generateFileChecksum(mockImageFile);
console.log('[TEST DATA] Generated checksum for test:', checksum);
// Track created files for cleanup // Track created files for cleanup
const uploadDir = path.resolve(__dirname, '../../../flyer-images'); const uploadDir = path.resolve(__dirname, '../../../flyer-images');
@@ -125,17 +132,22 @@ describe('Flyer Processing Background Job Integration Test', () => {
createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName)); createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));
// Act 1: Upload the file to start the background job. // Act 1: Upload the file to start the background job.
const testBaseUrl = getTestBaseUrl();
console.log('[TEST ACTION] Uploading file with baseUrl:', testBaseUrl);
const uploadReq = request const uploadReq = request
.post('/api/ai/upload-and-process') .post('/api/ai/upload-and-process')
.field('checksum', checksum) .field('checksum', checksum)
// Pass the baseUrl directly in the form data to ensure the worker receives it, // Pass the baseUrl directly in the form data to ensure the worker receives it,
// bypassing issues with vi.stubEnv in multi-threaded test environments. // bypassing issues with vi.stubEnv in multi-threaded test environments.
.field('baseUrl', getTestBaseUrl()) .field('baseUrl', testBaseUrl)
.attach('flyerFile', uniqueContent, uniqueFileName); .attach('flyerFile', uniqueContent, uniqueFileName);
if (token) { if (token) {
uploadReq.set('Authorization', `Bearer ${token}`); uploadReq.set('Authorization', `Bearer ${token}`);
} }
const uploadResponse = await uploadReq; const uploadResponse = await uploadReq;
console.log('[TEST RESPONSE] Upload status:', uploadResponse.status);
console.log('[TEST RESPONSE] Upload body:', JSON.stringify(uploadResponse.body));
const { jobId } = uploadResponse.body; const { jobId } = uploadResponse.body;
// Assert 1: Check that a job ID was returned. // Assert 1: Check that a job ID was returned.
@@ -149,6 +161,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
statusReq.set('Authorization', `Bearer ${token}`); statusReq.set('Authorization', `Bearer ${token}`);
} }
const statusResponse = await statusReq; const statusResponse = await statusReq;
console.log(`[TEST POLL] Job ${jobId} current state:`, statusResponse.body?.state);
return statusResponse.body; return statusResponse.body;
}, },
(status) => status.state === 'completed' || status.state === 'failed', (status) => status.state === 'completed' || status.state === 'failed',

View File

@@ -1,8 +1,7 @@
// src/tests/integration/flyer.integration.test.ts // src/tests/integration/flyer.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import { getPool } from '../../services/db/connection.db'; import { getPool } from '../../services/db/connection.db';
import app from '../../../server';
import type { Flyer, FlyerItem } from '../../types'; import type { Flyer, FlyerItem } from '../../types';
import { cleanupDb } from '../utils/cleanup'; import { cleanupDb } from '../utils/cleanup';
import { TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers'; import { TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
@@ -14,12 +13,16 @@ import { TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
describe('Public Flyer API Routes Integration Tests', () => { describe('Public Flyer API Routes Integration Tests', () => {
let flyers: Flyer[] = []; let flyers: Flyer[] = [];
// Use a supertest instance for all requests in this file // Use a supertest instance for all requests in this file
const request = supertest(app); let request: ReturnType<typeof supertest>;
let testStoreId: number; let testStoreId: number;
let createdFlyerId: number; let createdFlyerId: number;
// Fetch flyers once before all tests in this suite to use in subsequent tests. // Fetch flyers once before all tests in this suite to use in subsequent tests.
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Ensure at least one flyer exists // Ensure at least one flyer exists
const storeRes = await getPool().query( const storeRes = await getPool().query(
`INSERT INTO public.stores (name) VALUES ('Integration Test Store') RETURNING store_id`, `INSERT INTO public.stores (name) VALUES ('Integration Test Store') RETURNING store_id`,
@@ -45,6 +48,7 @@ describe('Public Flyer API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
// Clean up the test data created in beforeAll to prevent polluting the test database. // Clean up the test data created in beforeAll to prevent polluting the test database.
await cleanupDb({ await cleanupDb({
flyerIds: [createdFlyerId], flyerIds: [createdFlyerId],

View File

@@ -1,7 +1,6 @@
// src/tests/integration/gamification.integration.test.ts // src/tests/integration/gamification.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import path from 'path'; import path from 'path';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { getPool } from '../../services/db/connection.db'; import { getPool } from '../../services/db/connection.db';
@@ -26,8 +25,6 @@ import { cleanupFiles } from '../utils/cleanupFiles';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
const { mockExtractCoreData } = vi.hoisted(() => ({ const { mockExtractCoreData } = vi.hoisted(() => ({
mockExtractCoreData: vi.fn(), mockExtractCoreData: vi.fn(),
})); }));
@@ -53,6 +50,7 @@ vi.mock('../../utils/imageProcessor', async () => {
}); });
describe('Gamification Flow Integration Test', () => { describe('Gamification Flow Integration Test', () => {
let request: ReturnType<typeof supertest>;
let testUser: UserProfile; let testUser: UserProfile;
let authToken: string; let authToken: string;
const createdFlyerIds: number[] = []; const createdFlyerIds: number[] = [];
@@ -60,6 +58,12 @@ describe('Gamification Flow Integration Test', () => {
const createdStoreIds: number[] = []; const createdStoreIds: number[] = [];
beforeAll(async () => { beforeAll(async () => {
// Stub environment variables for URL generation in the background worker.
// This needs to be in beforeAll to ensure it's set before any code that might use it is imported.
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Create a new user specifically for this test suite to ensure a clean slate. // Create a new user specifically for this test suite to ensure a clean slate.
({ user: testUser, token: authToken } = await createAndLoginUser({ ({ user: testUser, token: authToken } = await createAndLoginUser({
email: `gamification-user-${Date.now()}@example.com`, email: `gamification-user-${Date.now()}@example.com`,
@@ -67,10 +71,6 @@ describe('Gamification Flow Integration Test', () => {
request, request,
})); }));
// Stub environment variables for URL generation in the background worker.
// This needs to be in beforeAll to ensure it's set before any code that might use it is imported.
vi.stubEnv('FRONTEND_URL', 'http://localhost:3001');
// Setup default mock response for the AI service's extractCoreDataFromFlyerImage method. // Setup default mock response for the AI service's extractCoreDataFromFlyerImage method.
mockExtractCoreData.mockResolvedValue({ mockExtractCoreData.mockResolvedValue({
store_name: 'Gamification Test Store', store_name: 'Gamification Test Store',
@@ -90,6 +90,7 @@ describe('Gamification Flow Integration Test', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
await cleanupDb({ await cleanupDb({
userIds: testUser ? [testUser.user.user_id] : [], userIds: testUser ? [testUser.user.user_id] : [],
flyerIds: createdFlyerIds, flyerIds: createdFlyerIds,
@@ -253,7 +254,7 @@ describe('Gamification Flow Integration Test', () => {
// 8. Assert that the URLs are fully qualified. // 8. Assert that the URLs are fully qualified.
expect(savedFlyer.image_url).to.equal(newFlyer.image_url); expect(savedFlyer.image_url).to.equal(newFlyer.image_url);
expect(savedFlyer.icon_url).to.equal(newFlyer.icon_url); expect(savedFlyer.icon_url).to.equal(newFlyer.icon_url);
const expectedBaseUrl = getTestBaseUrl(3001); const expectedBaseUrl = getTestBaseUrl();
expect(newFlyer.image_url).toContain(`${expectedBaseUrl}/flyer-images/`); expect(newFlyer.image_url).toContain(`${expectedBaseUrl}/flyer-images/`);
}); });
}); });

View File

@@ -1,7 +1,6 @@
// src/tests/integration/notification.integration.test.ts // src/tests/integration/notification.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import { createAndLoginUser } from '../utils/testHelpers'; import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup'; import { cleanupDb } from '../utils/cleanup';
import type { UserProfile, Notification } from '../../types'; import type { UserProfile, Notification } from '../../types';
@@ -11,14 +10,17 @@ import { getPool } from '../../services/db/connection.db';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('Notification API Routes Integration Tests', () => { describe('Notification API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let testUser: UserProfile; let testUser: UserProfile;
let authToken: string; let authToken: string;
const createdUserIds: string[] = []; const createdUserIds: string[] = [];
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// 1. Create a user for the tests // 1. Create a user for the tests
const { user, token } = await createAndLoginUser({ const { user, token } = await createAndLoginUser({
email: `notification-user-${Date.now()}@example.com`, email: `notification-user-${Date.now()}@example.com`,
@@ -46,6 +48,7 @@ describe('Notification API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
// Notifications are deleted via CASCADE when the user is deleted. // Notifications are deleted via CASCADE when the user is deleted.
await cleanupDb({ await cleanupDb({
userIds: createdUserIds, userIds: createdUserIds,

View File

@@ -1,7 +1,6 @@
// src/tests/integration/price.integration.test.ts // src/tests/integration/price.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import { getPool } from '../../services/db/connection.db'; import { getPool } from '../../services/db/connection.db';
import { TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers'; import { TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
@@ -9,9 +8,8 @@ import { TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('Price History API Integration Test (/api/price-history)', () => { describe('Price History API Integration Test (/api/price-history)', () => {
let request: ReturnType<typeof supertest>;
let masterItemId: number; let masterItemId: number;
let storeId: number; let storeId: number;
let flyerId1: number; let flyerId1: number;
@@ -19,6 +17,10 @@ describe('Price History API Integration Test (/api/price-history)', () => {
let flyerId3: number; let flyerId3: number;
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
const pool = getPool(); const pool = getPool();
// 1. Create a master grocery item // 1. Create a master grocery item
@@ -71,6 +73,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
const pool = getPool(); const pool = getPool();
// The CASCADE on the tables should handle flyer_items. // The CASCADE on the tables should handle flyer_items.
// The delete on flyers cascades to flyer_items, which fires a trigger `recalculate_price_history_on_flyer_item_delete`. // The delete on flyers cascades to flyer_items, which fires a trigger `recalculate_price_history_on_flyer_item_delete`.
@@ -94,7 +97,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
}); });
it('should return the correct price history for a given master item ID', async () => { it('should return the correct price history for a given master item ID', async () => {
const response = await request.post('/api/price-history').send({ masterItemIds: [masterItemId] }); const response = await request.post('/api/price-history').set('Authorization', 'Bearer ${token}').send({ masterItemIds: [masterItemId] });
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toBeInstanceOf(Array); expect(response.body).toBeInstanceOf(Array);
@@ -108,6 +111,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
it('should respect the limit parameter', async () => { it('should respect the limit parameter', async () => {
const response = await request const response = await request
.post('/api/price-history') .post('/api/price-history')
.set('Authorization', 'Bearer ${token}')
.send({ masterItemIds: [masterItemId], limit: 2 }); .send({ masterItemIds: [masterItemId], limit: 2 });
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -119,6 +123,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
it('should respect the offset parameter', async () => { it('should respect the offset parameter', async () => {
const response = await request const response = await request
.post('/api/price-history') .post('/api/price-history')
.set('Authorization', 'Bearer ${token}')
.send({ masterItemIds: [masterItemId], limit: 2, offset: 1 }); .send({ masterItemIds: [masterItemId], limit: 2, offset: 1 });
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -128,7 +133,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
}); });
it('should return price history sorted by date in ascending order', async () => { it('should return price history sorted by date in ascending order', async () => {
const response = await request.post('/api/price-history').send({ masterItemIds: [masterItemId] }); const response = await request.post('/api/price-history').set('Authorization', 'Bearer ${token}').send({ masterItemIds: [masterItemId] });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const history = response.body; const history = response.body;
@@ -143,7 +148,7 @@ describe('Price History API Integration Test (/api/price-history)', () => {
}); });
it('should return an empty array for a master item ID with no price history', async () => { it('should return an empty array for a master item ID with no price history', async () => {
const response = await request.post('/api/price-history').send({ masterItemIds: [999999] }); const response = await request.post('/api/price-history').set('Authorization', 'Bearer ${token}').send({ masterItemIds: [999999] });
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual([]); expect(response.body).toEqual([]);
}); });

View File

@@ -1,7 +1,6 @@
// src/tests/integration/public.routes.integration.test.ts // src/tests/integration/public.routes.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import type { import type {
Flyer, Flyer,
FlyerItem, FlyerItem,
@@ -20,16 +19,19 @@ import { createAndLoginUser, TEST_EXAMPLE_DOMAIN } from '../utils/testHelpers';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('Public API Routes Integration Tests', () => { describe('Public API Routes Integration Tests', () => {
// Shared state for tests // Shared state for tests
let request: ReturnType<typeof supertest>;
let testUser: UserProfile; let testUser: UserProfile;
let testRecipe: Recipe; let testRecipe: Recipe;
let testFlyer: Flyer; let testFlyer: Flyer;
let testStoreId: number; let testStoreId: number;
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
const pool = getPool(); const pool = getPool();
// Create a user to own the recipe // Create a user to own the recipe
const userEmail = `public-routes-user-${Date.now()}@example.com`; const userEmail = `public-routes-user-${Date.now()}@example.com`;
@@ -77,6 +79,7 @@ describe('Public API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
await cleanupDb({ await cleanupDb({
userIds: testUser ? [testUser.user.user_id] : [], userIds: testUser ? [testUser.user.user_id] : [],
recipeIds: testRecipe ? [testRecipe.recipe_id] : [], recipeIds: testRecipe ? [testRecipe.recipe_id] : [],

View File

@@ -1,7 +1,6 @@
// src/tests/integration/recipe.integration.test.ts // src/tests/integration/recipe.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import { createAndLoginUser } from '../utils/testHelpers'; import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup'; import { cleanupDb } from '../utils/cleanup';
import type { UserProfile, Recipe, RecipeComment } from '../../types'; import type { UserProfile, Recipe, RecipeComment } from '../../types';
@@ -13,9 +12,8 @@ import { aiService } from '../../services/aiService.server';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('Recipe API Routes Integration Tests', () => { describe('Recipe API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let testUser: UserProfile; let testUser: UserProfile;
let authToken: string; let authToken: string;
let testRecipe: Recipe; let testRecipe: Recipe;
@@ -23,6 +21,10 @@ describe('Recipe API Routes Integration Tests', () => {
const createdRecipeIds: number[] = []; const createdRecipeIds: number[] = [];
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Create a user to own the recipe and perform authenticated actions // Create a user to own the recipe and perform authenticated actions
const { user, token } = await createAndLoginUser({ const { user, token } = await createAndLoginUser({
email: `recipe-user-${Date.now()}@example.com`, email: `recipe-user-${Date.now()}@example.com`,
@@ -48,6 +50,7 @@ describe('Recipe API Routes Integration Tests', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
// Clean up all created resources // Clean up all created resources
await cleanupDb({ await cleanupDb({
userIds: createdUserIds, userIds: createdUserIds,

View File

@@ -1,13 +1,23 @@
// src/tests/integration/server.integration.test.ts // src/tests/integration/server.integration.test.ts
import { describe, it, expect } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
/** /**
* @vitest-environment node * @vitest-environment node
*/ */
describe('Server Initialization Smoke Test', () => { describe('Server Initialization Smoke Test', () => {
let app: any;
beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
app = (await import('../../../server')).default;
});
afterAll(() => {
vi.unstubAllEnvs();
});
it('should import the server app without crashing', () => { it('should import the server app without crashing', () => {
// This test's primary purpose is to ensure that all top-level code in `server.ts` // This test's primary purpose is to ensure that all top-level code in `server.ts`
// can execute without throwing an error. This catches issues like syntax errors, // can execute without throwing an error. This catches issues like syntax errors,

View File

@@ -1,13 +1,23 @@
// src/tests/integration/system.integration.test.ts // src/tests/integration/system.integration.test.ts
import { describe, it, expect } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
/** /**
* @vitest-environment node * @vitest-environment node
*/ */
describe('System API Routes Integration Tests', () => { describe('System API Routes Integration Tests', () => {
let app: any;
beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
app = (await import('../../../server')).default;
});
afterAll(() => {
vi.unstubAllEnvs();
});
describe('GET /api/system/pm2-status', () => { describe('GET /api/system/pm2-status', () => {
it('should return a status for PM2', async () => { it('should return a status for PM2', async () => {
const request = supertest(app); const request = supertest(app);

View File

@@ -1,9 +1,8 @@
// src/tests/integration/user.integration.test.ts // src/tests/integration/user.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import path from 'path'; import path from 'path';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import app from '../../../server';
import { logger } from '../../services/logger.server'; import { logger } from '../../services/logger.server';
import { getPool } from '../../services/db/connection.db'; import { getPool } from '../../services/db/connection.db';
import type { UserProfile, MasterGroceryItem, ShoppingList } from '../../types'; import type { UserProfile, MasterGroceryItem, ShoppingList } from '../../types';
@@ -15,9 +14,8 @@ import { cleanupFiles } from '../utils/cleanupFiles';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('User API Routes Integration Tests', () => { describe('User API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let testUser: UserProfile; let testUser: UserProfile;
let authToken: string; let authToken: string;
const createdUserIds: string[] = []; const createdUserIds: string[] = [];
@@ -25,6 +23,10 @@ describe('User API Routes Integration Tests', () => {
// Before any tests run, create a new user and log them in. // Before any tests run, create a new user and log them in.
// The token will be used for all subsequent API calls in this test suite. // The token will be used for all subsequent API calls in this test suite.
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
const email = `user-test-${Date.now()}@example.com`; const email = `user-test-${Date.now()}@example.com`;
const { user, token } = await createAndLoginUser({ email, fullName: 'Test User', request }); const { user, token } = await createAndLoginUser({ email, fullName: 'Test User', request });
testUser = user; testUser = user;
@@ -35,6 +37,7 @@ describe('User API Routes Integration Tests', () => {
// After all tests, clean up by deleting the created user. // After all tests, clean up by deleting the created user.
// This now cleans up ALL users created by this test suite to prevent pollution. // This now cleans up ALL users created by this test suite to prevent pollution.
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
await cleanupDb({ userIds: createdUserIds }); await cleanupDb({ userIds: createdUserIds });
// Safeguard to clean up any avatar files created during tests. // Safeguard to clean up any avatar files created during tests.

View File

@@ -1,7 +1,6 @@
// src/tests/integration/user.routes.integration.test.ts // src/tests/integration/user.routes.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import app from '../../../server';
import type { UserProfile } from '../../types'; import type { UserProfile } from '../../types';
import { createAndLoginUser } from '../utils/testHelpers'; import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup'; import { cleanupDb } from '../utils/cleanup';
@@ -10,15 +9,18 @@ import { cleanupDb } from '../utils/cleanup';
* @vitest-environment node * @vitest-environment node
*/ */
const request = supertest(app);
describe('User Routes Integration Tests (/api/users)', () => { describe('User Routes Integration Tests (/api/users)', () => {
let request: ReturnType<typeof supertest>;
let authToken = ''; let authToken = '';
let testUser: UserProfile; let testUser: UserProfile;
const createdUserIds: string[] = []; const createdUserIds: string[] = [];
// Authenticate once before all tests in this suite to get a JWT. // Authenticate once before all tests in this suite to get a JWT.
beforeAll(async () => { beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Use the helper to create and log in a user in one step. // Use the helper to create and log in a user in one step.
const { user, token } = await createAndLoginUser({ const { user, token } = await createAndLoginUser({
fullName: 'User Routes Test User', fullName: 'User Routes Test User',
@@ -30,6 +32,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
}); });
afterAll(async () => { afterAll(async () => {
vi.unstubAllEnvs();
await cleanupDb({ userIds: createdUserIds }); await cleanupDb({ userIds: createdUserIds });
}); });

View File

@@ -22,6 +22,11 @@ const getPool = () => {
* and then rebuilds it from the master rollup script. * and then rebuilds it from the master rollup script.
*/ */
export async function setup() { export async function setup() {
// Ensure we are in the correct environment for these tests.
process.env.NODE_ENV = 'test';
// Set the FRONTEND_URL globally for any scripts or processes spawned here.
process.env.FRONTEND_URL = process.env.FRONTEND_URL || 'https://example.com';
// --- START DEBUG LOGGING --- // --- START DEBUG LOGGING ---
// Log the database connection details being used by the Vitest GLOBAL SETUP process. // Log the database connection details being used by the Vitest GLOBAL SETUP process.
// These variables are inherited from the CI environment. // These variables are inherited from the CI environment.

View File

@@ -1,7 +1,6 @@
// src/tests/setup/integration-global-setup.ts // src/tests/setup/integration-global-setup.ts
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import type { Server } from 'http'; import type { Server } from 'http';
import app from '../../../server'; // Import the Express app
import { logger } from '../../services/logger.server'; import { logger } from '../../services/logger.server';
import { getPool } from '../../services/db/connection.db'; import { getPool } from '../../services/db/connection.db';
@@ -13,6 +12,9 @@ let globalPool: ReturnType<typeof getPool> | null = null;
export async function setup() { export async function setup() {
// Ensure we are in the correct environment for these tests. // Ensure we are in the correct environment for these tests.
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
// Fix: Set the FRONTEND_URL globally for the test server instance
process.env.FRONTEND_URL = 'https://example.com';
console.log(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Setup ---`); console.log(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Setup ---`);
// The integration setup is now the single source of truth for preparing the test DB. // The integration setup is now the single source of truth for preparing the test DB.
@@ -30,6 +32,10 @@ export async function setup() {
console.log(`[PID:${process.pid}] Initializing global database pool...`); console.log(`[PID:${process.pid}] Initializing global database pool...`);
globalPool = getPool(); globalPool = getPool();
// Fix: Dynamic import AFTER env vars are set
const appModule = await import('../../../server');
const app = appModule.default;
// Programmatically start the server within the same process. // Programmatically start the server within the same process.
const port = process.env.PORT || 3001; const port = process.env.PORT || 3001;
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {

View File

@@ -178,7 +178,7 @@ export const createMockFlyer = (
store_id: overrides.store_id ?? overrides.store?.store_id, store_id: overrides.store_id ?? overrides.store?.store_id,
}); });
const baseUrl = 'http://localhost:3001'; // A reasonable default for tests const baseUrl = 'https://example.com'; // A reasonable default for tests
// Determine the final file_name to generate dependent properties from. // Determine the final file_name to generate dependent properties from.
const fileName = overrides.file_name ?? `flyer-${flyerId}.jpg`; const fileName = overrides.file_name ?? `flyer-${flyerId}.jpg`;

View File

@@ -7,8 +7,8 @@ import supertest from 'supertest';
export const TEST_PASSWORD = 'a-much-stronger-password-for-testing-!@#$'; export const TEST_PASSWORD = 'a-much-stronger-password-for-testing-!@#$';
export const TEST_EXAMPLE_DOMAIN = 'https://example.com'; export const TEST_EXAMPLE_DOMAIN = 'https://example.com';
export const getTestBaseUrl = (fallbackPort = 3000): string => { export const getTestBaseUrl = (): string => {
const url = process.env.FRONTEND_URL || `http://localhost:${fallbackPort}`; const url = process.env.FRONTEND_URL || `https://example.com`;
return url.endsWith('/') ? url.slice(0, -1) : url; return url.endsWith('/') ? url.slice(0, -1) : url;
}; };

View File

@@ -43,6 +43,7 @@ export async function processAndSaveImage(
.toFile(outputPath); .toFile(outputPath);
logger.info(`Successfully processed image and saved to ${outputPath}`); logger.info(`Successfully processed image and saved to ${outputPath}`);
console.log('[DEBUG] processAndSaveImage returning:', outputFileName);
return outputFileName; return outputFileName;
} catch (error) { } catch (error) {
logger.error( logger.error(
@@ -84,6 +85,7 @@ export async function generateFlyerIcon(
.toFile(outputPath); .toFile(outputPath);
logger.info(`Successfully generated icon: ${outputPath}`); logger.info(`Successfully generated icon: ${outputPath}`);
console.log('[DEBUG] generateFlyerIcon returning:', iconFileName);
return iconFileName; return iconFileName;
} catch (error) { } catch (error) {
logger.error( logger.error(

View File

@@ -56,29 +56,21 @@ describe('serverUtils', () => {
expect(mockLogger.warn).not.toHaveBeenCalled(); expect(mockLogger.warn).not.toHaveBeenCalled();
}); });
it('should fall back to localhost with default port 3000 if no URL is provided', () => { it('should fall back to example.com with default port 3000 if no URL is provided', () => {
delete process.env.FRONTEND_URL; delete process.env.FRONTEND_URL;
delete process.env.BASE_URL; delete process.env.BASE_URL;
delete process.env.PORT; delete process.env.PORT;
const baseUrl = getBaseUrl(mockLogger); const baseUrl = getBaseUrl(mockLogger);
expect(baseUrl).toBe('http://localhost:3000'); expect(baseUrl).toBe('https://example.com:3000');
expect(mockLogger.warn).not.toHaveBeenCalled(); expect(mockLogger.warn).not.toHaveBeenCalled();
}); });
it('should fall back to localhost with the specified PORT if no URL is provided', () => {
delete process.env.FRONTEND_URL;
delete process.env.BASE_URL;
process.env.PORT = '8888';
const baseUrl = getBaseUrl(mockLogger);
expect(baseUrl).toBe('http://localhost:8888');
});
it('should log a warning and fall back if FRONTEND_URL is invalid (does not start with http)', () => { it('should log a warning and fall back if FRONTEND_URL is invalid (does not start with http)', () => {
process.env.FRONTEND_URL = 'invalid.url.com'; process.env.FRONTEND_URL = 'invalid.url.com';
const baseUrl = getBaseUrl(mockLogger); const baseUrl = getBaseUrl(mockLogger);
expect(baseUrl).toBe('http://localhost:3000'); expect(baseUrl).toBe('https://example.com:3000');
expect(mockLogger.warn).toHaveBeenCalledWith( expect(mockLogger.warn).toHaveBeenCalledWith(
"[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('invalid.url.com'). Falling back to default local URL: http://localhost:3000", "[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('invalid.url.com'). Falling back to default local URL: https://example.com:3000",
); );
}); });
}); });

View File

@@ -14,7 +14,7 @@ export function getBaseUrl(logger: Logger): string {
let baseUrl = (process.env.FRONTEND_URL || process.env.BASE_URL || '').trim(); let baseUrl = (process.env.FRONTEND_URL || process.env.BASE_URL || '').trim();
if (!baseUrl || !baseUrl.startsWith('http')) { if (!baseUrl || !baseUrl.startsWith('http')) {
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const fallbackUrl = `http://localhost:${port}`; const fallbackUrl = `https://example.com:${port}`;
if (baseUrl) { if (baseUrl) {
logger.warn( logger.warn(
`[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('${baseUrl}'). Falling back to default local URL: ${fallbackUrl}`, `[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('${baseUrl}'). Falling back to default local URL: ${fallbackUrl}`,

View File

@@ -47,7 +47,7 @@ const finalConfig = mergeConfig(
// Fix: Set environment variables to ensure generated URLs pass validation // Fix: Set environment variables to ensure generated URLs pass validation
env: { env: {
NODE_ENV: 'test', NODE_ENV: 'test',
BASE_URL: 'http://example.com', // Use a standard domain to pass strict URL validation BASE_URL: 'https://example.com', // Use a standard domain to pass strict URL validation
PORT: '3000', PORT: '3000',
}, },
// This setup script starts the backend server before tests run. // This setup script starts the backend server before tests run.