package main import ( "encoding/json" "flag" "fmt" "log" "net/http" "os" "os/exec" "path/filepath" "strings" "text/template" "time" ) type TestScenario struct { Name string `json:"name"` Description string `json:"description"` Command string `json:"command"` RunTwice bool `json:"run_twice"` SetupCommands []string `json:"setup_commands,omitempty"` VerifyCommands []string `json:"verify_commands,omitempty"` ExpectedStructure []string `json:"expected_structure"` Disabled bool `json:"disabled,omitempty"` SkipTokenVerification bool `json:"skip_token_verification,omitempty"` } type TestConfig struct { TestScenarios []TestScenario `json:"test_scenarios"` } type TestContext struct { BaseURL string Token string GhorgDir string } type TestRunner struct { config *TestConfig context *TestContext } func NewTestRunner(configPath string, context *TestContext) (*TestRunner, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read test config: %w", err) } config := &TestConfig{} if err := json.Unmarshal(data, config); err != nil { return nil, fmt.Errorf("failed to parse test config: %w", err) } return &TestRunner{ config: config, context: context, }, nil } func (tr *TestRunner) RunAllTests() error { log.Printf("Starting integration tests with %d scenarios...", len(tr.config.TestScenarios)) // Ensure the ghorg directory exists if err := tr.ensureGhorgDirectoryExists(); err != nil { return fmt.Errorf("failed to create ghorg directory: %w", err) } // Clean up any existing test directories if err := tr.cleanupTestDirectories(); err != nil { log.Printf("Warning: Failed to clean up test directories: %v", err) } passed := 0 failed := 0 skipped := 0 for i, scenario := range tr.config.TestScenarios { log.Printf("\n=== Running Test %d/%d: %s ===", i+1, len(tr.config.TestScenarios), scenario.Name) log.Printf("Description: %s", scenario.Description) if scenario.Disabled { log.Printf("⏭️ SKIPPED: %s (test is disabled)", scenario.Name) skipped++ continue } // Extended pre-test server health check with more retries log.Printf("🔍 Checking server health before test...") if err := tr.waitForServerRecovery(10, 5*time.Second); err != nil { log.Printf("⚠️ Server health check failed before test: %v", err) log.Printf("❌ FAILED: %s - server not healthy", scenario.Name) failed++ // Wait longer before continuing after server health failure if i < len(tr.config.TestScenarios)-1 { log.Printf("⏳ Skipping server recovery wait for faster testing...") // time.Sleep removed } continue } // Additional delay before starting test to ensure server stability log.Printf("⏳ Starting test immediately...") time.Sleep(1 * time.Second) // Minimal coordination delay // Execute the test if err := tr.runTest(&scenario); err != nil { log.Printf("❌ FAILED: %s - %v", scenario.Name, err) failed++ // Exponential backoff after failed tests waitTime := 15 + (failed * 5) // Start at 15s, add 5s per failure if waitTime > 60 { waitTime = 60 // Cap at 60 seconds } if i < len(tr.config.TestScenarios)-1 { log.Printf("⏳ Waiting %d seconds after failed test for server recovery (attempt %d)...", waitTime, failed) // time.Sleep removed for faster execution // Additional health check after failure to ensure recovery log.Printf("🔍 Verifying server recovery after failure...") if err := tr.waitForServerRecovery(15, 5*time.Second); err != nil { log.Printf("⚠️ Server failed to recover properly: %v", err) log.Printf("⏳ Additional 30 second recovery wait...") // time.Sleep removed for faster execution } } } else { log.Printf("✅ PASSED: %s", scenario.Name) passed++ // Longer delay between successful tests to prevent server overload if i < len(tr.config.TestScenarios)-1 { log.Printf("⏳ Waiting 8 seconds before next test...") // time.Sleep removed for faster execution } } } log.Printf("\n=== Test Results ===") log.Printf("Passed: %d", passed) log.Printf("Failed: %d", failed) log.Printf("Skipped: %d", skipped) log.Printf("Total: %d", len(tr.config.TestScenarios)) if failed > 0 { return fmt.Errorf("integration tests failed: %d passed, %d failed, %d skipped", passed, failed, skipped) } return nil } func (tr *TestRunner) ensureGhorgDirectoryExists() error { if err := os.MkdirAll(tr.context.GhorgDir, 0755); err != nil { return fmt.Errorf("failed to create ghorg directory %s: %w", tr.context.GhorgDir, err) } return nil } func (tr *TestRunner) cleanupTestDirectories() error { log.Printf("Cleaning up test directories in %s...", tr.context.GhorgDir) // Remove all directories that start with "local-bitbucket-" entries, err := os.ReadDir(tr.context.GhorgDir) if err != nil { return fmt.Errorf("failed to read ghorg directory: %w", err) } for _, entry := range entries { if entry.IsDir() && strings.HasPrefix(entry.Name(), "local-bitbucket-") { dirPath := filepath.Join(tr.context.GhorgDir, entry.Name()) log.Printf("Removing test directory: %s", dirPath) if err := os.RemoveAll(dirPath); err != nil { log.Printf("Warning: Failed to remove directory %s: %v", dirPath, err) } } } // Also clean up any test directories in /tmp tmpEntries, err := os.ReadDir("/tmp") if err == nil { for _, entry := range tmpEntries { if entry.IsDir() && strings.HasPrefix(entry.Name(), "bitbucket-") { dirPath := filepath.Join("/tmp", entry.Name()) log.Printf("Removing tmp test directory: %s", dirPath) if err := os.RemoveAll(dirPath); err != nil { log.Printf("Warning: Failed to remove tmp directory %s: %v", dirPath, err) } } } } return nil } // checkServerHealth performs a comprehensive health check on the Bitbucket Server func (tr *TestRunner) checkServerHealth() error { client := &http.Client{ Timeout: 15 * time.Second, } // First check: Basic status endpoint statusResp, err := client.Get(tr.context.BaseURL + "/status") if err != nil { return fmt.Errorf("server status check failed: %w", err) } defer statusResp.Body.Close() if statusResp.StatusCode != http.StatusOK { return fmt.Errorf("server status check failed with status %d", statusResp.StatusCode) } // Second check: Try to access the projects API with authentication req, err := http.NewRequest("GET", tr.context.BaseURL+"/rest/api/1.0/projects", nil) if err != nil { return fmt.Errorf("failed to create health check request: %w", err) } req.SetBasicAuth("admin", tr.context.Token) resp, err := client.Do(req) if err != nil { return fmt.Errorf("server API health check failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("server API health check failed with status %d", resp.StatusCode) } // Third check: Verify we can access a specific project (LBP1) projectReq, err := http.NewRequest("GET", tr.context.BaseURL+"/rest/api/1.0/projects/LBP1", nil) if err != nil { return fmt.Errorf("failed to create project health check request: %w", err) } projectReq.SetBasicAuth("admin", tr.context.Token) projectResp, err := client.Do(projectReq) if err != nil { return fmt.Errorf("project health check failed: %w", err) } defer projectResp.Body.Close() if projectResp.StatusCode != http.StatusOK { return fmt.Errorf("project health check failed with status %d", projectResp.StatusCode) } return nil } // waitForServerRecovery waits for the server to be healthy with retries func (tr *TestRunner) waitForServerRecovery(retries int, delayBetweenRetries time.Duration) error { for i := 0; i < retries; i++ { if err := tr.checkServerHealth(); err == nil { if i > 0 { log.Printf("✅ Server recovered after %d attempts", i+1) } return nil } if i < retries-1 { log.Printf("⏳ Server not ready (attempt %d/%d), waiting %v...", i+1, retries, delayBetweenRetries) time.Sleep(delayBetweenRetries) } } return fmt.Errorf("server failed to recover after %d attempts", retries) } func (tr *TestRunner) runTest(scenario *TestScenario) error { // Run setup commands if any for _, setupCmd := range scenario.SetupCommands { log.Printf("Running setup command: %s", setupCmd) if err := tr.executeCommand(setupCmd); err != nil { return fmt.Errorf("setup command failed: %w", err) } } // Render the command template with context variables command, err := tr.renderCommand(scenario.Command) if err != nil { return fmt.Errorf("failed to render command: %w", err) } log.Printf("Executing: %s", command) // Run the command if err := tr.executeCommand(command); err != nil { return fmt.Errorf("command execution failed: %w", err) } // Run the command twice if specified if scenario.RunTwice { log.Printf("Running command again (run_twice=true)...") if err := tr.executeCommand(command); err != nil { return fmt.Errorf("second command execution failed: %w", err) } } // Verify the expected structure if len(scenario.ExpectedStructure) > 0 { if err := tr.verifyExpectedStructure(scenario.ExpectedStructure); err != nil { return fmt.Errorf("structure verification failed: %w", err) } } // Run verification commands if any for _, verifyCmd := range scenario.VerifyCommands { log.Printf("Running verification command: %s", verifyCmd) if err := tr.executeCommand(verifyCmd); err != nil { return fmt.Errorf("verification command failed: %w", err) } } return nil } func (tr *TestRunner) renderCommand(commandTemplate string) (string, error) { tmpl, err := template.New("command").Parse(commandTemplate) if err != nil { return "", fmt.Errorf("failed to parse command template: %w", err) } var buf strings.Builder if err := tmpl.Execute(&buf, tr.context); err != nil { return "", fmt.Errorf("failed to execute command template: %w", err) } return buf.String(), nil } func (tr *TestRunner) executeCommand(command string) error { // Change to the ghorg directory originalDir, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } if err := os.Chdir(tr.context.GhorgDir); err != nil { return fmt.Errorf("failed to change to ghorg directory: %w", err) } defer func() { if err := os.Chdir(originalDir); err != nil { log.Printf("Warning: Failed to change back to original directory: %v", err) } }() // Split the command into parts parts := strings.Fields(command) if len(parts) == 0 { return fmt.Errorf("empty command") } // Execute the command cmd := exec.Command(parts[0], parts[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // Set environment variables for Bitbucket Server testing env := os.Environ() env = append(env, "GHORG_INSECURE_BITBUCKET_CLIENT=true") env = append(env, "GHORG_CLONE_PROTOCOL=https") cmd.Env = env return cmd.Run() } func (tr *TestRunner) verifyExpectedStructure(expectedPaths []string) error { log.Printf("Verifying expected structure (%d paths)...", len(expectedPaths)) missing := []string{} for _, expectedPath := range expectedPaths { // Handle absolute paths and relative paths var fullPath string if filepath.IsAbs(expectedPath) { fullPath = expectedPath } else { fullPath = filepath.Join(tr.context.GhorgDir, expectedPath) } if _, err := os.Stat(fullPath); os.IsNotExist(err) { missing = append(missing, expectedPath) log.Printf("❌ Missing: %s (checked: %s)", expectedPath, fullPath) } else { log.Printf("✅ Found: %s", expectedPath) } } if len(missing) > 0 { return fmt.Errorf("missing expected paths: %s", strings.Join(missing, ", ")) } log.Printf("All expected paths verified successfully!") return nil } func main() { var ( token = flag.String("token", "", "Bitbucket Server API token or password") baseURL = flag.String("base-url", "http://bitbucket.example.com:7990", "Bitbucket Server base URL") ghorgDir = flag.String("ghorg-dir", "", "Directory where ghorg will clone repositories") configPath = flag.String("config", "configs/test-scenarios.json", "Path to test scenarios configuration file") ) flag.Parse() if *token == "" { log.Fatal("Token is required") } if *ghorgDir == "" { homeDir, err := os.UserHomeDir() if err != nil { log.Fatalf("Failed to get home directory: %v", err) } *ghorgDir = filepath.Join(homeDir, "ghorg") } context := &TestContext{ BaseURL: *baseURL, Token: *token, GhorgDir: *ghorgDir, } runner, err := NewTestRunner(*configPath, context) if err != nil { log.Fatalf("Failed to create test runner: %v", err) } if err := runner.RunAllTests(); err != nil { log.Fatalf("Integration tests failed: %v", err) } log.Println("All integration tests passed successfully!") }