Files
Charon/tests/utils/health-check.ts
2026-03-04 18:34:49 +00:00

422 lines
11 KiB
TypeScript

/**
* Health Check Utilities - Environment verification for E2E tests
*
* These utilities ensure the test environment is healthy and ready
* before running tests, preventing false failures from infrastructure issues.
*
* @example
* ```typescript
* // In playwright.config.ts or global setup
* import { waitForHealthyEnvironment, verifyTestPrerequisites } from './utils/health-check';
*
* await waitForHealthyEnvironment('http://localhost:8080');
* await verifyTestPrerequisites();
* ```
*/
/**
* Health response from the API
*/
interface HealthResponse {
status: string;
database?: string;
version?: string;
uptime?: number;
}
/**
* Options for health check
*/
export interface HealthCheckOptions {
/** Maximum time to wait for healthy status (default: 60000ms) */
timeout?: number;
/** Interval between health check attempts (default: 2000ms) */
interval?: number;
/** Whether to log progress (default: true) */
verbose?: boolean;
}
/**
* Wait for the environment to be healthy
*
* Polls the health endpoint until the service reports healthy status
* or the timeout is reached.
*
* @param baseURL - Base URL of the application
* @param options - Configuration options
* @throws Error if environment doesn't become healthy within timeout
*/
export async function waitForHealthyEnvironment(
baseURL: string,
options: HealthCheckOptions = {}
): Promise<void> {
const { timeout = 60000, interval = 2000, verbose = true } = options;
const startTime = Date.now();
if (verbose) {
console.log(`⏳ Waiting for environment to be healthy at ${baseURL}...`);
}
while (Date.now() - startTime < timeout) {
try {
const response = await fetch(`${baseURL}/api/v1/health`);
if (response.ok) {
const health = (await response.json()) as HealthResponse;
// Check for healthy status
const isHealthy =
health.status === 'healthy' ||
health.status === 'ok' ||
health.status === 'up';
// Check database connectivity if present
const dbHealthy =
!health.database ||
health.database === 'connected' ||
health.database === 'ok' ||
health.database === 'healthy';
if (isHealthy && dbHealthy) {
if (verbose) {
console.log('✅ Environment is healthy');
if (health.version) {
console.log(` Version: ${health.version}`);
}
}
return;
}
if (verbose) {
console.log(` Status: ${health.status}, Database: ${health.database || 'unknown'}`);
}
}
} catch (error) {
// Service not ready yet - continue waiting
if (verbose && Date.now() - startTime > 10000) {
console.log(` Still waiting... (${Math.round((Date.now() - startTime) / 1000)}s)`);
}
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(
`Environment not healthy after ${timeout}ms. ` +
`Check that the application is running at ${baseURL}`
);
}
/**
* Prerequisite check result
*/
export interface PrerequisiteCheck {
name: string;
passed: boolean;
message?: string;
}
/**
* Options for prerequisite verification
*/
export interface PrerequisiteOptions {
/** Base URL (defaults to PLAYWRIGHT_BASE_URL env var) */
baseURL?: string;
/** Whether to throw on failure (default: true) */
throwOnFailure?: boolean;
/** Whether to log results (default: true) */
verbose?: boolean;
}
/**
* Verify all test prerequisites are met
*
* Checks critical system requirements before running tests:
* - API is accessible
* - Database is writable
* - Docker is accessible (if needed for proxy tests)
*
* @param options - Configuration options
* @returns Array of check results
* @throws Error if any critical check fails and throwOnFailure is true
*/
export async function verifyTestPrerequisites(
options: PrerequisiteOptions = {}
): Promise<PrerequisiteCheck[]> {
const {
baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
throwOnFailure = true,
verbose = true,
} = options;
const results: PrerequisiteCheck[] = [];
if (verbose) {
console.log('🔍 Verifying test prerequisites...');
}
// Check 1: API Health
const apiCheck = await checkAPIHealth(baseURL);
results.push(apiCheck);
if (verbose) {
logCheckResult(apiCheck);
}
// Check 2: Database writability (via test endpoint if available)
const dbCheck = await checkDatabaseWritable(baseURL);
results.push(dbCheck);
if (verbose) {
logCheckResult(dbCheck);
}
// Check 3: Docker accessibility (optional - for proxy host tests)
const dockerCheck = await checkDockerAccessible(baseURL);
results.push(dockerCheck);
if (verbose) {
logCheckResult(dockerCheck);
}
// Check 4: Authentication service
const authCheck = await checkAuthService(baseURL);
results.push(authCheck);
if (verbose) {
logCheckResult(authCheck);
}
// Determine if critical checks failed
const criticalChecks = results.filter(
(r) => r.name === 'API Health' || r.name === 'Database Writable'
);
const failedCritical = criticalChecks.filter((r) => !r.passed);
if (failedCritical.length > 0 && throwOnFailure) {
const failedNames = failedCritical.map((r) => r.name).join(', ');
throw new Error(`Critical prerequisite checks failed: ${failedNames}`);
}
if (verbose) {
const passed = results.filter((r) => r.passed).length;
console.log(`\n📋 Prerequisites: ${passed}/${results.length} passed`);
}
return results;
}
/**
* Check if the API is responding
*/
async function checkAPIHealth(baseURL: string): Promise<PrerequisiteCheck> {
try {
const response = await fetch(`${baseURL}/api/v1/health`, {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (response.ok) {
return { name: 'API Health', passed: true };
}
return {
name: 'API Health',
passed: false,
message: `HTTP ${response.status}: ${response.statusText}`,
};
} catch (error) {
return {
name: 'API Health',
passed: false,
message: `Connection failed: ${(error as Error).message}`,
};
}
}
/**
* Check if the database is writable
*/
async function checkDatabaseWritable(baseURL: string): Promise<PrerequisiteCheck> {
try {
// Try the test endpoint if available
const response = await fetch(`${baseURL}/api/v1/test/db-check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (response.ok) {
return { name: 'Database Writable', passed: true };
}
// If test endpoint doesn't exist, check via health endpoint
if (response.status === 404) {
const healthResponse = await fetch(`${baseURL}/api/v1/health`);
if (healthResponse.ok) {
const health = (await healthResponse.json()) as HealthResponse;
const dbOk =
health.database === 'connected' ||
health.database === 'ok' ||
!health.database; // Assume OK if not reported
return {
name: 'Database Writable',
passed: dbOk,
message: dbOk ? undefined : `Database status: ${health.database}`,
};
}
}
return {
name: 'Database Writable',
passed: false,
message: `Check failed: HTTP ${response.status}`,
};
} catch (error) {
return {
name: 'Database Writable',
passed: false,
message: `Check failed: ${(error as Error).message}`,
};
}
}
/**
* Check if Docker is accessible (for proxy host tests)
*/
async function checkDockerAccessible(baseURL: string): Promise<PrerequisiteCheck> {
try {
const response = await fetch(`${baseURL}/api/v1/test/docker-check`, {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (response.ok) {
return { name: 'Docker Accessible', passed: true };
}
// If endpoint doesn't exist, mark as skipped (not critical)
if (response.status === 404) {
return {
name: 'Docker Accessible',
passed: true,
message: 'Check endpoint not available (skipped)',
};
}
return {
name: 'Docker Accessible',
passed: false,
message: `HTTP ${response.status}`,
};
} catch (error) {
// Docker check is optional - mark as passed with warning
return {
name: 'Docker Accessible',
passed: true,
message: 'Could not verify (optional)',
};
}
}
/**
* Check if authentication service is working
*/
async function checkAuthService(baseURL: string): Promise<PrerequisiteCheck> {
try {
// Try to access login page or auth endpoint
const response = await fetch(`${baseURL}/api/v1/auth/status`, {
method: 'GET',
headers: { Accept: 'application/json' },
});
// 401 Unauthorized is expected without auth token
if (response.ok || response.status === 401) {
return { name: 'Auth Service', passed: true };
}
// Try login endpoint
if (response.status === 404) {
const loginResponse = await fetch(`${baseURL}/login`, {
method: 'GET',
});
if (loginResponse.ok || loginResponse.status === 200) {
return { name: 'Auth Service', passed: true };
}
}
return {
name: 'Auth Service',
passed: false,
message: `Unexpected status: HTTP ${response.status}`,
};
} catch (error) {
return {
name: 'Auth Service',
passed: false,
message: `Check failed: ${(error as Error).message}`,
};
}
}
/**
* Log a check result to console
*/
function logCheckResult(check: PrerequisiteCheck): void {
const icon = check.passed ? '✅' : '❌';
const suffix = check.message ? ` (${check.message})` : '';
console.log(` ${icon} ${check.name}${suffix}`);
}
/**
* Quick health check - returns true if environment is ready
*
* Use this for conditional test skipping or quick validation.
*
* @param baseURL - Base URL of the application
* @returns true if environment is healthy
*/
export async function isEnvironmentReady(baseURL: string): Promise<boolean> {
try {
const response = await fetch(`${baseURL}/api/v1/health`);
if (!response.ok) return false;
const health = (await response.json()) as HealthResponse;
return (
health.status === 'healthy' ||
health.status === 'ok' ||
health.status === 'up'
);
} catch {
return false;
}
}
/**
* Get environment info for debugging
*
* @param baseURL - Base URL of the application
* @returns Environment information object
*/
export async function getEnvironmentInfo(
baseURL: string
): Promise<Record<string, unknown>> {
try {
const response = await fetch(`${baseURL}/api/v1/health`);
if (!response.ok) {
return { status: 'unhealthy', httpStatus: response.status };
}
const health = await response.json();
return {
...health,
baseURL,
timestamp: new Date().toISOString(),
};
} catch (error) {
return {
status: 'unreachable',
error: (error as Error).message,
baseURL,
timestamp: new Date().toISOString(),
};
}
}