feat(#357): Add OpenBao to Docker Compose with turnkey setup
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Implements secure credential storage using OpenBao Transit encryption. Features: - Auto-initialization on first run (1-of-1 Shamir key for dev) - Auto-unseal on container restart with verification and retry logic - Transit secrets engine with 4 named encryption keys - AppRole authentication with Transit-only policy - Localhost-only API binding for security - Comprehensive integration test suite (22 tests, all passing) Security: - API bound to 127.0.0.1 (localhost only, no external access) - Unseal verification with 3-attempt retry logic - Sanitized error messages in tests (no secret leakage) - Volume-based secret reading (doesn't require running container) Files: - docker/openbao/config.hcl: Server configuration - docker/openbao/init.sh: Auto-init/unseal script - docker/docker-compose.yml: OpenBao and init services - tests/integration/openbao.test.ts: Full test coverage - .env.example: OpenBao configuration variables Closes #357 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -8,7 +8,7 @@ const execAsync = promisify(exec);
|
||||
* Docker Stack Integration Tests
|
||||
* Tests the full Docker Compose stack deployment
|
||||
*/
|
||||
describe('Docker Stack Integration Tests', () => {
|
||||
describe("Docker Stack Integration Tests", () => {
|
||||
const TIMEOUT = 120000; // 2 minutes for Docker operations
|
||||
const HEALTH_CHECK_RETRIES = 30;
|
||||
const HEALTH_CHECK_INTERVAL = 2000;
|
||||
@@ -28,19 +28,14 @@ describe('Docker Stack Integration Tests', () => {
|
||||
*/
|
||||
async function waitForService(
|
||||
serviceName: string,
|
||||
retries = HEALTH_CHECK_RETRIES,
|
||||
retries = HEALTH_CHECK_RETRIES
|
||||
): Promise<boolean> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`docker compose ps --format json ${serviceName}`,
|
||||
);
|
||||
const { stdout } = await execAsync(`docker compose ps --format json ${serviceName}`);
|
||||
const serviceInfo = JSON.parse(stdout);
|
||||
|
||||
if (
|
||||
serviceInfo.Health === 'healthy' ||
|
||||
serviceInfo.State === 'running'
|
||||
) {
|
||||
if (serviceInfo.Health === "healthy" || serviceInfo.State === "running") {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -64,190 +59,186 @@ describe('Docker Stack Integration Tests', () => {
|
||||
}
|
||||
}
|
||||
|
||||
describe('Core Services', () => {
|
||||
describe("Core Services", () => {
|
||||
beforeAll(async () => {
|
||||
// Ensure clean state
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
it(
|
||||
'should start PostgreSQL with health check',
|
||||
"should start PostgreSQL with health check",
|
||||
async () => {
|
||||
// Start only PostgreSQL
|
||||
await dockerCompose('up -d postgres');
|
||||
await dockerCompose("up -d postgres");
|
||||
|
||||
// Wait for service to be healthy
|
||||
const isHealthy = await waitForService('postgres');
|
||||
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');
|
||||
const ps = await dockerCompose("ps postgres");
|
||||
expect(ps).toContain("mosaic-postgres");
|
||||
expect(ps).toContain("Up");
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should start Valkey with health check',
|
||||
"should start Valkey with health check",
|
||||
async () => {
|
||||
// Start Valkey
|
||||
await dockerCompose('up -d valkey');
|
||||
await dockerCompose("up -d valkey");
|
||||
|
||||
// Wait for service to be healthy
|
||||
const isHealthy = await waitForService('valkey');
|
||||
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');
|
||||
const ps = await dockerCompose("ps valkey");
|
||||
expect(ps).toContain("mosaic-valkey");
|
||||
expect(ps).toContain("Up");
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should have proper network configuration',
|
||||
"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');
|
||||
const { stdout } = await execAsync("docker network ls");
|
||||
expect(stdout).toContain("mosaic-internal");
|
||||
expect(stdout).toContain("mosaic-public");
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should have proper volume configuration',
|
||||
"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');
|
||||
const { stdout } = await execAsync("docker volume ls");
|
||||
expect(stdout).toContain("mosaic-postgres-data");
|
||||
expect(stdout).toContain("mosaic-valkey-data");
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('Application Services', () => {
|
||||
describe("Application Services", () => {
|
||||
beforeAll(async () => {
|
||||
// Start core services first
|
||||
await dockerCompose('up -d postgres valkey');
|
||||
await waitForService('postgres');
|
||||
await waitForService('valkey');
|
||||
await dockerCompose("up -d postgres valkey");
|
||||
await waitForService("postgres");
|
||||
await waitForService("valkey");
|
||||
}, TIMEOUT);
|
||||
|
||||
afterAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
it(
|
||||
'should start API service with dependencies',
|
||||
"should start API service with dependencies",
|
||||
async () => {
|
||||
// Start API
|
||||
await dockerCompose('up -d api');
|
||||
await dockerCompose("up -d api");
|
||||
|
||||
// Wait for API to be healthy
|
||||
const isHealthy = await waitForService('api');
|
||||
const isHealthy = await waitForService("api");
|
||||
expect(isHealthy).toBe(true);
|
||||
|
||||
// Verify API is accessible
|
||||
const apiHealthy = await checkHttpEndpoint('http://localhost:3001/health');
|
||||
const apiHealthy = await checkHttpEndpoint("http://localhost:3001/health");
|
||||
expect(apiHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should start Web service with dependencies',
|
||||
"should start Web service with dependencies",
|
||||
async () => {
|
||||
// Ensure API is running
|
||||
await dockerCompose('up -d api');
|
||||
await waitForService('api');
|
||||
await dockerCompose("up -d api");
|
||||
await waitForService("api");
|
||||
|
||||
// Start Web
|
||||
await dockerCompose('up -d web');
|
||||
await dockerCompose("up -d web");
|
||||
|
||||
// Wait for Web to be healthy
|
||||
const isHealthy = await waitForService('web');
|
||||
const isHealthy = await waitForService("web");
|
||||
expect(isHealthy).toBe(true);
|
||||
|
||||
// Verify Web is accessible
|
||||
const webHealthy = await checkHttpEndpoint('http://localhost:3000');
|
||||
const webHealthy = await checkHttpEndpoint("http://localhost:3000");
|
||||
expect(webHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('Optional Services (Profiles)', () => {
|
||||
describe("Optional Services (Profiles)", () => {
|
||||
afterAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
it(
|
||||
'should start Authentik services with profile',
|
||||
"should start Authentik services with profile",
|
||||
async () => {
|
||||
// Start Authentik services using profile
|
||||
await dockerCompose('--profile authentik up -d');
|
||||
await dockerCompose("--profile authentik up -d");
|
||||
|
||||
// Wait for Authentik dependencies
|
||||
await waitForService('authentik-postgres');
|
||||
await waitForService('authentik-redis');
|
||||
await waitForService("authentik-postgres");
|
||||
await waitForService("authentik-redis");
|
||||
|
||||
// Verify Authentik server starts
|
||||
const isHealthy = await waitForService('authentik-server');
|
||||
const isHealthy = await waitForService("authentik-server");
|
||||
expect(isHealthy).toBe(true);
|
||||
|
||||
// Verify Authentik is accessible
|
||||
const authentikHealthy = await checkHttpEndpoint(
|
||||
'http://localhost:9000/-/health/live/',
|
||||
);
|
||||
const authentikHealthy = await checkHttpEndpoint("http://localhost:9000/-/health/live/");
|
||||
expect(authentikHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should start Ollama service with profile',
|
||||
"should start Ollama service with profile",
|
||||
async () => {
|
||||
// Start Ollama using profile
|
||||
await dockerCompose('--profile ollama up -d ollama');
|
||||
await dockerCompose("--profile ollama up -d ollama");
|
||||
|
||||
// Wait for Ollama to start
|
||||
const isHealthy = await waitForService('ollama');
|
||||
const isHealthy = await waitForService("ollama");
|
||||
expect(isHealthy).toBe(true);
|
||||
|
||||
// Verify Ollama is accessible
|
||||
const ollamaHealthy = await checkHttpEndpoint(
|
||||
'http://localhost:11434/api/tags',
|
||||
);
|
||||
const ollamaHealthy = await checkHttpEndpoint("http://localhost:11434/api/tags");
|
||||
expect(ollamaHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('Full Stack', () => {
|
||||
describe("Full Stack", () => {
|
||||
it(
|
||||
'should start entire stack with all services',
|
||||
"should start entire stack with all services",
|
||||
async () => {
|
||||
// Clean slate
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
|
||||
// Start all core services (without profiles)
|
||||
await dockerCompose('up -d');
|
||||
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');
|
||||
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);
|
||||
@@ -255,255 +246,246 @@ describe('Docker Stack Integration Tests', () => {
|
||||
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');
|
||||
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,
|
||||
TIMEOUT * 2
|
||||
);
|
||||
});
|
||||
|
||||
describe('Service Dependencies', () => {
|
||||
describe("Service Dependencies", () => {
|
||||
beforeAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
afterAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
it(
|
||||
'should enforce dependency order',
|
||||
"should enforce dependency order",
|
||||
async () => {
|
||||
// Start web service (should auto-start dependencies)
|
||||
await dockerCompose('up -d web');
|
||||
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');
|
||||
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,
|
||||
TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('Container Labels', () => {
|
||||
describe("Container Labels", () => {
|
||||
beforeAll(async () => {
|
||||
await dockerCompose('up -d postgres valkey');
|
||||
await dockerCompose("up -d postgres valkey");
|
||||
}, TIMEOUT);
|
||||
|
||||
afterAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
it('should have proper labels on containers', async () => {
|
||||
it("should have proper labels on containers", async () => {
|
||||
// Check PostgreSQL labels
|
||||
const { stdout: pgLabels } = await execAsync(
|
||||
'docker inspect mosaic-postgres --format "{{json .Config.Labels}}"',
|
||||
'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();
|
||||
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}}"',
|
||||
'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();
|
||||
expect(valkeyLabelsObj["com.mosaic.service"]).toBe("cache");
|
||||
expect(valkeyLabelsObj["com.mosaic.description"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Failure Scenarios', () => {
|
||||
describe("Failure Scenarios", () => {
|
||||
const TIMEOUT = 120000;
|
||||
|
||||
beforeAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
afterAll(async () => {
|
||||
await dockerCompose('down -v').catch(() => {});
|
||||
await dockerCompose("down -v").catch(() => {});
|
||||
}, TIMEOUT);
|
||||
|
||||
it(
|
||||
'should handle service restart after crash',
|
||||
"should handle service restart after crash",
|
||||
async () => {
|
||||
await dockerCompose('up -d postgres');
|
||||
await waitForService('postgres');
|
||||
await dockerCompose("up -d postgres");
|
||||
await waitForService("postgres");
|
||||
|
||||
const { stdout: containerName } = await execAsync(
|
||||
'docker compose ps -q 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');
|
||||
await dockerCompose("up -d postgres");
|
||||
|
||||
const isHealthy = await waitForService('postgres');
|
||||
const isHealthy = await waitForService("postgres");
|
||||
expect(isHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should handle port conflict gracefully',
|
||||
"should handle port conflict gracefully",
|
||||
async () => {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
'docker run -d -p 5432:5432 --name port-blocker postgres:17-alpine',
|
||||
"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');
|
||||
await dockerCompose("up -d postgres").catch((error) => {
|
||||
expect(error.message).toContain("port is already allocated");
|
||||
});
|
||||
} finally {
|
||||
await execAsync('docker rm -f port-blocker').catch(() => {});
|
||||
await execAsync("docker rm -f port-blocker").catch(() => {});
|
||||
}
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should handle invalid volume mount paths',
|
||||
"should handle invalid volume mount paths",
|
||||
async () => {
|
||||
try {
|
||||
await execAsync(
|
||||
'docker run -d --name invalid-mount -v /nonexistent/path:/data postgres:17-alpine',
|
||||
"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}}"',
|
||||
'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(() => {});
|
||||
await execAsync("docker rm -f invalid-mount").catch(() => {});
|
||||
}
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should handle network partition scenarios',
|
||||
"should handle network partition scenarios",
|
||||
async () => {
|
||||
await dockerCompose('up -d postgres valkey');
|
||||
await waitForService('postgres');
|
||||
await waitForService('valkey');
|
||||
await dockerCompose("up -d postgres valkey");
|
||||
await waitForService("postgres");
|
||||
await waitForService("valkey");
|
||||
|
||||
const { stdout: postgresContainer } = await execAsync(
|
||||
'docker compose ps -q postgres',
|
||||
);
|
||||
const { stdout: postgresContainer } = await execAsync("docker compose ps -q postgres");
|
||||
const trimmedPostgres = postgresContainer.trim();
|
||||
|
||||
await execAsync(
|
||||
`docker network disconnect mosaic-internal ${trimmedPostgres}`,
|
||||
);
|
||||
await execAsync(`docker network disconnect mosaic-internal ${trimmedPostgres}`);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
await execAsync(
|
||||
`docker network connect mosaic-internal ${trimmedPostgres}`,
|
||||
);
|
||||
await execAsync(`docker network connect mosaic-internal ${trimmedPostgres}`);
|
||||
|
||||
const isHealthy = await waitForService('postgres');
|
||||
const isHealthy = await waitForService("postgres");
|
||||
expect(isHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should handle container out of memory scenario',
|
||||
"should handle container out of memory scenario",
|
||||
async () => {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
'docker run -d --name mem-limited --memory="10m" postgres:17-alpine',
|
||||
'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}}"',
|
||||
'docker inspect mem-limited --format "{{.State.Status}}"'
|
||||
);
|
||||
|
||||
expect(['exited', 'running']).toContain(status.trim());
|
||||
expect(["exited", "running"]).toContain(status.trim());
|
||||
} finally {
|
||||
await execAsync('docker rm -f mem-limited').catch(() => {});
|
||||
await execAsync("docker rm -f mem-limited").catch(() => {});
|
||||
}
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should handle disk space issues gracefully',
|
||||
"should handle disk space issues gracefully",
|
||||
async () => {
|
||||
await dockerCompose('up -d postgres');
|
||||
await waitForService('postgres');
|
||||
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');
|
||||
const { stdout } = await execAsync("docker system df");
|
||||
expect(stdout).toContain("Images");
|
||||
expect(stdout).toContain("Containers");
|
||||
expect(stdout).toContain("Volumes");
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should handle service dependency failures',
|
||||
"should handle service dependency failures",
|
||||
async () => {
|
||||
await dockerCompose('up -d postgres').catch(() => {});
|
||||
await dockerCompose("up -d postgres").catch(() => {});
|
||||
|
||||
const { stdout: postgresContainer } = await execAsync(
|
||||
'docker compose ps -q postgres',
|
||||
);
|
||||
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');
|
||||
await dockerCompose("up -d api");
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
|
||||
await dockerCompose('start postgres').catch(() => {});
|
||||
await waitForService('postgres');
|
||||
await dockerCompose("start postgres").catch(() => {});
|
||||
await waitForService("postgres");
|
||||
|
||||
await dockerCompose('up -d api');
|
||||
const apiHealthy = await waitForService('api');
|
||||
await dockerCompose("up -d api");
|
||||
const apiHealthy = await waitForService("api");
|
||||
expect(apiHealthy).toBe(true);
|
||||
}
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should recover from corrupted volume data',
|
||||
"should recover from corrupted volume data",
|
||||
async () => {
|
||||
await dockerCompose('up -d postgres');
|
||||
await waitForService('postgres');
|
||||
await dockerCompose("up -d postgres");
|
||||
await waitForService("postgres");
|
||||
|
||||
await dockerCompose('down');
|
||||
await dockerCompose("down");
|
||||
|
||||
await execAsync('docker volume rm mosaic-postgres-data').catch(() => {});
|
||||
await execAsync("docker volume rm mosaic-postgres-data").catch(() => {});
|
||||
|
||||
await dockerCompose('up -d postgres');
|
||||
const isHealthy = await waitForService('postgres');
|
||||
await dockerCompose("up -d postgres");
|
||||
const isHealthy = await waitForService("postgres");
|
||||
expect(isHealthy).toBe(true);
|
||||
},
|
||||
TIMEOUT,
|
||||
TIMEOUT
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user