177 lines
6.6 KiB
TypeScript
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.');
|
|
}
|
|
}
|