386 lines
12 KiB
Go
386 lines
12 KiB
Go
// ABOUTME: Integration tests for collector and receiver interaction.
|
|
// ABOUTME: Tests that the push client can successfully send metrics to the receiver.
|
|
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"edp.buildth.ing/DevFW-CICD/forgejo-runner-resource-collector/internal/receiver"
|
|
"edp.buildth.ing/DevFW-CICD/forgejo-runner-resource-collector/internal/summary"
|
|
)
|
|
|
|
const (
|
|
testReadToken = "integration-test-token"
|
|
testHMACKey = "integration-hmac-key"
|
|
)
|
|
|
|
// setupTestReceiver creates a test receiver with SQLite storage, auth, and HTTP server
|
|
func setupTestReceiver(t *testing.T) (*receiver.Store, *httptest.Server, func()) {
|
|
t.Helper()
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
store, err := receiver.NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("NewStore() error = %v", err)
|
|
}
|
|
|
|
handler := receiver.NewHandler(store, slog.New(slog.NewTextHandler(io.Discard, nil)), testReadToken, testHMACKey)
|
|
mux := http.NewServeMux()
|
|
handler.RegisterRoutes(mux)
|
|
|
|
server := httptest.NewServer(mux)
|
|
|
|
cleanup := func() {
|
|
server.Close()
|
|
_ = store.Close()
|
|
}
|
|
|
|
return store, server, cleanup
|
|
}
|
|
|
|
// generatePushToken generates a scoped push token for an execution context
|
|
func generatePushToken(exec summary.ExecutionContext) string {
|
|
return receiver.GenerateScopedToken(testHMACKey, exec.Organization, exec.Repository, exec.Workflow, exec.Job)
|
|
}
|
|
|
|
func TestPushClientToReceiver(t *testing.T) {
|
|
store, server, cleanup := setupTestReceiver(t)
|
|
defer cleanup()
|
|
|
|
// Test execution context
|
|
testCtx := summary.ExecutionContext{
|
|
Organization: "integration-org",
|
|
Repository: "integration-repo",
|
|
Workflow: "test.yml",
|
|
Job: "integration-test",
|
|
RunID: "run-integration-123",
|
|
}
|
|
|
|
// Create a test summary
|
|
testSummary := &summary.RunSummary{
|
|
StartTime: time.Now().Add(-time.Minute),
|
|
EndTime: time.Now(),
|
|
DurationSeconds: 60.0,
|
|
SampleCount: 10,
|
|
CPUTotal: summary.StatSummary{Peak: 85.5, Avg: 42.3, P95: 78.0},
|
|
MemUsedBytes: summary.StatSummary{Peak: 4294967296, Avg: 2147483648, P95: 3865470566},
|
|
MemUsedPercent: summary.StatSummary{Peak: 50.0, Avg: 25.0, P95: 45.0},
|
|
TopCPUProcesses: []summary.ProcessPeak{
|
|
{PID: 1234, Name: "test-process", PeakCPU: 45.0, PeakMem: 1073741824},
|
|
},
|
|
TopMemProcesses: []summary.ProcessPeak{
|
|
{PID: 1234, Name: "test-process", PeakCPU: 45.0, PeakMem: 1073741824},
|
|
},
|
|
}
|
|
|
|
// Build payload matching what push client sends
|
|
payload := struct {
|
|
Execution summary.ExecutionContext `json:"execution"`
|
|
Summary summary.RunSummary `json:"run_summary"`
|
|
}{
|
|
Execution: testCtx,
|
|
Summary: *testSummary,
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
|
|
// Send via HTTP client with scoped push token
|
|
pushToken := generatePushToken(testCtx)
|
|
req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/metrics", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("NewRequest() error = %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+pushToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Do() error = %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusCreated)
|
|
}
|
|
|
|
// Verify metrics were stored
|
|
metrics, err := store.GetMetricsByWorkflowJob("integration-org", "integration-repo", "test.yml", "integration-test")
|
|
if err != nil {
|
|
t.Fatalf("GetMetricsByWorkflowJob() error = %v", err)
|
|
}
|
|
|
|
if len(metrics) != 1 {
|
|
t.Fatalf("got %d metrics, want 1", len(metrics))
|
|
}
|
|
|
|
m := metrics[0]
|
|
if m.Organization != testCtx.Organization {
|
|
t.Errorf("Organization = %q, want %q", m.Organization, testCtx.Organization)
|
|
}
|
|
if m.Repository != testCtx.Repository {
|
|
t.Errorf("Repository = %q, want %q", m.Repository, testCtx.Repository)
|
|
}
|
|
if m.Workflow != testCtx.Workflow {
|
|
t.Errorf("Workflow = %q, want %q", m.Workflow, testCtx.Workflow)
|
|
}
|
|
if m.Job != testCtx.Job {
|
|
t.Errorf("Job = %q, want %q", m.Job, testCtx.Job)
|
|
}
|
|
if m.RunID != testCtx.RunID {
|
|
t.Errorf("RunID = %q, want %q", m.RunID, testCtx.RunID)
|
|
}
|
|
|
|
// Verify payload was stored correctly
|
|
var storedSummary summary.RunSummary
|
|
if err := json.Unmarshal([]byte(m.Payload), &storedSummary); err != nil {
|
|
t.Fatalf("Unmarshal payload error = %v", err)
|
|
}
|
|
|
|
if storedSummary.SampleCount != testSummary.SampleCount {
|
|
t.Errorf("SampleCount = %d, want %d", storedSummary.SampleCount, testSummary.SampleCount)
|
|
}
|
|
if storedSummary.CPUTotal.Peak != testSummary.CPUTotal.Peak {
|
|
t.Errorf("CPUTotal.Peak = %f, want %f", storedSummary.CPUTotal.Peak, testSummary.CPUTotal.Peak)
|
|
}
|
|
}
|
|
|
|
func TestPushClientIntegration(t *testing.T) {
|
|
store, server, cleanup := setupTestReceiver(t)
|
|
defer cleanup()
|
|
|
|
// Set environment variables for the push client
|
|
t.Setenv("GITHUB_REPOSITORY_OWNER", "push-client-org")
|
|
t.Setenv("GITHUB_REPOSITORY", "push-client-repo")
|
|
t.Setenv("GITHUB_WORKFLOW", "push-test.yml")
|
|
t.Setenv("GITHUB_JOB", "push-job")
|
|
t.Setenv("GITHUB_RUN_ID", "push-run-456")
|
|
|
|
// Generate scoped push token
|
|
pushToken := receiver.GenerateScopedToken(testHMACKey, "push-client-org", "push-client-repo", "push-test.yml", "push-job")
|
|
|
|
// Create push client with token - it reads execution context from env vars
|
|
pushClient := summary.NewPushClient(server.URL+"/api/v1/metrics", pushToken)
|
|
|
|
// Verify execution context was read from env
|
|
ctx := pushClient.ExecutionContext()
|
|
if ctx.Organization != "push-client-org" {
|
|
t.Errorf("Organization = %q, want %q", ctx.Organization, "push-client-org")
|
|
}
|
|
|
|
// Create and push a summary
|
|
testSummary := &summary.RunSummary{
|
|
StartTime: time.Now().Add(-30 * time.Second),
|
|
EndTime: time.Now(),
|
|
DurationSeconds: 30.0,
|
|
SampleCount: 6,
|
|
CPUTotal: summary.StatSummary{Peak: 50.0, Avg: 25.0, P95: 45.0},
|
|
MemUsedBytes: summary.StatSummary{Peak: 1000000, Avg: 500000, P95: 900000},
|
|
MemUsedPercent: summary.StatSummary{Peak: 10.0, Avg: 5.0, P95: 9.0},
|
|
}
|
|
|
|
// Push the summary
|
|
err := pushClient.Push(context.Background(), testSummary)
|
|
if err != nil {
|
|
t.Fatalf("Push() error = %v", err)
|
|
}
|
|
|
|
// Verify it was stored
|
|
metrics, err := store.GetMetricsByWorkflowJob("push-client-org", "push-client-repo", "push-test.yml", "push-job")
|
|
if err != nil {
|
|
t.Fatalf("GetMetricsByWorkflowJob() error = %v", err)
|
|
}
|
|
|
|
if len(metrics) != 1 {
|
|
t.Fatalf("got %d metrics, want 1", len(metrics))
|
|
}
|
|
|
|
if metrics[0].RunID != "push-run-456" {
|
|
t.Errorf("RunID = %q, want %q", metrics[0].RunID, "push-run-456")
|
|
}
|
|
}
|
|
|
|
func TestMultiplePushes(t *testing.T) {
|
|
store, server, cleanup := setupTestReceiver(t)
|
|
defer cleanup()
|
|
|
|
// Simulate multiple workflow runs pushing metrics via direct HTTP POST
|
|
runs := []summary.ExecutionContext{
|
|
{Organization: "org-a", Repository: "repo-1", Workflow: "ci.yml", Job: "build", RunID: "run-1"},
|
|
{Organization: "org-a", Repository: "repo-1", Workflow: "ci.yml", Job: "build", RunID: "run-2"},
|
|
{Organization: "org-a", Repository: "repo-1", Workflow: "ci.yml", Job: "test", RunID: "run-1"},
|
|
{Organization: "org-a", Repository: "repo-2", Workflow: "ci.yml", Job: "build", RunID: "run-1"},
|
|
}
|
|
|
|
for _, execCtx := range runs {
|
|
payload := struct {
|
|
Execution summary.ExecutionContext `json:"execution"`
|
|
Summary summary.RunSummary `json:"run_summary"`
|
|
}{
|
|
Execution: execCtx,
|
|
Summary: summary.RunSummary{
|
|
SampleCount: 5,
|
|
CPUTotal: summary.StatSummary{Peak: 50.0},
|
|
},
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
|
|
pushToken := generatePushToken(execCtx)
|
|
req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/metrics", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("NewRequest() error = %v for run %+v", err, execCtx)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+pushToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Do() error = %v for run %+v", err, execCtx)
|
|
}
|
|
_ = resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d for run %+v", resp.StatusCode, http.StatusCreated, execCtx)
|
|
}
|
|
}
|
|
|
|
// Verify filtering works correctly
|
|
metrics, err := store.GetMetricsByWorkflowJob("org-a", "repo-1", "ci.yml", "build")
|
|
if err != nil {
|
|
t.Fatalf("GetMetricsByWorkflowJob() error = %v", err)
|
|
}
|
|
if len(metrics) != 2 {
|
|
t.Errorf("got %d metrics for org-a/repo-1/ci.yml/build, want 2", len(metrics))
|
|
}
|
|
|
|
metrics, err = store.GetMetricsByWorkflowJob("org-a", "repo-1", "ci.yml", "test")
|
|
if err != nil {
|
|
t.Fatalf("GetMetricsByWorkflowJob() error = %v", err)
|
|
}
|
|
if len(metrics) != 1 {
|
|
t.Errorf("got %d metrics for org-a/repo-1/ci.yml/test, want 1", len(metrics))
|
|
}
|
|
}
|
|
|
|
func TestPushClientWithTokenIntegration(t *testing.T) {
|
|
readToken := "integration-read-secret"
|
|
hmacKey := "integration-hmac-secret"
|
|
store, server, cleanup := setupTestReceiverWithToken(t, readToken, hmacKey)
|
|
defer cleanup()
|
|
|
|
// Generate a scoped token via the API
|
|
tokenReqBody, _ := json.Marshal(map[string]string{
|
|
"organization": "token-org",
|
|
"repository": "token-repo",
|
|
"workflow": "ci.yml",
|
|
"job": "build",
|
|
})
|
|
tokenReq, _ := http.NewRequest(http.MethodPost, server.URL+"/api/v1/token", bytes.NewReader(tokenReqBody))
|
|
tokenReq.Header.Set("Authorization", "Bearer "+readToken)
|
|
tokenReq.Header.Set("Content-Type", "application/json")
|
|
|
|
tokenResp, err := http.DefaultClient.Do(tokenReq)
|
|
if err != nil {
|
|
t.Fatalf("token request error: %v", err)
|
|
}
|
|
defer func() { _ = tokenResp.Body.Close() }()
|
|
|
|
if tokenResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("token request status = %d, want %d", tokenResp.StatusCode, http.StatusOK)
|
|
}
|
|
|
|
var tokenBody struct {
|
|
Token string `json:"token"`
|
|
}
|
|
if err := json.NewDecoder(tokenResp.Body).Decode(&tokenBody); err != nil {
|
|
t.Fatalf("decode token response: %v", err)
|
|
}
|
|
|
|
// Use the scoped token to push metrics
|
|
t.Setenv("GITHUB_REPOSITORY_OWNER", "token-org")
|
|
t.Setenv("GITHUB_REPOSITORY", "token-repo")
|
|
t.Setenv("GITHUB_WORKFLOW", "ci.yml")
|
|
t.Setenv("GITHUB_JOB", "build")
|
|
t.Setenv("GITHUB_RUN_ID", "token-run-1")
|
|
|
|
pushClient := summary.NewPushClient(server.URL+"/api/v1/metrics", tokenBody.Token)
|
|
|
|
testSummary := &summary.RunSummary{
|
|
StartTime: time.Now().Add(-10 * time.Second),
|
|
EndTime: time.Now(),
|
|
DurationSeconds: 10.0,
|
|
SampleCount: 2,
|
|
}
|
|
|
|
if err := pushClient.Push(context.Background(), testSummary); err != nil {
|
|
t.Fatalf("Push() error = %v", err)
|
|
}
|
|
|
|
// Verify stored
|
|
metrics, err := store.GetMetricsByWorkflowJob("token-org", "token-repo", "ci.yml", "build")
|
|
if err != nil {
|
|
t.Fatalf("GetMetricsByWorkflowJob() error = %v", err)
|
|
}
|
|
if len(metrics) != 1 {
|
|
t.Fatalf("got %d metrics, want 1", len(metrics))
|
|
}
|
|
if metrics[0].RunID != "token-run-1" {
|
|
t.Errorf("RunID = %q, want %q", metrics[0].RunID, "token-run-1")
|
|
}
|
|
}
|
|
|
|
func TestPushClientWithWrongTokenIntegration(t *testing.T) {
|
|
readToken := "integration-read-secret"
|
|
hmacKey := "integration-hmac-secret"
|
|
_, server, cleanup := setupTestReceiverWithToken(t, readToken, hmacKey)
|
|
defer cleanup()
|
|
|
|
t.Setenv("GITHUB_REPOSITORY_OWNER", "token-org")
|
|
t.Setenv("GITHUB_REPOSITORY", "token-repo")
|
|
t.Setenv("GITHUB_WORKFLOW", "ci.yml")
|
|
t.Setenv("GITHUB_JOB", "build")
|
|
t.Setenv("GITHUB_RUN_ID", "token-run-2")
|
|
|
|
pushClient := summary.NewPushClient(server.URL+"/api/v1/metrics", "wrong-token")
|
|
|
|
err := pushClient.Push(context.Background(), &summary.RunSummary{SampleCount: 1})
|
|
if err == nil {
|
|
t.Error("Push() with wrong token should fail")
|
|
}
|
|
}
|
|
|
|
func setupTestReceiverWithToken(t *testing.T, readToken, hmacKey string) (*receiver.Store, *httptest.Server, func()) {
|
|
t.Helper()
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
store, err := receiver.NewStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("NewStore() error = %v", err)
|
|
}
|
|
|
|
handler := receiver.NewHandler(store, slog.New(slog.NewTextHandler(io.Discard, nil)), readToken, hmacKey)
|
|
mux := http.NewServeMux()
|
|
handler.RegisterRoutes(mux)
|
|
|
|
server := httptest.NewServer(mux)
|
|
|
|
cleanup := func() {
|
|
server.Close()
|
|
_ = store.Close()
|
|
}
|
|
|
|
return store, server, cleanup
|
|
}
|