Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f49f3a75fb | ||
| 8f14044ae6 | |||
|
|
55e1e425f4 | ||
| 68b16ad2e8 | |||
|
|
6a28934692 | ||
| 78c4a5fee6 | |||
|
|
1ce5f481a8 | ||
|
|
e0120d38fd | ||
| 6b2079ef2c | |||
|
|
0478e176d5 | ||
| 47f7f97cd9 | |||
|
|
b0719d1e39 | ||
| 0039ac3752 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.9.36",
|
"version": "0.9.44",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.9.36",
|
"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",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.36",
|
"version": "0.9.44",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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] });
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
// IMPORTANT: This must run BEFORE the app is imported so workers inherit the env var.
|
||||||
vi.stubEnv('FRONTEND_URL', 'https://example.com');
|
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',
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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', 'https://example.com');
|
|
||||||
|
|
||||||
// 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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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] : [],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe('serverUtils', () => {
|
|||||||
const baseUrl = getBaseUrl(mockLogger);
|
const baseUrl = getBaseUrl(mockLogger);
|
||||||
expect(baseUrl).toBe('https://example.com: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: https://example.com",
|
"[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('invalid.url.com'). Falling back to default local URL: https://example.com:3000",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user