package tests import ( "os" "os/exec" "path/filepath" "strings" "testing" "time" tests "github.com/jfraeys/fetch_ml/tests/fixtures" ) // TestHomelabSetupE2E tests the complete homelab setup workflow end-to-end func TestHomelabSetupE2E(t *testing.T) { // Skip if essential tools not available manageScript := "../../tools/manage.sh" if _, err := os.Stat(manageScript); os.IsNotExist(err) { t.Skip("manage.sh not found") } cliPath := "../../cli/zig-out/bin/ml" if _, err := os.Stat(cliPath); os.IsNotExist(err) { t.Skip("CLI not built - run 'make build' first") } // Use fixtures for manage script operations ms := tests.NewManageScript(manageScript) defer ms.StopAndCleanup() // Ensure cleanup 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) } // Give services time to start time.Sleep(2 * time.Second) // Reduced from 3 seconds // 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) } // Give services time to start time.Sleep(3 * time.Second) // 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, 0755); 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), 0644); err != nil { t.Fatalf("Failed to create CLI config: %v", err) } // Test CLI init initCmd := exec.Command(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.Command(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.Command("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.Command("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.Command("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.Command("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.Command("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.Command("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 := "../../tools/manage.sh" if _, err := os.Stat(manageScript); os.IsNotExist(err) { t.Skip("manage.sh not found") } // Use fixtures for manage script operations ms := tests.NewManageScript(manageScript) 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 ms := tests.NewManageScript(manageScript) 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") } }) }