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 ); }); });