- Add end-to-end tests for complete workflow validation - Include integration tests for API and database interactions - Add unit tests for all major components and utilities - Include performance tests for payload handling - Add CLI API integration tests - Include Podman container integration tests - Add WebSocket and queue execution tests - Include shell script tests for setup validation Provides comprehensive test coverage ensuring platform reliability and functionality across all components and interactions.
323 lines
9.4 KiB
Go
323 lines
9.4 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|