import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); /** * Docker Stack Integration Tests * Tests the full Docker Compose stack deployment */ describe('Docker Stack Integration Tests', () => { const TIMEOUT = 120000; // 2 minutes for Docker operations const HEALTH_CHECK_RETRIES = 30; const HEALTH_CHECK_INTERVAL = 2000; /** * Helper function to execute Docker Compose commands */ async function dockerCompose(command: string): Promise { const { stdout } = await execAsync(`docker compose ${command}`, { cwd: process.cwd(), }); return stdout; } /** * Helper function to check if a service is healthy */ async function waitForService( serviceName: string, retries = HEALTH_CHECK_RETRIES, ): Promise { for (let i = 0; i < retries; i++) { try { const { stdout } = await execAsync( `docker compose ps --format json ${serviceName}`, ); const serviceInfo = JSON.parse(stdout); if ( serviceInfo.Health === 'healthy' || serviceInfo.State === 'running' ) { return true; } } catch (error) { // Service not ready yet } await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL)); } return false; } /** * Helper function to check HTTP endpoint */ async function checkHttpEndpoint(url: string): Promise { try { const response = await fetch(url); return response.ok; } catch (error) { return false; } } describe('Core Services', () => { beforeAll(async () => { // Ensure clean state await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); afterAll(async () => { // Cleanup await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); it( 'should start PostgreSQL with health check', async () => { // Start only PostgreSQL await dockerCompose('up -d postgres'); // Wait for service to be healthy const isHealthy = await waitForService('postgres'); expect(isHealthy).toBe(true); // Verify container is running const ps = await dockerCompose('ps postgres'); expect(ps).toContain('mosaic-postgres'); expect(ps).toContain('Up'); }, TIMEOUT, ); it( 'should start Valkey with health check', async () => { // Start Valkey await dockerCompose('up -d valkey'); // Wait for service to be healthy const isHealthy = await waitForService('valkey'); expect(isHealthy).toBe(true); // Verify container is running const ps = await dockerCompose('ps valkey'); expect(ps).toContain('mosaic-valkey'); expect(ps).toContain('Up'); }, TIMEOUT, ); it( 'should have proper network configuration', async () => { // Check if mosaic-internal network exists const { stdout } = await execAsync('docker network ls'); expect(stdout).toContain('mosaic-internal'); expect(stdout).toContain('mosaic-public'); }, TIMEOUT, ); it( 'should have proper volume configuration', async () => { // Check if volumes are created const { stdout } = await execAsync('docker volume ls'); expect(stdout).toContain('mosaic-postgres-data'); expect(stdout).toContain('mosaic-valkey-data'); }, TIMEOUT, ); }); describe('Application Services', () => { beforeAll(async () => { // Start core services first await dockerCompose('up -d postgres valkey'); await waitForService('postgres'); await waitForService('valkey'); }, TIMEOUT); afterAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); it( 'should start API service with dependencies', async () => { // Start API await dockerCompose('up -d api'); // Wait for API to be healthy const isHealthy = await waitForService('api'); expect(isHealthy).toBe(true); // Verify API is accessible const apiHealthy = await checkHttpEndpoint('http://localhost:3001/health'); expect(apiHealthy).toBe(true); }, TIMEOUT, ); it( 'should start Web service with dependencies', async () => { // Ensure API is running await dockerCompose('up -d api'); await waitForService('api'); // Start Web await dockerCompose('up -d web'); // Wait for Web to be healthy const isHealthy = await waitForService('web'); expect(isHealthy).toBe(true); // Verify Web is accessible const webHealthy = await checkHttpEndpoint('http://localhost:3000'); expect(webHealthy).toBe(true); }, TIMEOUT, ); }); describe('Optional Services (Profiles)', () => { afterAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); it( 'should start Authentik services with profile', async () => { // Start Authentik services using profile await dockerCompose('--profile authentik up -d'); // Wait for Authentik dependencies await waitForService('authentik-postgres'); await waitForService('authentik-redis'); // Verify Authentik server starts const isHealthy = await waitForService('authentik-server'); expect(isHealthy).toBe(true); // Verify Authentik is accessible const authentikHealthy = await checkHttpEndpoint( 'http://localhost:9000/-/health/live/', ); expect(authentikHealthy).toBe(true); }, TIMEOUT, ); it( 'should start Ollama service with profile', async () => { // Start Ollama using profile await dockerCompose('--profile ollama up -d ollama'); // Wait for Ollama to start const isHealthy = await waitForService('ollama'); expect(isHealthy).toBe(true); // Verify Ollama is accessible const ollamaHealthy = await checkHttpEndpoint( 'http://localhost:11434/api/tags', ); expect(ollamaHealthy).toBe(true); }, TIMEOUT, ); }); describe('Full Stack', () => { it( 'should start entire stack with all services', async () => { // Clean slate await dockerCompose('down -v').catch(() => {}); // Start all core services (without profiles) await dockerCompose('up -d'); // Wait for all core services const postgresHealthy = await waitForService('postgres'); const valkeyHealthy = await waitForService('valkey'); const apiHealthy = await waitForService('api'); const webHealthy = await waitForService('web'); expect(postgresHealthy).toBe(true); expect(valkeyHealthy).toBe(true); expect(apiHealthy).toBe(true); expect(webHealthy).toBe(true); // Verify all services are running const ps = await dockerCompose('ps'); expect(ps).toContain('mosaic-postgres'); expect(ps).toContain('mosaic-valkey'); expect(ps).toContain('mosaic-api'); expect(ps).toContain('mosaic-web'); }, TIMEOUT * 2, ); }); describe('Service Dependencies', () => { beforeAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); afterAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); it( 'should enforce dependency order', async () => { // Start web service (should auto-start dependencies) await dockerCompose('up -d web'); // Wait a bit for services to start await new Promise((resolve) => setTimeout(resolve, 10000)); // Verify all dependencies are running const ps = await dockerCompose('ps'); expect(ps).toContain('mosaic-postgres'); expect(ps).toContain('mosaic-valkey'); expect(ps).toContain('mosaic-api'); expect(ps).toContain('mosaic-web'); }, TIMEOUT, ); }); describe('Container Labels', () => { beforeAll(async () => { await dockerCompose('up -d postgres valkey'); }, TIMEOUT); afterAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); it('should have proper labels on containers', async () => { // Check PostgreSQL labels const { stdout: pgLabels } = await execAsync( 'docker inspect mosaic-postgres --format "{{json .Config.Labels}}"', ); const pgLabelsObj = JSON.parse(pgLabels); expect(pgLabelsObj['com.mosaic.service']).toBe('database'); expect(pgLabelsObj['com.mosaic.description']).toBeDefined(); // Check Valkey labels const { stdout: valkeyLabels } = await execAsync( 'docker inspect mosaic-valkey --format "{{json .Config.Labels}}"', ); const valkeyLabelsObj = JSON.parse(valkeyLabels); expect(valkeyLabelsObj['com.mosaic.service']).toBe('cache'); expect(valkeyLabelsObj['com.mosaic.description']).toBeDefined(); }); describe('Failure Scenarios', () => { const TIMEOUT = 120000; beforeAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); afterAll(async () => { await dockerCompose('down -v').catch(() => {}); }, TIMEOUT); it( 'should handle service restart after crash', async () => { await dockerCompose('up -d postgres'); await waitForService('postgres'); const { stdout: containerName } = await execAsync( 'docker compose ps -q postgres', ); const trimmedName = containerName.trim(); await execAsync(`docker kill ${trimmedName}`); await new Promise((resolve) => setTimeout(resolve, 3000)); await dockerCompose('up -d postgres'); const isHealthy = await waitForService('postgres'); expect(isHealthy).toBe(true); }, TIMEOUT, ); it( 'should handle port conflict gracefully', async () => { try { const { stdout } = await execAsync( 'docker run -d -p 5432:5432 --name port-blocker postgres:17-alpine', ); await dockerCompose('up -d postgres').catch((error) => { expect(error.message).toContain('port is already allocated'); }); } finally { await execAsync('docker rm -f port-blocker').catch(() => {}); } }, TIMEOUT, ); it( 'should handle invalid volume mount paths', async () => { try { await execAsync( 'docker run -d --name invalid-mount -v /nonexistent/path:/data postgres:17-alpine', ); const { stdout } = await execAsync( 'docker ps -a -f name=invalid-mount --format "{{.Status}}"', ); expect(stdout).toBeDefined(); } catch (error) { expect(error).toBeDefined(); } finally { await execAsync('docker rm -f invalid-mount').catch(() => {}); } }, TIMEOUT, ); it( 'should handle network partition scenarios', async () => { await dockerCompose('up -d postgres valkey'); await waitForService('postgres'); await waitForService('valkey'); const { stdout: postgresContainer } = await execAsync( 'docker compose ps -q postgres', ); const trimmedPostgres = postgresContainer.trim(); await execAsync( `docker network disconnect mosaic-internal ${trimmedPostgres}`, ); await new Promise((resolve) => setTimeout(resolve, 2000)); await execAsync( `docker network connect mosaic-internal ${trimmedPostgres}`, ); const isHealthy = await waitForService('postgres'); expect(isHealthy).toBe(true); }, TIMEOUT, ); it( 'should handle container out of memory scenario', async () => { try { const { stdout } = await execAsync( 'docker run -d --name mem-limited --memory="10m" postgres:17-alpine', ); await new Promise((resolve) => setTimeout(resolve, 5000)); const { stdout: status } = await execAsync( 'docker inspect mem-limited --format "{{.State.Status}}"', ); expect(['exited', 'running']).toContain(status.trim()); } finally { await execAsync('docker rm -f mem-limited').catch(() => {}); } }, TIMEOUT, ); it( 'should handle disk space issues gracefully', async () => { await dockerCompose('up -d postgres'); await waitForService('postgres'); const { stdout } = await execAsync('docker system df'); expect(stdout).toContain('Images'); expect(stdout).toContain('Containers'); expect(stdout).toContain('Volumes'); }, TIMEOUT, ); it( 'should handle service dependency failures', async () => { await dockerCompose('up -d postgres').catch(() => {}); const { stdout: postgresContainer } = await execAsync( 'docker compose ps -q postgres', ); const trimmedPostgres = postgresContainer.trim(); if (trimmedPostgres) { await execAsync(`docker stop ${trimmedPostgres}`); try { await dockerCompose('up -d api'); } catch (error) { expect(error).toBeDefined(); } await dockerCompose('start postgres').catch(() => {}); await waitForService('postgres'); await dockerCompose('up -d api'); const apiHealthy = await waitForService('api'); expect(apiHealthy).toBe(true); } }, TIMEOUT, ); it( 'should recover from corrupted volume data', async () => { await dockerCompose('up -d postgres'); await waitForService('postgres'); await dockerCompose('down'); await execAsync('docker volume rm mosaic-postgres-data').catch(() => {}); await dockerCompose('up -d postgres'); const isHealthy = await waitForService('postgres'); expect(isHealthy).toBe(true); }, TIMEOUT, ); }); });