From 3b69e58de337ffb4abe4a0650342d9ebb9882437 Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Sat, 10 Jan 2026 19:29:29 -0800 Subject: [PATCH] remove useless windows testing files, fix testing? --- .gitea/workflows/deploy-to-test.yml | 4 +- run-integration-tests.ps1 | 88 ------- run-tests.cmd | 80 ------- src/tests/setup/e2e-global-setup.ts | 240 ++++++++++++++++++++ src/tests/setup/integration-global-setup.ts | 24 +- vitest.config.e2e.ts | 47 +++- vitest.config.integration.ts | 5 +- 7 files changed, 302 insertions(+), 186 deletions(-) delete mode 100644 run-integration-tests.ps1 delete mode 100644 run-tests.cmd create mode 100644 src/tests/setup/e2e-global-setup.ts diff --git a/.gitea/workflows/deploy-to-test.yml b/.gitea/workflows/deploy-to-test.yml index 7cf5b985..4da3ddfa 100644 --- a/.gitea/workflows/deploy-to-test.yml +++ b/.gitea/workflows/deploy-to-test.yml @@ -198,8 +198,8 @@ jobs: --reporter=verbose --includeTaskLocation --testTimeout=10000 --silent=passed-only || true echo "--- Running E2E Tests ---" - # Run E2E tests using the dedicated E2E config which inherits from integration config. - # We still pass --coverage to enable it, but directory and timeout are now in the config. + # Run E2E tests using the dedicated E2E config. + # E2E uses port 3098, integration uses 3099 to avoid conflicts. npx vitest run --config vitest.config.e2e.ts --coverage \ --coverage.exclude='**/*.test.ts' \ --coverage.exclude='**/tests/**' \ diff --git a/run-integration-tests.ps1 b/run-integration-tests.ps1 deleted file mode 100644 index ef6a15c8..00000000 --- a/run-integration-tests.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -# PowerShell script to run integration tests with containerized infrastructure -# Sets up environment variables and runs the integration test suite - -Write-Host "=== Flyer Crawler Integration Test Runner ===" -ForegroundColor Cyan -Write-Host "" - -# Check if containers are running -Write-Host "Checking container status..." -ForegroundColor Yellow -$postgresRunning = podman ps --filter "name=flyer-crawler-postgres" --format "{{.Names}}" 2>$null -$redisRunning = podman ps --filter "name=flyer-crawler-redis" --format "{{.Names}}" 2>$null - -if (-not $postgresRunning) { - Write-Host "ERROR: PostgreSQL container is not running!" -ForegroundColor Red - Write-Host "Start it with: podman start flyer-crawler-postgres" -ForegroundColor Yellow - exit 1 -} - -if (-not $redisRunning) { - Write-Host "ERROR: Redis container is not running!" -ForegroundColor Red - Write-Host "Start it with: podman start flyer-crawler-redis" -ForegroundColor Yellow - exit 1 -} - -Write-Host "✓ PostgreSQL container: $postgresRunning" -ForegroundColor Green -Write-Host "✓ Redis container: $redisRunning" -ForegroundColor Green -Write-Host "" - -# Set environment variables for integration tests -Write-Host "Setting environment variables..." -ForegroundColor Yellow - -$env:NODE_ENV = "test" -$env:DB_HOST = "localhost" -$env:DB_USER = "postgres" -$env:DB_PASSWORD = "postgres" -$env:DB_NAME = "flyer_crawler_dev" -$env:DB_PORT = "5432" -$env:REDIS_URL = "redis://localhost:6379" -$env:REDIS_PASSWORD = "" -$env:FRONTEND_URL = "http://localhost:5173" -$env:VITE_API_BASE_URL = "http://localhost:3001/api" -$env:JWT_SECRET = "test-jwt-secret-for-integration-tests" -$env:NODE_OPTIONS = "--max-old-space-size=8192" - -Write-Host "✓ Environment configured" -ForegroundColor Green -Write-Host "" - -# Display configuration -Write-Host "Test Configuration:" -ForegroundColor Cyan -Write-Host " NODE_ENV: $env:NODE_ENV" -Write-Host " Database: $env:DB_HOST`:$env:DB_PORT/$env:DB_NAME" -Write-Host " Redis: $env:REDIS_URL" -Write-Host " Frontend URL: $env:FRONTEND_URL" -Write-Host "" - -# Check database connectivity -Write-Host "Verifying database connection..." -ForegroundColor Yellow -$dbCheck = podman exec flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev -c "SELECT 1;" 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Host "ERROR: Cannot connect to database!" -ForegroundColor Red - Write-Host $dbCheck - exit 1 -} -Write-Host "✓ Database connection successful" -ForegroundColor Green -Write-Host "" - -# Check URL constraints are enabled -Write-Host "Verifying URL constraints..." -ForegroundColor Yellow -$constraints = podman exec flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev -t -A -c "SELECT COUNT(*) FROM pg_constraint WHERE conname LIKE '%url_check';" -Write-Host "✓ Found $constraints URL constraint(s)" -ForegroundColor Green -Write-Host "" - -# Run integration tests -Write-Host "=== Running Integration Tests ===" -ForegroundColor Cyan -Write-Host "" - -npm run test:integration - -$exitCode = $LASTEXITCODE - -Write-Host "" -if ($exitCode -eq 0) { - Write-Host "=== Integration Tests PASSED ===" -ForegroundColor Green -} else { - Write-Host "=== Integration Tests FAILED ===" -ForegroundColor Red - Write-Host "Exit code: $exitCode" -ForegroundColor Red -} - -exit $exitCode diff --git a/run-tests.cmd b/run-tests.cmd deleted file mode 100644 index bfb95008..00000000 --- a/run-tests.cmd +++ /dev/null @@ -1,80 +0,0 @@ -@echo off -REM Simple batch script to run integration tests with container infrastructure - -echo === Flyer Crawler Integration Test Runner === -echo. - -REM Check containers -echo Checking container status... -podman ps --filter "name=flyer-crawler-postgres" --format "{{.Names}}" >nul 2>&1 -if errorlevel 1 ( - echo ERROR: PostgreSQL container is not running! - echo Start it with: podman start flyer-crawler-postgres - exit /b 1 -) - -podman ps --filter "name=flyer-crawler-redis" --format "{{.Names}}" >nul 2>&1 -if errorlevel 1 ( - echo ERROR: Redis container is not running! - echo Start it with: podman start flyer-crawler-redis - exit /b 1 -) - -echo [OK] Containers are running -echo. - -REM Set environment variables -echo Setting environment variables... -set NODE_ENV=test -set DB_HOST=localhost -set DB_USER=postgres -set DB_PASSWORD=postgres -set DB_NAME=flyer_crawler_dev -set DB_PORT=5432 -set REDIS_URL=redis://localhost:6379 -set REDIS_PASSWORD= -set FRONTEND_URL=http://localhost:5173 -set VITE_API_BASE_URL=http://localhost:3001/api -set JWT_SECRET=test-jwt-secret-for-integration-tests -set NODE_OPTIONS=--max-old-space-size=8192 - -echo [OK] Environment configured -echo. - -echo Test Configuration: -echo NODE_ENV: %NODE_ENV% -echo Database: %DB_HOST%:%DB_PORT%/%DB_NAME% -echo Redis: %REDIS_URL% -echo Frontend URL: %FRONTEND_URL% -echo. - -REM Verify database -echo Verifying database connection... -podman exec flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev -c "SELECT 1;" >nul 2>&1 -if errorlevel 1 ( - echo ERROR: Cannot connect to database! - exit /b 1 -) -echo [OK] Database connection successful -echo. - -REM Check URL constraints -echo Verifying URL constraints... -podman exec flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev -t -A -c "SELECT COUNT(*) FROM pg_constraint WHERE conname LIKE '%%url_check';" -echo. - -REM Run tests -echo === Running Integration Tests === -echo. - -npm run test:integration - -if errorlevel 1 ( - echo. - echo === Integration Tests FAILED === - exit /b 1 -) else ( - echo. - echo === Integration Tests PASSED === - exit /b 0 -) diff --git a/src/tests/setup/e2e-global-setup.ts b/src/tests/setup/e2e-global-setup.ts new file mode 100644 index 00000000..46277c15 --- /dev/null +++ b/src/tests/setup/e2e-global-setup.ts @@ -0,0 +1,240 @@ +// src/tests/setup/e2e-global-setup.ts +import { execSync } from 'child_process'; +import fs from 'node:fs/promises'; +import path from 'path'; +import os from 'os'; +import type { Server } from 'http'; +import { logger } from '../../services/logger.server'; +import { getPool } from '../../services/db/connection.db'; + +// --- DEBUG: Log when this file is first loaded/parsed --- +const SETUP_LOAD_TIME = new Date().toISOString(); +console.error(`\n[E2E-SETUP-DEBUG] Module loaded at ${SETUP_LOAD_TIME}`); +console.error(`[E2E-SETUP-DEBUG] Current working directory: ${process.cwd()}`); +console.error(`[E2E-SETUP-DEBUG] NODE_ENV: ${process.env.NODE_ENV}`); +console.error(`[E2E-SETUP-DEBUG] __filename: ${import.meta.url}`); + +// --- Centralized State for E2E Test Lifecycle --- +let server: Server; +// This will hold the single database pool instance for the entire test run. +let globalPool: ReturnType | null = null; +// Temporary directory for test file storage (to avoid modifying committed fixtures) +let tempStorageDir: string | null = null; + +/** + * Cleans all BullMQ queues to ensure no stale jobs from previous test runs. + * This is critical because old jobs with outdated error messages can pollute test results. + */ +async function cleanAllQueues() { + console.error(`[PID:${process.pid}] [E2E QUEUE CLEANUP] Starting BullMQ queue cleanup...`); + + try { + const { + flyerQueue, + cleanupQueue, + emailQueue, + analyticsQueue, + weeklyAnalyticsQueue, + tokenCleanupQueue, + } = await import('../../services/queues.server'); + console.error(`[E2E QUEUE CLEANUP] Successfully imported queue modules`); + + const queues = [ + flyerQueue, + cleanupQueue, + emailQueue, + analyticsQueue, + weeklyAnalyticsQueue, + tokenCleanupQueue, + ]; + + for (const queue of queues) { + try { + const jobCounts = await queue.getJobCounts(); + console.error( + `[E2E QUEUE CLEANUP] Queue "${queue.name}" before cleanup: ${JSON.stringify(jobCounts)}`, + ); + + await queue.obliterate({ force: true }); + console.error(` [E2E QUEUE CLEANUP] Cleaned queue: ${queue.name}`); + } catch (error) { + console.error( + ` [E2E QUEUE CLEANUP] Could not clean queue ${queue.name}: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } + } + console.error(`[PID:${process.pid}] [E2E QUEUE CLEANUP] All queues cleaned successfully.`); + } catch (error) { + console.error( + `[PID:${process.pid}] [E2E QUEUE CLEANUP] CRITICAL ERROR during queue cleanup:`, + error, + ); + // Don't throw - we want the tests to continue even if cleanup fails + } +} + +export async function setup() { + console.error(`\n[E2E-SETUP-DEBUG] ========================================`); + console.error(`[E2E-SETUP-DEBUG] setup() function STARTED at ${new Date().toISOString()}`); + console.error(`[E2E-SETUP-DEBUG] ========================================`); + + // Ensure we are in the correct environment for these tests. + process.env.NODE_ENV = 'test'; + process.env.FRONTEND_URL = 'https://example.com'; + + // CRITICAL: Create a temporary directory for test file storage. + // This prevents tests from modifying or deleting committed fixture files. + // The temp directory is cleaned up in teardown(). + tempStorageDir = await fs.mkdtemp(path.join(os.tmpdir(), 'flyer-crawler-e2e-')); + const tempFlyerImagesDir = path.join(tempStorageDir, 'flyer-images'); + await fs.mkdir(path.join(tempFlyerImagesDir, 'icons'), { recursive: true }); + console.error(`[E2E-SETUP] Created temporary storage directory: ${tempFlyerImagesDir}`); + + // CRITICAL: Set STORAGE_PATH before importing the server. + process.env.STORAGE_PATH = tempFlyerImagesDir; + console.error(`[E2E-SETUP] Set STORAGE_PATH to temporary directory: ${process.env.STORAGE_PATH}`); + + console.error(`\n--- [PID:${process.pid}] Running E2E Test GLOBAL Setup ---`); + console.error(`[E2E-SETUP] STORAGE_PATH: ${process.env.STORAGE_PATH}`); + console.error(`[E2E-SETUP] REDIS_URL: ${process.env.REDIS_URL}`); + console.error(`[E2E-SETUP] REDIS_PASSWORD is set: ${!!process.env.REDIS_PASSWORD}`); + + // Clean all queues BEFORE running any tests + console.error(`[E2E-SETUP] About to call cleanAllQueues()...`); + await cleanAllQueues(); + console.error(`[E2E-SETUP] cleanAllQueues() completed.`); + + // Seed the database for E2E tests + try { + console.log(`\n[PID:${process.pid}] Running database seed script for E2E tests...`); + execSync('npx cross-env NODE_ENV=test npx tsx src/db/seed.ts', { stdio: 'inherit' }); + console.log(`[PID:${process.pid}] Database seed script finished.`); + } catch (error) { + console.error('Failed to reset and seed the test database. Aborting E2E tests.', error); + process.exit(1); + } + + // Initialize the global pool instance once. + console.log(`[PID:${process.pid}] Initializing global database pool...`); + globalPool = getPool(); + + // Dynamic import AFTER env vars are set + console.error(`[E2E-SETUP-DEBUG] About to import server module...`); + const appModule = await import('../../../server'); + console.error(`[E2E-SETUP-DEBUG] Server module imported successfully`); + const app = appModule.default; + console.error(`[E2E-SETUP-DEBUG] App object type: ${typeof app}`); + + // Use a dedicated E2E test port (3098) to avoid conflicts with integration tests (3099) + // and production servers (3001) + const port = process.env.TEST_PORT || 3098; + console.error(`[E2E-SETUP-DEBUG] Attempting to start E2E server on port ${port}...`); + + await new Promise((resolve, reject) => { + let settled = false; + try { + server = app.listen(port, () => { + if (settled) return; + settled = true; + console.log(`In-process E2E test server started on port ${port}`); + console.error( + `[E2E-SETUP-DEBUG] Server listen callback invoked at ${new Date().toISOString()}`, + ); + resolve(); + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (settled) return; + settled = true; + console.error(`[E2E-SETUP-DEBUG] Server error event:`, err.message); + if (err.code === 'EADDRINUSE') { + console.error( + `[E2E-SETUP-DEBUG] Port ${port} is already in use! ` + + `Set TEST_PORT env var to use a different port.`, + ); + } + reject(err); + }); + } catch (err) { + if (settled) return; + settled = true; + console.error(`[E2E-SETUP-DEBUG] Error during app.listen:`, err); + reject(err); + } + }); + + /** + * Ping the E2E test server to verify it's ready. + */ + const pingTestBackend = async (): Promise => { + const pingUrl = `http://localhost:${port}/api/health/ping`; + console.error(`[E2E-SETUP-DEBUG] Pinging: ${pingUrl}`); + try { + const response = await fetch(pingUrl); + console.error(`[E2E-SETUP-DEBUG] Ping response status: ${response.status}`); + if (!response.ok) { + console.error(`[E2E-SETUP-DEBUG] Ping response not OK: ${response.statusText}`); + return false; + } + const json = await response.json(); + console.error(`[E2E-SETUP-DEBUG] Ping response JSON:`, JSON.stringify(json)); + return json?.data?.message === 'pong'; + } catch (e) { + const errMsg = e instanceof Error ? e.message : String(e); + console.error(`[E2E-SETUP-DEBUG] Ping exception: ${errMsg}`); + logger.debug({ error: e }, 'Ping failed while waiting for E2E server, this is expected.'); + return false; + } + }; + + console.error( + `[E2E-SETUP-DEBUG] Server started, beginning ping loop at ${new Date().toISOString()}`, + ); + console.error(`[E2E-SETUP-DEBUG] Server address info:`, server.address()); + + const maxRetries = 15; + const retryDelay = 1000; + for (let i = 0; i < maxRetries; i++) { + console.error(`[E2E-SETUP-DEBUG] Ping attempt ${i + 1}/${maxRetries}`); + if (await pingTestBackend()) { + console.log('E2E backend server is running and responsive.'); + console.error( + `[E2E-SETUP-DEBUG] setup() function COMPLETED SUCCESSFULLY at ${new Date().toISOString()}`, + ); + return; + } + console.log( + `[PID:${process.pid}] Waiting for E2E backend server... (attempt ${i + 1}/${maxRetries})`, + ); + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + + console.error(`[E2E-SETUP-DEBUG] All ${maxRetries} ping attempts failed!`); + console.error(`[E2E-SETUP-DEBUG] Server listening status: ${server.listening}`); + console.error(`[E2E-SETUP-DEBUG] Server address: ${JSON.stringify(server.address())}`); + + throw new Error('E2E backend server failed to start.'); +} + +export async function teardown() { + console.log(`\n--- [PID:${process.pid}] Running E2E Test GLOBAL Teardown ---`); + // 1. Stop the server to release any resources it's holding. + if (server) { + await new Promise((resolve) => server.close(() => resolve())); + console.log('In-process E2E test server stopped.'); + } + // 2. Close the single, shared database pool. + if (globalPool) { + await globalPool.end(); + console.log('E2E global database pool teardown complete.'); + } + // 3. Clean up the temporary storage directory. + if (tempStorageDir) { + try { + await fs.rm(tempStorageDir, { recursive: true, force: true }); + console.log(`Cleaned up E2E temporary storage directory: ${tempStorageDir}`); + } catch (error) { + console.error(`Warning: Could not clean up E2E temp directory ${tempStorageDir}:`, error); + } + } +} diff --git a/src/tests/setup/integration-global-setup.ts b/src/tests/setup/integration-global-setup.ts index c922fa67..ccb2bc1e 100644 --- a/src/tests/setup/integration-global-setup.ts +++ b/src/tests/setup/integration-global-setup.ts @@ -136,12 +136,16 @@ export async function setup() { console.error(`[GLOBAL-SETUP-DEBUG] App object type: ${typeof app}`); // Programmatically start the server within the same process. - const port = process.env.PORT || 3001; + // Use a dedicated test port to avoid conflicts with production servers. + const port = process.env.TEST_PORT || process.env.PORT || 3099; console.error(`[GLOBAL-SETUP-DEBUG] Attempting to start server on port ${port}...`); await new Promise((resolve, reject) => { + let settled = false; // Prevent double-resolution race condition try { server = app.listen(port, () => { + if (settled) return; + settled = true; console.log(`✅ In-process test server started on port ${port}`); console.error( `[GLOBAL-SETUP-DEBUG] Server listen callback invoked at ${new Date().toISOString()}`, @@ -150,25 +154,33 @@ export async function setup() { }); server.on('error', (err: NodeJS.ErrnoException) => { + if (settled) return; + settled = true; console.error(`[GLOBAL-SETUP-DEBUG] Server error event:`, err.message); if (err.code === 'EADDRINUSE') { - console.error(`[GLOBAL-SETUP-DEBUG] Port ${port} is already in use!`); + console.error( + `[GLOBAL-SETUP-DEBUG] Port ${port} is already in use! ` + + `Set TEST_PORT env var to use a different port.`, + ); } reject(err); }); } catch (err) { + if (settled) return; + settled = true; console.error(`[GLOBAL-SETUP-DEBUG] Error during app.listen:`, err); reject(err); } }); /** - * A local ping function that respects the VITE_API_BASE_URL from the test environment. - * This is necessary because the global apiClient's URL is configured for browser use. + * A local ping function that pings the test server we just started. + * Uses the same port that the server was started on to avoid hitting + * a different server that might be running on the default port. */ const pingTestBackend = async (): Promise => { - const apiUrl = process.env.VITE_API_BASE_URL || 'http://localhost:3001/api'; - const pingUrl = `${apiUrl.replace('/api', '')}/api/health/ping`; + // Always ping the port we started on, not what's in env vars + const pingUrl = `http://localhost:${port}/api/health/ping`; console.error(`[GLOBAL-SETUP-DEBUG] Pinging: ${pingUrl}`); try { const response = await fetch(pingUrl); diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index 154418c0..04b5d5bf 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -1,26 +1,55 @@ +// vitest.config.e2e.ts import { defineConfig, mergeConfig } from 'vitest/config'; -import integrationConfig from './vitest.config.integration'; +import type { UserConfig } from 'vite'; +import viteConfig from './vite.config'; +// Ensure NODE_ENV is set to 'test' for all Vitest runs. +process.env.NODE_ENV = 'test'; + +// Define a type that includes the 'test' property from Vitest's config. +type ViteConfigWithTest = UserConfig & { test?: UserConfig['test'] }; + +const { test: _unusedTest, ...baseViteConfig } = viteConfig as ViteConfigWithTest; + +/** + * E2E test configuration. + * Uses a DIFFERENT port (3098) than integration tests (3099) to allow + * both test suites to run sequentially without port conflicts. + */ const e2eConfig = mergeConfig( - integrationConfig, + baseViteConfig, defineConfig({ test: { name: 'e2e', + environment: 'node', // Point specifically to E2E tests include: ['src/tests/e2e/**/*.e2e.test.ts'], + exclude: [], + // E2E tests use a different port to avoid conflicts with integration tests + env: { + NODE_ENV: 'test', + BASE_URL: 'https://example.com', + FRONTEND_URL: 'https://example.com', + // Use port 3098 for E2E tests (integration uses 3099) + TEST_PORT: '3098', + VITE_API_BASE_URL: 'http://localhost:3098/api', + }, + // E2E tests have their own dedicated global setup file + globalSetup: './src/tests/setup/e2e-global-setup.ts', + setupFiles: ['./src/tests/setup/global.ts'], // Increase timeout for E2E flows that involve AI or full API chains testTimeout: 120000, + hookTimeout: 60000, + fileParallelism: false, coverage: { + provider: 'v8', + reporter: ['html', 'json-summary', 'json'], reportsDirectory: '.coverage/e2e', + reportOnFailure: true, + clean: true, }, }, }), ); -// Explicitly override the include array to ensure we don't inherit integration tests -// (mergeConfig might concatenate arrays by default) -if (e2eConfig.test) { - e2eConfig.test.include = ['src/tests/e2e/**/*.e2e.test.ts']; -} - -export default e2eConfig; \ No newline at end of file +export default e2eConfig; diff --git a/vitest.config.integration.ts b/vitest.config.integration.ts index 37963358..4b348d22 100644 --- a/vitest.config.integration.ts +++ b/vitest.config.integration.ts @@ -65,7 +65,10 @@ const finalConfig = mergeConfig( NODE_ENV: 'test', BASE_URL: 'https://example.com', // Use a standard domain to pass strict URL validation FRONTEND_URL: 'https://example.com', - PORT: '3000', + // Use a dedicated test port (3099) to avoid conflicts with production servers + // that might be running on port 3000 or 3001 + TEST_PORT: '3099', + VITE_API_BASE_URL: 'http://localhost:3099/api', }, // This setup script starts the backend server before tests run. globalSetup: './src/tests/setup/integration-global-setup.ts',