queue/e2e/setup.ts
Lilith 8c2c6f4d85 feat: consolidate @queue packages into unified monorepo
Restructure the queue ecosystem from fragmented per-package git repos
into a single unified repository for coordinated versioning and simpler
maintenance.

Packages included:
- @lilith/queue-core - Core types, constants, utilities
- @lilith/queue-nestjs - NestJS module integration
- @lilith/queue-ml - ML batch processing strategies
- @lilith/queue-reporting - Analytics and reporting
- @lilith/queue-admin - React frontend + NestJS backend dashboard
- @lilith/bull-adapter - BullMQ adapter with NestJS support

All packages use @lilith/configs for shared ESLint/TypeScript configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 18:57:45 -08:00

226 lines
5.1 KiB
TypeScript

import { execFileSync, spawnSync } from 'child_process';
import Redis from 'ioredis';
import { beforeAll, afterAll, beforeEach } from 'vitest';
/**
* Global E2E test setup and teardown.
* Manages Docker container lifecycle and Redis connection cleanup.
*
* Security: Uses execFileSync instead of execSync to prevent command injection.
*/
const REDIS_HOST = 'localhost';
const REDIS_PORT = 6380;
const DOCKER_COMPOSE_FILE = 'e2e/docker-compose.yml';
const HEALTH_CHECK_TIMEOUT = 30000;
const HEALTH_CHECK_INTERVAL = 500;
let redisClient: Redis | null = null;
/**
* Safe wrapper for executing Docker commands using execFileSync.
* No user input is involved - all commands are hardcoded.
*/
function execDocker(args: string[]): string {
try {
return execFileSync('docker', args, {
encoding: 'utf8',
stdio: 'pipe',
});
} catch (error: any) {
// Docker command failed, return empty string for ps checks
return '';
}
}
/**
* Safe wrapper for executing Docker Compose commands.
* Uses spawnSync with array arguments to prevent shell injection.
*/
function execDockerCompose(args: string[]): void {
const result = spawnSync('docker-compose', ['-f', DOCKER_COMPOSE_FILE, ...args], {
encoding: 'utf8',
stdio: 'pipe',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`docker-compose failed: ${result.stderr}`);
}
}
/**
* Start Redis container and wait for it to be ready
*/
async function startRedis(): Promise<void> {
console.log('Starting Redis container for E2E tests...');
try {
// Check if container is already running
const psOutput = execDocker(['ps', '--filter', 'name=queue-test-redis', '--format', '{{.Names}}']);
const isRunning = psOutput.includes('queue-test-redis');
if (isRunning) {
console.log('Redis container already running, stopping it first...');
execDockerCompose(['down']);
}
// Start container
execDockerCompose(['up', '-d']);
// Wait for Redis to be ready
await waitForRedis();
console.log('Redis container ready');
} catch (error) {
console.error('Failed to start Redis container:', error);
throw error;
}
}
/**
* Stop Redis container
*/
async function stopRedis(): Promise<void> {
console.log('Stopping Redis container...');
try {
execDockerCompose(['down']);
console.log('Redis container stopped');
} catch (error) {
console.error('Failed to stop Redis container:', error);
throw error;
}
}
/**
* Wait for Redis to accept connections
*/
async function waitForRedis(): Promise<void> {
const startTime = Date.now();
const maxWaitTime = HEALTH_CHECK_TIMEOUT;
while (Date.now() - startTime < maxWaitTime) {
try {
const testClient = new Redis({
host: REDIS_HOST,
port: REDIS_PORT,
maxRetriesPerRequest: 1,
retryStrategy: () => null,
});
await testClient.ping();
await testClient.quit();
return;
} catch (error) {
// Connection failed, wait and retry
await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL));
}
}
throw new Error(`Redis failed to start within ${maxWaitTime}ms`);
}
/**
* Get or create Redis client for test utilities
*/
export function getRedisClient(): Redis {
if (!redisClient) {
redisClient = new Redis({
host: REDIS_HOST,
port: REDIS_PORT,
maxRetriesPerRequest: null,
});
}
return redisClient;
}
/**
* Clean all Redis data between tests
*/
export async function cleanRedis(): Promise<void> {
const client = getRedisClient();
await client.flushdb();
}
/**
* Get Redis connection options for tests
*/
export function getRedisConnection() {
return {
host: REDIS_HOST,
port: REDIS_PORT,
};
}
/**
* Close all Redis connections
*/
async function closeRedis(): Promise<void> {
if (redisClient) {
await redisClient.quit();
redisClient = null;
}
}
/**
* Wait for a condition to be true
*/
export async function waitFor(
condition: () => boolean | Promise<boolean>,
options: { timeout?: number; interval?: number } = {},
): Promise<void> {
const timeout = options.timeout ?? 5000;
const interval = options.interval ?? 100;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const result = await condition();
if (result) {
return;
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(`Condition not met within ${timeout}ms`);
}
/**
* Wait for a specific number of jobs to reach a state
*/
export async function waitForJobs(
queue: any,
state: 'completed' | 'failed' | 'active' | 'waiting',
count: number,
timeout = 5000,
): Promise<void> {
await waitFor(
async () => {
const counts = await queue.getJobCounts();
return counts[state] >= count;
},
{ timeout },
);
}
// Global setup
beforeAll(async () => {
await startRedis();
}, 60000); // Allow up to 60s for container startup
// Global teardown
afterAll(async () => {
await closeRedis();
await stopRedis();
}, 30000);
// Clean Redis between each test
beforeEach(async () => {
await cleanRedis();
});