feat(#37-41): Add domains, ideas, relationships, agents, widgets schema
Schema additions for issues #37-41: New models: - Domain (#37): Life domains (work, marriage, homelab, etc.) - Idea (#38): Brain dumps with pgvector embeddings - Relationship (#39): Generic entity linking (blocks, depends_on) - Agent (#40): ClawdBot agent tracking with metrics - AgentSession (#40): Conversation session tracking - WidgetDefinition (#41): HUD widget registry - UserLayout (#41): Per-user dashboard configuration Updated models: - Task, Event, Project: Added domainId foreign key - User, Workspace: Added new relations New enums: - IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED - RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc. - AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED - EntityType: Added IDEA, DOMAIN Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
509
tests/integration/docker-stack.test.ts
Normal file
509
tests/integration/docker-stack.test.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
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<string> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
80
tests/integration/docker/README.md
Normal file
80
tests/integration/docker/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Docker Integration Tests
|
||||
|
||||
This directory contains integration tests for Docker Compose deployment modes.
|
||||
|
||||
## Traefik Integration Tests
|
||||
|
||||
Tests for Traefik reverse proxy integration in bundled, upstream, and none modes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- `jq` for JSON parsing: `apt install jq` or `brew install jq`
|
||||
- `curl` for HTTP testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./traefik.test.sh all
|
||||
|
||||
# Run specific mode tests
|
||||
./traefik.test.sh bundled
|
||||
./traefik.test.sh upstream
|
||||
./traefik.test.sh none
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
#### Bundled Mode Tests
|
||||
- Traefik container starts with `traefik-bundled` profile
|
||||
- Traefik dashboard is accessible
|
||||
- Traefik API responds correctly
|
||||
- Services have proper Traefik labels
|
||||
- Routes are registered with Traefik
|
||||
|
||||
#### Upstream Mode Tests
|
||||
- Bundled Traefik does not start
|
||||
- Services connect to external Traefik network
|
||||
- Services have labels for external discovery
|
||||
- Correct network configuration
|
||||
|
||||
#### None Mode Tests
|
||||
- No Traefik container starts
|
||||
- Traefik labels are disabled
|
||||
- Direct port access works
|
||||
- Services accessible via published ports
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
These tests can be run in CI/CD pipelines:
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Run Traefik integration tests
|
||||
run: ./tests/integration/docker/traefik.test.sh all
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Test cleanup issues
|
||||
If tests fail and leave containers running:
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yml down -v
|
||||
docker network rm traefik-public-test
|
||||
```
|
||||
|
||||
#### Permission denied
|
||||
Make sure the test script is executable:
|
||||
```bash
|
||||
chmod +x traefik.test.sh
|
||||
```
|
||||
|
||||
#### Port conflicts
|
||||
Ensure ports 8080, 3000, 3001 are available before running tests.
|
||||
390
tests/integration/docker/traefik.test.sh
Executable file
390
tests/integration/docker/traefik.test.sh
Executable file
@@ -0,0 +1,390 @@
|
||||
#!/bin/bash
|
||||
# Integration tests for Traefik deployment modes
|
||||
# Tests bundled, upstream, and none modes
|
||||
#
|
||||
# Usage:
|
||||
# ./traefik.test.sh [bundled|upstream|none|all]
|
||||
#
|
||||
# Requirements:
|
||||
# - Docker and Docker Compose installed
|
||||
# - jq for JSON parsing
|
||||
# - curl for HTTP testing
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test counters
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Test result tracking
|
||||
declare -a FAILED_TESTS=()
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_test() {
|
||||
echo -e "\n${YELLOW}[TEST]${NC} $1"
|
||||
((TESTS_RUN++))
|
||||
}
|
||||
|
||||
log_pass() {
|
||||
echo -e "${GREEN}[PASS]${NC} $1"
|
||||
((TESTS_PASSED++))
|
||||
}
|
||||
|
||||
log_fail() {
|
||||
echo -e "${RED}[FAIL]${NC} $1"
|
||||
((TESTS_FAILED++))
|
||||
FAILED_TESTS+=("$1")
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
log_info "Cleaning up test environment..."
|
||||
docker compose -f docker-compose.test.yml down -v 2>/dev/null || true
|
||||
docker network rm traefik-public-test 2>/dev/null || true
|
||||
rm -f .env.test docker-compose.test.yml
|
||||
}
|
||||
|
||||
# Trap cleanup on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
log_info "Checking prerequisites..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker compose &> /dev/null; then
|
||||
log_error "Docker Compose is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq is not installed (required for JSON parsing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl &> /dev/null; then
|
||||
log_error "curl is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_pass "All prerequisites satisfied"
|
||||
}
|
||||
|
||||
# Test bundled Traefik mode
|
||||
test_bundled_mode() {
|
||||
log_info "=========================================="
|
||||
log_info "Testing Bundled Traefik Mode"
|
||||
log_info "=========================================="
|
||||
|
||||
# Create test environment file
|
||||
cat > .env.test <<EOF
|
||||
TRAEFIK_MODE=bundled
|
||||
MOSAIC_API_DOMAIN=api.mosaic.test
|
||||
MOSAIC_WEB_DOMAIN=mosaic.test
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.test
|
||||
TRAEFIK_DASHBOARD_ENABLED=true
|
||||
TRAEFIK_TLS_ENABLED=false
|
||||
TRAEFIK_ACME_EMAIL=test@example.com
|
||||
POSTGRES_PASSWORD=test_password
|
||||
AUTHENTIK_SECRET_KEY=test_secret_key_minimum_50_characters_long_for_testing
|
||||
AUTHENTIK_POSTGRES_PASSWORD=test_auth_password
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=test_admin
|
||||
JWT_SECRET=test_jwt_secret_minimum_32_chars
|
||||
EOF
|
||||
|
||||
# Copy docker-compose.yml to test version
|
||||
cp docker-compose.yml docker-compose.test.yml
|
||||
|
||||
log_test "Starting services with bundled Traefik profile"
|
||||
if docker compose -f docker-compose.test.yml --env-file .env.test --profile traefik-bundled up -d; then
|
||||
log_pass "Services started successfully"
|
||||
else
|
||||
log_fail "Failed to start services"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 10 # Wait for services to initialize
|
||||
|
||||
log_test "Verifying Traefik container is running"
|
||||
if docker ps --filter "name=mosaic-traefik" --format "{{.Names}}" | grep -q "mosaic-traefik"; then
|
||||
log_pass "Traefik container is running"
|
||||
else
|
||||
log_fail "Traefik container not found"
|
||||
fi
|
||||
|
||||
log_test "Verifying Traefik dashboard is accessible"
|
||||
if curl -sf http://localhost:8080/dashboard/ > /dev/null; then
|
||||
log_pass "Traefik dashboard is accessible"
|
||||
else
|
||||
log_fail "Traefik dashboard not accessible"
|
||||
fi
|
||||
|
||||
log_test "Verifying Traefik API endpoint"
|
||||
if curl -sf http://localhost:8080/api/overview | jq -e '.http.routers' > /dev/null; then
|
||||
log_pass "Traefik API is responding"
|
||||
else
|
||||
log_fail "Traefik API not responding correctly"
|
||||
fi
|
||||
|
||||
log_test "Verifying API service has Traefik labels"
|
||||
LABELS=$(docker inspect mosaic-api --format='{{json .Config.Labels}}')
|
||||
if echo "$LABELS" | jq -e '."traefik.enable"' > /dev/null; then
|
||||
log_pass "API service has Traefik labels"
|
||||
else
|
||||
log_fail "API service missing Traefik labels"
|
||||
fi
|
||||
|
||||
log_test "Verifying Web service has Traefik labels"
|
||||
LABELS=$(docker inspect mosaic-web --format='{{json .Config.Labels}}')
|
||||
if echo "$LABELS" | jq -e '."traefik.enable"' > /dev/null; then
|
||||
log_pass "Web service has Traefik labels"
|
||||
else
|
||||
log_fail "Web service missing Traefik labels"
|
||||
fi
|
||||
|
||||
log_test "Checking Traefik routes configuration"
|
||||
ROUTES=$(curl -sf http://localhost:8080/api/http/routers | jq -r 'keys[]')
|
||||
if echo "$ROUTES" | grep -q "mosaic-api"; then
|
||||
log_pass "API route registered with Traefik"
|
||||
else
|
||||
log_fail "API route not found in Traefik"
|
||||
fi
|
||||
|
||||
if echo "$ROUTES" | grep -q "mosaic-web"; then
|
||||
log_pass "Web route registered with Traefik"
|
||||
else
|
||||
log_fail "Web route not found in Traefik"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
docker compose -f docker-compose.test.yml --env-file .env.test down -v
|
||||
}
|
||||
|
||||
# Test upstream Traefik mode
|
||||
test_upstream_mode() {
|
||||
log_info "=========================================="
|
||||
log_info "Testing Upstream Traefik Mode"
|
||||
log_info "=========================================="
|
||||
|
||||
# Create external network for upstream Traefik
|
||||
log_test "Creating external Traefik network"
|
||||
if docker network create traefik-public-test 2>/dev/null; then
|
||||
log_pass "External network created"
|
||||
else
|
||||
log_warn "Network already exists or creation failed"
|
||||
fi
|
||||
|
||||
# Create test environment file
|
||||
cat > .env.test <<EOF
|
||||
TRAEFIK_MODE=upstream
|
||||
MOSAIC_API_DOMAIN=api.mosaic.test
|
||||
MOSAIC_WEB_DOMAIN=mosaic.test
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.test
|
||||
TRAEFIK_NETWORK=traefik-public-test
|
||||
POSTGRES_PASSWORD=test_password
|
||||
AUTHENTIK_SECRET_KEY=test_secret_key_minimum_50_characters_long_for_testing
|
||||
AUTHENTIK_POSTGRES_PASSWORD=test_auth_password
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=test_admin
|
||||
JWT_SECRET=test_jwt_secret_minimum_32_chars
|
||||
EOF
|
||||
|
||||
# Copy docker-compose.yml to test version
|
||||
cp docker-compose.yml docker-compose.test.yml
|
||||
|
||||
log_test "Starting services in upstream mode (no bundled Traefik)"
|
||||
if docker compose -f docker-compose.test.yml --env-file .env.test up -d; then
|
||||
log_pass "Services started successfully"
|
||||
else
|
||||
log_fail "Failed to start services"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 10 # Wait for services to initialize
|
||||
|
||||
log_test "Verifying Traefik container is NOT running"
|
||||
if ! docker ps --filter "name=mosaic-traefik" --format "{{.Names}}" | grep -q "mosaic-traefik"; then
|
||||
log_pass "Bundled Traefik correctly not started"
|
||||
else
|
||||
log_fail "Bundled Traefik should not be running in upstream mode"
|
||||
fi
|
||||
|
||||
log_test "Verifying API service is connected to external network"
|
||||
NETWORKS=$(docker inspect mosaic-api --format='{{json .NetworkSettings.Networks}}' | jq -r 'keys[]')
|
||||
if echo "$NETWORKS" | grep -q "traefik-public-test"; then
|
||||
log_pass "API service connected to external Traefik network"
|
||||
else
|
||||
log_fail "API service not connected to external Traefik network"
|
||||
fi
|
||||
|
||||
log_test "Verifying Web service is connected to external network"
|
||||
NETWORKS=$(docker inspect mosaic-web --format='{{json .NetworkSettings.Networks}}' | jq -r 'keys[]')
|
||||
if echo "$NETWORKS" | grep -q "traefik-public-test"; then
|
||||
log_pass "Web service connected to external Traefik network"
|
||||
else
|
||||
log_fail "Web service not connected to external Traefik network"
|
||||
fi
|
||||
|
||||
log_test "Verifying API service has correct Traefik labels for upstream"
|
||||
LABELS=$(docker inspect mosaic-api --format='{{json .Config.Labels}}')
|
||||
if echo "$LABELS" | jq -e '."traefik.enable" == "true"' > /dev/null && \
|
||||
echo "$LABELS" | jq -e '."traefik.docker.network"' > /dev/null; then
|
||||
log_pass "API service has correct upstream Traefik labels"
|
||||
else
|
||||
log_fail "API service missing or incorrect upstream Traefik labels"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
docker compose -f docker-compose.test.yml --env-file .env.test down -v
|
||||
docker network rm traefik-public-test 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Test none mode (direct port exposure)
|
||||
test_none_mode() {
|
||||
log_info "=========================================="
|
||||
log_info "Testing None Mode (Direct Ports)"
|
||||
log_info "=========================================="
|
||||
|
||||
# Create test environment file
|
||||
cat > .env.test <<EOF
|
||||
TRAEFIK_MODE=none
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
POSTGRES_PASSWORD=test_password
|
||||
AUTHENTIK_SECRET_KEY=test_secret_key_minimum_50_characters_long_for_testing
|
||||
AUTHENTIK_POSTGRES_PASSWORD=test_auth_password
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=test_admin
|
||||
JWT_SECRET=test_jwt_secret_minimum_32_chars
|
||||
EOF
|
||||
|
||||
# Copy docker-compose.yml to test version
|
||||
cp docker-compose.yml docker-compose.test.yml
|
||||
|
||||
log_test "Starting services in none mode (no Traefik)"
|
||||
if docker compose -f docker-compose.test.yml --env-file .env.test up -d; then
|
||||
log_pass "Services started successfully"
|
||||
else
|
||||
log_fail "Failed to start services"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 10 # Wait for services to initialize
|
||||
|
||||
log_test "Verifying Traefik container is NOT running"
|
||||
if ! docker ps --filter "name=mosaic-traefik" --format "{{.Names}}" | grep -q "mosaic-traefik"; then
|
||||
log_pass "Traefik correctly not started in none mode"
|
||||
else
|
||||
log_fail "Traefik should not be running in none mode"
|
||||
fi
|
||||
|
||||
log_test "Verifying API service Traefik labels are disabled"
|
||||
LABELS=$(docker inspect mosaic-api --format='{{json .Config.Labels}}')
|
||||
if echo "$LABELS" | jq -e '."traefik.enable" == "false"' > /dev/null; then
|
||||
log_pass "API service Traefik labels correctly disabled"
|
||||
else
|
||||
log_fail "API service Traefik labels should be disabled"
|
||||
fi
|
||||
|
||||
log_test "Verifying direct port access to API"
|
||||
MAX_RETRIES=30
|
||||
RETRY_COUNT=0
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||
if curl -sf http://localhost:3001/health > /dev/null 2>&1; then
|
||||
log_pass "API accessible via direct port 3001"
|
||||
break
|
||||
fi
|
||||
((RETRY_COUNT++))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
|
||||
log_fail "API not accessible via direct port 3001"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
docker compose -f docker-compose.test.yml --env-file .env.test down -v
|
||||
}
|
||||
|
||||
# Print test summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
log_info "=========================================="
|
||||
log_info "Test Summary"
|
||||
log_info "=========================================="
|
||||
echo "Total tests run: $TESTS_RUN"
|
||||
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||
|
||||
if [ $TESTS_FAILED -gt 0 ]; then
|
||||
echo ""
|
||||
log_error "Failed tests:"
|
||||
for test in "${FAILED_TESTS[@]}"; do
|
||||
echo " - $test"
|
||||
done
|
||||
echo ""
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
log_pass "All tests passed!"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
TEST_MODE="${1:-all}"
|
||||
|
||||
log_info "Mosaic Stack - Traefik Integration Tests"
|
||||
log_info "Test mode: $TEST_MODE"
|
||||
echo ""
|
||||
|
||||
check_prerequisites
|
||||
|
||||
case "$TEST_MODE" in
|
||||
bundled)
|
||||
test_bundled_mode
|
||||
;;
|
||||
upstream)
|
||||
test_upstream_mode
|
||||
;;
|
||||
none)
|
||||
test_none_mode
|
||||
;;
|
||||
all)
|
||||
test_bundled_mode
|
||||
test_upstream_mode
|
||||
test_none_mode
|
||||
;;
|
||||
*)
|
||||
log_error "Invalid test mode: $TEST_MODE"
|
||||
log_info "Usage: $0 [bundled|upstream|none|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
17
tests/integration/vitest.config.ts
Normal file
17
tests/integration/vitest.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
name: 'docker-integration',
|
||||
include: ['**/*.test.ts'],
|
||||
testTimeout: 120000, // 2 minutes for Docker operations
|
||||
hookTimeout: 120000,
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['tests/integration/**/*.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user