/** * Integration Test Harness - Main Entry Point * Provides reusable setup, teardown, and utilities for integration tests */ import { DockerManager } from './docker-manager'; import { DatabaseManager } from './database-manager'; import { ApiClient } from './api-client'; import { DataFactory } from './data-factory'; export interface IntegrationTestConfig { api: { baseUrl: string; port: number; }; database: { host: string; port: number; database: string; user: string; password: string; }; timeouts?: { setup?: number; teardown?: number; test?: number; }; } export class IntegrationTestHarness { private docker: DockerManager; private database: DatabaseManager; private api: ApiClient; private factory: DataFactory; private config: IntegrationTestConfig; constructor(config: IntegrationTestConfig) { this.config = { timeouts: { setup: 120000, teardown: 30000, test: 60000, ...config.timeouts, }, ...config, }; this.docker = DockerManager.getInstance(); this.database = new DatabaseManager(config.database); this.api = new ApiClient({ baseUrl: config.api.baseUrl, timeout: 60000 }); this.factory = new DataFactory(this.database); } /** * Setup hook - starts Docker services and prepares database * Called once before all tests in a suite */ async beforeAll(): Promise { console.log('[Harness] Starting integration test setup...'); // Start Docker services await this.docker.start(); // Wait for database to be ready await this.database.waitForReady(this.config.timeouts.setup); // Wait for API to be ready await this.api.waitForReady(this.config.timeouts.setup); console.log('[Harness] ✓ Setup complete - all services ready'); } /** * Teardown hook - stops Docker services and cleans up * Called once after all tests in a suite */ async afterAll(): Promise { console.log('[Harness] Starting integration test teardown...'); try { await this.database.close(); this.docker.stop(); console.log('[Harness] ✓ Teardown complete'); } catch (error) { console.warn('[Harness] Teardown warning:', error); } } /** * Setup hook - prepares database for each test * Called before each test */ async beforeEach(): Promise { // Truncate all tables to ensure clean state await this.database.truncateAllTables(); // Optionally seed minimal required data // await this.database.seedMinimalData(); } /** * Teardown hook - cleanup after each test * Called after each test */ async afterEach(): Promise { // Clean up any test-specific resources // This can be extended by individual tests } /** * Get database manager */ getDatabase(): DatabaseManager { return this.database; } /** * Get API client */ getApi(): ApiClient { return this.api; } /** * Get Docker manager */ getDocker(): DockerManager { return this.docker; } /** * Get data factory */ getFactory(): DataFactory { return this.factory; } /** * Execute database transaction with automatic rollback * Useful for tests that need to verify transaction behavior */ async withTransaction(callback: (db: DatabaseManager) => Promise): Promise { await this.database.begin(); try { const result = await callback(this.database); await this.database.rollback(); // Always rollback in tests return result; } catch (error) { await this.database.rollback(); throw error; } } /** * Helper to verify constraint violations */ async expectConstraintViolation( operation: () => Promise, expectedConstraint?: string ): Promise { try { await operation(); throw new Error('Expected constraint violation but operation succeeded'); } catch (error) { // Check if it's a constraint violation const message = error instanceof Error ? error.message : String(error); const isConstraintError = message.includes('constraint') || message.includes('23505') || // Unique violation message.includes('23503') || // Foreign key violation message.includes('23514'); // Check violation if (!isConstraintError) { throw new Error(`Expected constraint violation but got: ${message}`); } if (expectedConstraint && !message.includes(expectedConstraint)) { throw new Error(`Expected constraint '${expectedConstraint}' but got: ${message}`); } } } } // Default configuration for docker-compose.test.yml export const DEFAULT_TEST_CONFIG: IntegrationTestConfig = { api: { baseUrl: 'http://localhost:3101', port: 3101, }, database: { host: 'localhost', port: 5433, database: 'gridpilot_test', user: 'gridpilot_test_user', password: 'gridpilot_test_pass', }, timeouts: { setup: 120000, teardown: 30000, test: 60000, }, }; /** * Create a test harness with default configuration */ export function createTestHarness(config?: Partial): IntegrationTestHarness { const mergedConfig = { ...DEFAULT_TEST_CONFIG, ...config, api: { ...DEFAULT_TEST_CONFIG.api, ...config?.api }, database: { ...DEFAULT_TEST_CONFIG.database, ...config?.database }, timeouts: { ...DEFAULT_TEST_CONFIG.timeouts, ...config?.timeouts }, }; return new IntegrationTestHarness(mergedConfig); }