From ce7831f4627497f04e5263620c0df9c75e031dee Mon Sep 17 00:00:00 2001 From: Vinu K Date: Tue, 20 Jan 2026 13:09:37 +0000 Subject: [PATCH] test(SUSTAINING-1678): Add integration tests Run integration tests on all the PRs Signed-off-by: Vinu K --- .github/workflows/integration-tests.yml | 46 +++ Makefile | 6 + internal/api/integration_test.go | 357 ++++++++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 internal/api/integration_test.go diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..a5c05ed --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,46 @@ +name: Integration Tests + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + integration-test: + name: Run Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y git jq + + - name: Install Go tools + run: | + go install golang.org/x/tools/cmd/digraph@latest + go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Build binaries + run: make gvs cg + + - name: Run integration tests + run: make test-integration + + - name: Cleanup + if: always() + run: | + pkill -f './bin/gvs' || true + rm -rf /tmp/gvs-cache /tmp/cg-* /tmp/gvc-* diff --git a/Makefile b/Makefile index 8cffc0e..821708d 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,12 @@ endif run: gvs cg ./bin/gvs +.PHONY: test-integration + +test-integration: + @echo "Running integration tests..." + go test -v -count=1 ./internal/api -run TestCallgraphIntegration -timeout 15m + .PHONY: gvs gvs: $(GVS_SOURCES) diff --git a/internal/api/integration_test.go b/internal/api/integration_test.go new file mode 100644 index 0000000..6727c2c --- /dev/null +++ b/internal/api/integration_test.go @@ -0,0 +1,357 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "syscall" + "testing" + "time" +) + +func TestCallgraphIntegration(t *testing.T) { + // Step 0: Clear cache + t.Log("Clearing cache directory...") + if err := os.RemoveAll("/tmp/gvs-cache"); err != nil && !os.IsNotExist(err) { + t.Logf("Warning: Failed to clear cache: %v", err) + } + + // Step 1: Build binaries + t.Log("Building binaries with make...") + buildCmd := exec.Command("make", "gvs", "cg") + buildCmd.Dir = "../../" // Go to project root + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr + if err := buildCmd.Run(); err != nil { + t.Fatalf("Failed to build binaries: %v", err) + } + + // Step 2: Start gvs server + t.Log("Starting gvs server on port 8087...") + serverCmd := exec.Command("./bin/gvs") + serverCmd.Dir = "../../" // Run from project root so bin/cg can be found + serverCmd.Env = append(os.Environ(), "GVS_PORT=8087") + serverCmd.Stdout = os.Stdout + serverCmd.Stderr = os.Stderr + + if err := serverCmd.Start(); err != nil { + t.Fatalf("Failed to start gvs server: %v", err) + } + + // Ensure cleanup + defer func() { + t.Log("Killing gvs server...") + if err := exec.Command("pkill", "-f", "./bin/gvs").Run(); err != nil { + t.Logf("Warning: pkill failed: %v", err) + } + // Also try to kill by PID as backup + if serverCmd.Process != nil { + serverCmd.Process.Signal(syscall.SIGTERM) + } + }() + + // Wait for server to start + t.Log("Waiting for server to be ready...") + if err := waitForServer("http://localhost:8087/healthz", 30*time.Second); err != nil { + t.Fatalf("Server did not start in time: %v", err) + } + + // Step 3: Make POST request to /callgraph with non-vulnerable commit + t.Log("Sending callgraph request for non-vulnerable commit...") + requestBody := map[string]interface{}{ + "repo": "https://github.com/openshift/metallb", + "branchOrCommit": "3bc20ed6603faa47e087032bf7a6aef90911d903", + "cve": "CVE-2024-45339", + "runFix": false, + } + + reqJSON, err := json.Marshal(requestBody) + if err != nil { + t.Fatalf("Failed to marshal request: %v", err) + } + + resp, err := http.Post( + "http://localhost:8087/callgraph", + "application/json", + bytes.NewBuffer(reqJSON), + ) + if err != nil { + t.Fatalf("Failed to send callgraph request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Callgraph request failed with status %d: %s", resp.StatusCode, body) + } + + var callgraphResp struct { + TaskID string `json:"taskId"` + } + if err := json.NewDecoder(resp.Body).Decode(&callgraphResp); err != nil { + t.Fatalf("Failed to decode callgraph response: %v", err) + } + + taskID := callgraphResp.TaskID + if taskID == "" { + t.Fatal("No taskId returned from callgraph request") + } + t.Logf("Received taskId: %s", taskID) + + // Step 4: Poll /status endpoint + t.Log("Polling status endpoint...") + var isVulnerable string + maxAttempts := 120 // 2 minutes with 1 second intervals + + for i := 0; i < maxAttempts; i++ { + statusReq := map[string]string{"taskId": taskID} + statusJSON, err := json.Marshal(statusReq) + if err != nil { + t.Fatalf("Failed to marshal status request: %v", err) + } + + statusResp, err := http.Post( + "http://localhost:8087/status", + "application/json", + bytes.NewBuffer(statusJSON), + ) + if err != nil { + t.Fatalf("Failed to send status request: %v", err) + } + + var statusResult struct { + Status string `json:"status"` + Output json.RawMessage `json:"output"` + Error string `json:"error"` + } + + body, err := io.ReadAll(statusResp.Body) + statusResp.Body.Close() + + if err != nil { + t.Fatalf("Failed to read status response: %v", err) + } + + if err := json.Unmarshal(body, &statusResult); err != nil { + t.Fatalf("Failed to decode status response: %v", err) + } + + t.Logf("Attempt %d: Status = %s", i+1, statusResult.Status) + + if statusResult.Status == "completed" { + // Parse the output to get IsVulnerable + var output struct { + IsVulnerable string `json:"IsVulnerable"` + } + if err := json.Unmarshal(statusResult.Output, &output); err != nil { + t.Fatalf("Failed to parse output: %v", err) + } + + isVulnerable = output.IsVulnerable + t.Logf("Task completed! IsVulnerable: %s", isVulnerable) + break + } else if statusResult.Status == "failed" { + t.Fatalf("Task failed with error: %s", statusResult.Error) + } + + // Wait before next poll + time.Sleep(1 * time.Second) + } + + if isVulnerable == "" { + t.Fatal("Task did not complete within timeout") + } + + // Step 5: Verify the result - this commit should be non-vulnerable + t.Logf("Final result - IsVulnerable: %s", isVulnerable) + + // This commit should not be vulnerable + if isVulnerable != "false" { + t.Errorf("Expected IsVulnerable: false, got: %s", isVulnerable) + } +} + +func TestCallgraphIntegrationVulnerable(t *testing.T) { + // Step 0: Clear cache + t.Log("Clearing cache directory...") + if err := os.RemoveAll("/tmp/gvs-cache"); err != nil && !os.IsNotExist(err) { + t.Logf("Warning: Failed to clear cache: %v", err) + } + + // Step 1: Build binaries + t.Log("Building binaries with make...") + buildCmd := exec.Command("make", "gvs", "cg") + buildCmd.Dir = "../../" // Go to project root + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr + if err := buildCmd.Run(); err != nil { + t.Fatalf("Failed to build binaries: %v", err) + } + + // Step 2: Start gvs server + t.Log("Starting gvs server on port 8087...") + serverCmd := exec.Command("./bin/gvs") + serverCmd.Dir = "../../" // Run from project root so bin/cg can be found + serverCmd.Env = append(os.Environ(), "GVS_PORT=8087") + serverCmd.Stdout = os.Stdout + serverCmd.Stderr = os.Stderr + + if err := serverCmd.Start(); err != nil { + t.Fatalf("Failed to start gvs server: %v", err) + } + + // Ensure cleanup + defer func() { + t.Log("Killing gvs server...") + if err := exec.Command("pkill", "-f", "./bin/gvs").Run(); err != nil { + t.Logf("Warning: pkill failed: %v", err) + } + // Also try to kill by PID as backup + if serverCmd.Process != nil { + serverCmd.Process.Signal(syscall.SIGTERM) + } + }() + + // Wait for server to start + t.Log("Waiting for server to be ready...") + if err := waitForServer("http://localhost:8087/healthz", 30*time.Second); err != nil { + t.Fatalf("Server did not start in time: %v", err) + } + + // Step 3: Make POST request to /callgraph with vulnerable commit + t.Log("Sending callgraph request for vulnerable commit...") + requestBody := map[string]interface{}{ + "repo": "https://github.com/openshift/metallb", + "branchOrCommit": "aee829d4d0938e0e2dc5462f886e448e86544db1", + "cve": "CVE-2024-45339", + "runFix": false, + } + + reqJSON, err := json.Marshal(requestBody) + if err != nil { + t.Fatalf("Failed to marshal request: %v", err) + } + + resp, err := http.Post( + "http://localhost:8087/callgraph", + "application/json", + bytes.NewBuffer(reqJSON), + ) + if err != nil { + t.Fatalf("Failed to send callgraph request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Callgraph request failed with status %d: %s", resp.StatusCode, body) + } + + var callgraphResp struct { + TaskID string `json:"taskId"` + } + if err := json.NewDecoder(resp.Body).Decode(&callgraphResp); err != nil { + t.Fatalf("Failed to decode callgraph response: %v", err) + } + + taskID := callgraphResp.TaskID + if taskID == "" { + t.Fatal("No taskId returned from callgraph request") + } + t.Logf("Received taskId: %s", taskID) + + // Step 4: Poll /status endpoint + t.Log("Polling status endpoint...") + var isVulnerable string + maxAttempts := 120 // 2 minutes with 1 second intervals + + for i := 0; i < maxAttempts; i++ { + statusReq := map[string]string{"taskId": taskID} + statusJSON, err := json.Marshal(statusReq) + if err != nil { + t.Fatalf("Failed to marshal status request: %v", err) + } + + statusResp, err := http.Post( + "http://localhost:8087/status", + "application/json", + bytes.NewBuffer(statusJSON), + ) + if err != nil { + t.Fatalf("Failed to send status request: %v", err) + } + + var statusResult struct { + Status string `json:"status"` + Output json.RawMessage `json:"output"` + Error string `json:"error"` + } + + body, err := io.ReadAll(statusResp.Body) + statusResp.Body.Close() + + if err != nil { + t.Fatalf("Failed to read status response: %v", err) + } + + if err := json.Unmarshal(body, &statusResult); err != nil { + t.Fatalf("Failed to decode status response: %v", err) + } + + t.Logf("Attempt %d: Status = %s", i+1, statusResult.Status) + + if statusResult.Status == "completed" { + // Parse the output to get IsVulnerable + var output struct { + IsVulnerable string `json:"IsVulnerable"` + } + if err := json.Unmarshal(statusResult.Output, &output); err != nil { + t.Fatalf("Failed to parse output: %v", err) + } + + isVulnerable = output.IsVulnerable + t.Logf("Task completed! IsVulnerable: %s", isVulnerable) + break + } else if statusResult.Status == "failed" { + t.Fatalf("Task failed with error: %s", statusResult.Error) + } + + // Wait before next poll + time.Sleep(1 * time.Second) + } + + if isVulnerable == "" { + t.Fatal("Task did not complete within timeout") + } + + // Step 5: Verify the result - this commit should be vulnerable + t.Logf("Final result - IsVulnerable: %s", isVulnerable) + + // This commit should be vulnerable + if isVulnerable != "true" { + t.Errorf("Expected IsVulnerable: true, got: %s", isVulnerable) + } +} + +// waitForServer waits for the server to be ready by polling the health endpoint +func waitForServer(healthURL string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + resp, err := http.Get(healthURL) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return nil + } + if resp != nil { + resp.Body.Close() + } + time.Sleep(500 * time.Millisecond) + } + + return fmt.Errorf("timeout waiting for server") +}