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>
226 lines
5.1 KiB
TypeScript
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();
|
|
});
|