Files
flyer-crawler.projectium.com/src/tests/setup/integration-global-setup.ts

177 lines
6.6 KiB
TypeScript

// src/tests/setup/integration-global-setup.ts
import { execSync } from 'child_process';
import fs from 'node:fs/promises';
import path from 'path';
import type { Server } from 'http';
import { logger } from '../../services/logger.server';
import { getPool } from '../../services/db/connection.db';
// --- Centralized State for Integration Test Lifecycle ---
let server: Server;
// This will hold the single database pool instance for the entire test run.
let globalPool: ReturnType<typeof getPool> | 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() {
// Use console.error for visibility in CI logs (stderr is often more reliable)
console.error(`[PID:${process.pid}] [QUEUE CLEANUP] Starting BullMQ queue cleanup...`);
try {
const {
flyerQueue,
cleanupQueue,
emailQueue,
analyticsQueue,
weeklyAnalyticsQueue,
tokenCleanupQueue,
} = await import('../../services/queues.server');
console.error(`[QUEUE CLEANUP] Successfully imported queue modules`);
const queues = [
flyerQueue,
cleanupQueue,
emailQueue,
analyticsQueue,
weeklyAnalyticsQueue,
tokenCleanupQueue,
];
for (const queue of queues) {
try {
// Log queue state before cleanup
const jobCounts = await queue.getJobCounts();
console.error(
`[QUEUE CLEANUP] Queue "${queue.name}" before cleanup: ${JSON.stringify(jobCounts)}`,
);
// obliterate() removes ALL data associated with the queue from Redis
await queue.obliterate({ force: true });
console.error(` ✅ [QUEUE CLEANUP] Cleaned queue: ${queue.name}`);
} catch (error) {
// Log but don't fail - the queue might not exist yet
console.error(
` ⚠️ [QUEUE CLEANUP] Could not clean queue ${queue.name}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
console.error(`✅ [PID:${process.pid}] [QUEUE CLEANUP] All queues cleaned successfully.`);
} catch (error) {
console.error(
`❌ [PID:${process.pid}] [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() {
// Ensure we are in the correct environment for these tests.
process.env.NODE_ENV = 'test';
// Fix: Set the FRONTEND_URL globally for the test server instance
process.env.FRONTEND_URL = 'https://example.com';
// CRITICAL: Set STORAGE_PATH before importing the server.
// The multer middleware runs an IIFE on import that creates directories based on this path.
// If not set, it defaults to /var/www/.../flyer-images which won't exist in the test environment.
if (!process.env.STORAGE_PATH) {
// Use path relative to the project root (where tests run from)
process.env.STORAGE_PATH = path.resolve(process.cwd(), 'flyer-images');
}
// Ensure the storage directories exist before the server starts
try {
await fs.mkdir(path.join(process.env.STORAGE_PATH, 'icons'), { recursive: true });
console.error(`[SETUP] Created storage directory: ${process.env.STORAGE_PATH}`);
} catch (error) {
console.error(`[SETUP] Warning: Could not create storage directory: ${error}`);
}
console.error(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Setup ---`);
console.error(`[SETUP] STORAGE_PATH: ${process.env.STORAGE_PATH}`);
console.error(`[SETUP] REDIS_URL: ${process.env.REDIS_URL}`);
console.error(`[SETUP] REDIS_PASSWORD is set: ${!!process.env.REDIS_PASSWORD}`);
// CRITICAL: Clean all queues BEFORE running any tests to remove stale jobs
// from previous test runs that may have outdated error messages.
console.error(`[SETUP] About to call cleanAllQueues()...`);
await cleanAllQueues();
console.error(`[SETUP] cleanAllQueues() completed.`);
// The integration setup is now the single source of truth for preparing the test DB.
// It runs the same seed script that `npm run db:reset:test` used.
try {
console.log(`\n[PID:${process.pid}] Running database seed script...`);
execSync('npm run db:reset:test', { stdio: 'inherit' });
console.log(`✅ [PID:${process.pid}] Database seed script finished.`);
} catch (error) {
console.error('🔴 Failed to reset and seed the test database. Aborting tests.', error);
process.exit(1);
}
// Initialize the global pool instance once.
console.log(`[PID:${process.pid}] Initializing global database pool...`);
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.
const port = process.env.PORT || 3001;
await new Promise<void>((resolve) => {
server = app.listen(port, () => {
console.log(`✅ In-process test server started on port ${port}`);
resolve();
});
});
/**
* 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.
*/
const pingTestBackend = async (): Promise<boolean> => {
const apiUrl = process.env.VITE_API_BASE_URL || 'http://localhost:3001/api';
try {
const response = await fetch(`${apiUrl.replace('/api', '')}/api/health/ping`);
if (!response.ok) return false;
const text = await response.text();
return text === 'pong';
} catch (e) {
logger.debug({ error: e }, 'Ping failed while waiting for server, this is expected.');
return false;
}
};
const maxRetries = 15;
const retryDelay = 1000;
for (let i = 0; i < maxRetries; i++) {
if (await pingTestBackend()) {
console.log('✅ Backend server is running and responsive.');
return;
}
console.log(
`[PID:${process.pid}] Waiting for backend server... (attempt ${i + 1}/${maxRetries})`,
);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
throw new Error('Backend server failed to start.');
}
export async function teardown() {
console.log(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Teardown ---`);
// 1. Stop the server to release any resources it's holding.
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
console.log('✅ In-process test server stopped.');
}
// 2. Close the single, shared database pool.
if (globalPool) {
await globalPool.end();
console.log('✅ Global database pool teardown complete.');
}
}