fetch_ml/tests/e2e/homelab_e2e_test.go
Jeremie Fraeys 27c8b08a16
test: Reorganize and add unit tests
Reorganize tests for better structure and coverage:
- Move container/security_test.go from internal/ to tests/unit/container/
- Move related tests to proper unit test locations
- Delete orphaned test files (startup_blacklist_test.go)
- Add privacy middleware unit tests
- Add worker config unit tests
- Update E2E tests for homelab and websocket scenarios
- Update test fixtures with utility functions
- Add CLI helper script for arraylist fixes
2026-02-18 21:28:13 -05:00

354 lines
10 KiB
Go

package tests
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
tests "github.com/jfraeys/fetch_ml/tests/fixtures"
)
const (
manageScriptPath = "../../tools/manage.sh"
)
// TestHomelabSetupE2E tests the complete homelab setup workflow end-to-end
func TestHomelabSetupE2E(t *testing.T) {
// Skip if essential tools not available
repoRoot := e2eRepoRoot(t)
manageScript := filepath.Join(repoRoot, "tools/manage.sh")
if _, err := os.Stat(manageScript); os.IsNotExist(err) {
t.Skip("manage.sh not found")
}
cliPath := e2eCLIPath(t)
if _, err := os.Stat(cliPath); os.IsNotExist(err) {
t.Skip("CLI not built - run 'make build' first")
}
// Use fixtures for manage script operations with correct working directory
ms := tests.NewManageScriptWithDir(manageScript, repoRoot)
defer ms.StopAndCleanup()
testDir := t.TempDir()
// Phase 1: Fresh Setup Simulation
t.Run("FreshSetup", func(t *testing.T) {
// Stop any existing services
_ = ms.Stop()
// Test initial status
output, err := ms.Status()
if err != nil {
t.Fatalf("Failed to get status: %v", err)
}
t.Logf("Initial status: %s", output)
// Start services
if err := ms.Start(); err != nil {
t.Skipf("Failed to start services: %v", err)
}
// Wait for health instead of fixed sleep
for range 10 {
healthOutput, err := ms.Health()
if err == nil &&
(strings.Contains(healthOutput, "API is healthy") ||
strings.Contains(healthOutput, "Port 9101 is open")) {
break
}
time.Sleep(300 * time.Millisecond)
}
// Verify with health check
healthOutput, err := ms.Health()
if err != nil {
t.Logf("Health check failed (services may not be fully started)")
} else {
if !strings.Contains(healthOutput, "API is healthy") && !strings.Contains(healthOutput, "Port 9101 is open") {
t.Errorf("Unexpected health check output: %s", healthOutput)
}
t.Log("Health check passed")
}
})
// Phase 2: Service Management Workflow
t.Run("ServiceManagement", func(t *testing.T) {
// Check initial status
output, err := ms.Status()
if err != nil {
t.Errorf("Status check failed: %v", err)
}
t.Logf("Initial status: %s", output)
// Start services
if err := ms.Start(); err != nil {
t.Skipf("Failed to start services: %v", err)
}
// Wait for health instead of fixed sleep
for range 15 {
healthOutput, err := ms.Health()
if err == nil &&
(strings.Contains(healthOutput, "API is healthy") ||
strings.Contains(healthOutput, "Port 9101 is open")) {
break
}
time.Sleep(300 * time.Millisecond)
}
// Verify with health check
healthOutput, err := ms.Health()
t.Logf("Health check output: %s", healthOutput)
if err != nil {
t.Logf("Health check failed (expected if services not fully started): %v", err)
}
// Check final status
statusOutput, err := ms.Status()
if err != nil {
t.Errorf("Final status check failed: %v", err)
}
t.Logf("Final status: %s", statusOutput)
})
// Phase 3: CLI Configuration Workflow
t.Run("CLIConfiguration", func(t *testing.T) {
// Create CLI config directory
cliConfigDir := filepath.Join(testDir, "cli_config")
if err := os.MkdirAll(cliConfigDir, 0750); err != nil {
t.Fatalf("Failed to create CLI config dir: %v", err)
}
// Create minimal config
configPath := filepath.Join(cliConfigDir, "config.yaml")
configContent := `
redis_addr: localhost:6379
redis_db: 13
`
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("Failed to create CLI config: %v", err)
}
// Test CLI init
initCmd := exec.CommandContext(context.Background(), cliPath, "init")
initCmd.Dir = cliConfigDir
initOutput, err := initCmd.CombinedOutput()
if err != nil {
t.Logf("CLI init failed (may be expected): %v", err)
}
t.Logf("CLI init output: %s", string(initOutput))
// Test CLI status
statusCmd := exec.CommandContext(context.Background(), cliPath, "status")
statusCmd.Dir = cliConfigDir
statusOutput, err := statusCmd.CombinedOutput()
if err != nil {
t.Logf("CLI status failed (may be expected): %v", err)
}
t.Logf("CLI status output: %s", string(statusOutput))
})
}
// TestDockerDeploymentE2E tests Docker deployment workflow
func TestDockerDeploymentE2E(t *testing.T) {
t.Parallel() // Enable parallel execution
if os.Getenv("FETCH_ML_E2E_DOCKER") != "1" {
t.Skip("Skipping DockerDeploymentE2E (set FETCH_ML_E2E_DOCKER=1 to enable)")
}
// Skip if Docker not available
dockerCompose := "../../docker-compose.yml"
if _, err := os.Stat(dockerCompose); os.IsNotExist(err) {
t.Skip("docker-compose.yml not found")
}
t.Run("DockerDeployment", func(t *testing.T) {
// Stop any existing containers
downCmd := exec.CommandContext(context.Background(),
"docker-compose", "-f", dockerCompose, "down", "--remove-orphans")
if err := downCmd.Run(); err != nil {
t.Logf("Warning: Failed to stop existing containers: %v", err)
}
// Start Docker containers
upCmd := exec.CommandContext(context.Background(), "docker-compose", "-f", dockerCompose, "up", "-d")
if err := upCmd.Run(); err != nil {
t.Fatalf("Failed to start Docker containers: %v", err)
}
// Wait for containers to be healthy using health checks instead of fixed sleep
maxWait := 15 * time.Second // Reduced from 30 seconds
start := time.Now()
apiHealthy := false
redisHealthy := false
for time.Since(start) < maxWait && (!apiHealthy || !redisHealthy) {
// Check if API container is healthy
if !apiHealthy {
healthCmd := exec.CommandContext(context.Background(),
"docker", "ps", "--filter", "name=ml-experiments-api", "--format", "{{.Status}}")
healthOutput, err := healthCmd.CombinedOutput()
if err == nil && strings.Contains(string(healthOutput), "healthy") {
t.Logf("API container became healthy in %v", time.Since(start))
apiHealthy = true
} else if err == nil && strings.Contains(string(healthOutput), "Up") {
// Accept "Up" status as good enough for testing
t.Logf("API container is up in %v (not necessarily healthy)", time.Since(start))
apiHealthy = true
}
}
// Check if Redis is healthy
if !redisHealthy {
redisCmd := exec.CommandContext(context.Background(),
"docker", "ps", "--filter", "name=ml-experiments-redis", "--format", "{{.Status}}")
redisOutput, err := redisCmd.CombinedOutput()
if err == nil && strings.Contains(string(redisOutput), "healthy") {
t.Logf("Redis container became healthy in %v", time.Since(start))
redisHealthy = true
}
}
// Break if both are healthy/up
if apiHealthy && redisHealthy {
t.Logf("All containers ready in %v", time.Since(start))
break
}
time.Sleep(500 * time.Millisecond) // Check more frequently
}
// Check container status
psCmd := exec.CommandContext(context.Background(),
"docker-compose", "-f", dockerCompose, "ps", "--format", "table {{.Name}}\t{{.Status}}")
psOutput, err := psCmd.CombinedOutput()
if err != nil {
t.Errorf("Docker ps failed: %v", err)
}
t.Logf("Docker containers status: %s", string(psOutput))
// Test API endpoint in Docker (quick check)
testDockerAPI(t)
// Cleanup Docker synchronously to ensure proper cleanup
t.Cleanup(func() {
downCmd := exec.CommandContext(context.Background(),
"docker-compose", "-f", dockerCompose, "down", "--remove-orphans", "--volumes")
if err := downCmd.Run(); err != nil {
t.Logf("Warning: Failed to stop Docker containers: %v", err)
}
})
})
}
// testDockerAPI tests the Docker API endpoint
func testDockerAPI(t *testing.T) {
// This would test the API endpoint - simplified for now
t.Log("Testing Docker API functionality...")
// In a real test, you would make HTTP requests to the API
}
// TestPerformanceE2E tests performance characteristics end-to-end
func TestPerformanceE2E(t *testing.T) {
t.Parallel() // Enable parallel execution
if os.Getenv("FETCH_ML_E2E_PERF") != "1" {
t.Skip("Skipping PerformanceE2E (set FETCH_ML_E2E_PERF=1 to enable)")
}
manageScript := manageScriptPath
if _, err := os.Stat(manageScript); os.IsNotExist(err) {
t.Skip("manage.sh not found")
}
// Use fixtures for manage script operations with correct working directory
repoRoot := e2eRepoRoot(t)
manageScript = filepath.Join(repoRoot, "tools/manage.sh")
ms := tests.NewManageScriptWithDir(manageScript, repoRoot)
t.Run("PerformanceMetrics", func(t *testing.T) {
// Test health check performance
start := time.Now()
_, err := ms.Health()
duration := time.Since(start)
t.Logf("Health check took %v", duration)
if duration > 10*time.Second {
t.Errorf("Health check took too long: %v", duration)
}
if err != nil {
t.Logf("Health check failed (expected if services not running)")
} else {
t.Log("Health check passed")
}
// Test status check performance
start = time.Now()
output, err := ms.Status()
duration = time.Since(start)
t.Logf("Status check took %v", duration)
t.Logf("Status output length: %d characters", len(output))
if duration > 5*time.Second {
t.Errorf("Status check took too long: %v", duration)
}
_ = err // Suppress unused variable warning
})
}
// TestConfigurationScenariosE2E tests various configuration scenarios end-to-end
func TestConfigurationScenariosE2E(t *testing.T) {
t.Parallel() // Enable parallel execution
manageScript := "../../tools/manage.sh"
if _, err := os.Stat(manageScript); os.IsNotExist(err) {
t.Skip("manage.sh not found")
}
// Use fixtures for manage script operations with correct working directory
repoRoot := e2eRepoRoot(t)
manageScript = filepath.Join(repoRoot, "tools/manage.sh")
ms := tests.NewManageScriptWithDir(manageScript, repoRoot)
t.Run("ConfigurationHandling", func(t *testing.T) {
testDir := t.TempDir()
// Test status with different configuration states
originalConfigDir := "../../configs"
tempConfigDir := filepath.Join(testDir, "configs_backup")
// Backup original configs if they exist
if _, err := os.Stat(originalConfigDir); err == nil {
if err := os.Rename(originalConfigDir, tempConfigDir); err != nil {
t.Fatalf("Failed to backup configs: %v", err)
}
defer func() {
_ = os.Rename(tempConfigDir, originalConfigDir)
}()
}
// Test status without configs
output, err := ms.Status()
if err != nil {
t.Errorf("Status check failed: %v", err)
}
t.Logf("Status without configs: %s", output)
// Test health without configs
_, err = ms.Health()
if err != nil {
t.Logf("Health check failed without configs (expected)")
} else {
t.Log("Health check passed without configs")
}
})
}