package receiver import ( "bytes" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "path/filepath" "testing" "edp.buildth.ing/DevFW-CICD/forgejo-runner-resource-collector/internal/summary" ) func TestHandler_ReceiveMetrics(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() payload := MetricsPayload{ Execution: ExecutionContext{ Organization: "test-org", Repository: "test-repo", Workflow: "ci.yml", Job: "build", RunID: "run-123", }, Summary: summary.RunSummary{ DurationSeconds: 60.0, SampleCount: 12, }, } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPost, "/api/v1/metrics", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated) } var resp map[string]any if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } if resp["status"] != "created" { t.Errorf("response status = %v, want %q", resp["status"], "created") } if resp["id"] == nil || resp["id"].(float64) == 0 { t.Error("response id is missing or zero") } } func TestHandler_ReceiveMetrics_InvalidJSON(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() req := httptest.NewRequest(http.MethodPost, "/api/v1/metrics", bytes.NewReader([]byte("not json"))) rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) } } func TestHandler_ReceiveMetrics_MissingRunID(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() payload := MetricsPayload{ Execution: ExecutionContext{ Organization: "test-org", Repository: "test-repo", // RunID is missing }, Summary: summary.RunSummary{}, } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPost, "/api/v1/metrics", bytes.NewReader(body)) rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) } } func TestHandler_GetByRunID(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() // First, save a metric payload := &MetricsPayload{ Execution: ExecutionContext{ Organization: "test-org", Repository: "test-repo", Workflow: "ci.yml", Job: "build", RunID: "run-get-test", }, Summary: summary.RunSummary{SampleCount: 5}, } if _, err := h.store.SaveMetric(payload); err != nil { t.Fatalf("SaveMetric() error = %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/v1/metrics/run/run-get-test", nil) rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } var metrics []Metric if err := json.NewDecoder(rec.Body).Decode(&metrics); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(metrics) != 1 { t.Errorf("got %d metrics, want 1", len(metrics)) } if metrics[0].RunID != "run-get-test" { t.Errorf("RunID = %q, want %q", metrics[0].RunID, "run-get-test") } } func TestHandler_GetByRunID_NotFound(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/api/v1/metrics/run/nonexistent", nil) rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } var metrics []Metric if err := json.NewDecoder(rec.Body).Decode(&metrics); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(metrics) != 0 { t.Errorf("got %d metrics, want 0", len(metrics)) } } func TestHandler_GetByRepository(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() // Save metrics for different repos payloads := []*MetricsPayload{ {Execution: ExecutionContext{Organization: "org-x", Repository: "repo-y", RunID: "r1"}}, {Execution: ExecutionContext{Organization: "org-x", Repository: "repo-y", RunID: "r2"}}, {Execution: ExecutionContext{Organization: "org-x", Repository: "repo-z", RunID: "r3"}}, } for _, p := range payloads { if _, err := h.store.SaveMetric(p); err != nil { t.Fatalf("SaveMetric() error = %v", err) } } req := httptest.NewRequest(http.MethodGet, "/api/v1/metrics/repo/org-x/repo-y", nil) rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } var metrics []Metric if err := json.NewDecoder(rec.Body).Decode(&metrics); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(metrics) != 2 { t.Errorf("got %d metrics, want 2", len(metrics)) } } func TestHandler_Health(t *testing.T) { h, cleanup := newTestHandler(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/health", nil) rec := httptest.NewRecorder() mux := http.NewServeMux() h.RegisterRoutes(mux) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } var resp map[string]string if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } if resp["status"] != "ok" { t.Errorf("status = %q, want %q", resp["status"], "ok") } } func newTestHandler(t *testing.T) (*Handler, func()) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.db") store, err := NewStore(dbPath) if err != nil { t.Fatalf("NewStore() error = %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) handler := NewHandler(store, logger) return handler, func() { _ = store.Close() } }