package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/monitoring" "github.com/rcourtman/pulse-go-rewrite/pkg/reporting" ) type stubReportingEngine struct { data []byte contentType string err error lastReq reporting.MetricReportRequest lastMulti reporting.MultiReportRequest } func (s *stubReportingEngine) Generate(req reporting.MetricReportRequest) ([]byte, string, error) { s.lastReq = req if s.err != nil { return nil, "", s.err } return s.data, s.contentType, nil } func (s *stubReportingEngine) GenerateMulti(req reporting.MultiReportRequest) ([]byte, string, error) { s.lastMulti = req if s.err != nil { return nil, "", s.err } return s.data, s.contentType, nil } func TestReportingHandlers_MethodNotAllowed(t *testing.T) { handler := NewReportingHandlers(nil) req := httptest.NewRequest(http.MethodPost, "/api/reporting", nil) rr := httptest.NewRecorder() handler.HandleGenerateReport(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rr.Code) } } func TestReportingHandlers_EngineUnavailable(t *testing.T) { original := reporting.GetEngine() reporting.SetEngine(nil) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil) req := httptest.NewRequest(http.MethodGet, "/api/reporting?resourceType=node&resourceId=1", nil) rr := httptest.NewRecorder() handler.HandleGenerateReport(rr, req) if rr.Code != http.StatusInternalServerError { t.Fatalf("expected status %d, got %d", http.StatusInternalServerError, rr.Code) } var resp map[string]any if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if resp["code"] != "engine_unavailable" { t.Fatalf("expected engine_unavailable, got %#v", resp["code"]) } } func TestReportingHandlers_InvalidFormatAndParams(t *testing.T) { engine := &stubReportingEngine{data: []byte("ok"), contentType: "text/plain"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil) req := httptest.NewRequest(http.MethodGet, "/api/reporting?format=txt&resourceType=node&resourceId=1", nil) rr := httptest.NewRecorder() handler.HandleGenerateReport(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code) } req = httptest.NewRequest(http.MethodGet, "/api/reporting?format=pdf", nil) rr = httptest.NewRecorder() handler.HandleGenerateReport(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code) } } func TestReportingHandlers_GenerateReport(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil) start := time.Now().Add(-2 * time.Hour).UTC().Format(time.RFC3339) end := time.Now().UTC().Format(time.RFC3339) query := url.Values{ "format": []string{"pdf"}, "resourceType": []string{"node"}, "resourceId": []string{"node-1"}, "metricType": []string{"cpu"}, "start": []string{start}, "end": []string{end}, "title": []string{"Node report"}, } req := httptest.NewRequest(http.MethodGet, "/api/reporting?"+query.Encode(), nil) rr := httptest.NewRecorder() handler.HandleGenerateReport(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) } if ct := rr.Header().Get("Content-Type"); ct != "application/pdf" { t.Fatalf("expected content-type application/pdf, got %q", ct) } if disp := rr.Header().Get("Content-Disposition"); !strings.Contains(disp, "report-node-1") { t.Fatalf("expected content-disposition to contain sanitized filename, got %q", disp) } if body := rr.Body.String(); body != "report" { t.Fatalf("expected report body, got %q", body) } if engine.lastReq.ResourceType != "node" || engine.lastReq.ResourceID != "node-1" { t.Fatalf("unexpected request: %+v", engine.lastReq) } } func TestReportingHandlers_GenerateReportUsesOrgMonitorMetricsStore(t *testing.T) { baseDir := t.TempDir() baseCfg := &config.Config{ DataPath: baseDir, ConfigPath: baseDir, } mtm := monitoring.NewMultiTenantMonitor(baseCfg, config.NewMultiTenantPersistence(baseDir), nil) t.Cleanup(mtm.Stop) defaultMonitor, err := mtm.GetMonitor("default") if err != nil { t.Fatalf("get default monitor: %v", err) } orgMonitor, err := mtm.GetMonitor("org-1") if err != nil { t.Fatalf("get org monitor: %v", err) } pointTime := time.Now().Add(-30 * time.Minute).UTC().Truncate(time.Second) defaultMonitor.GetMetricsStore().Write("node", "node-1", "cpu", 1111, pointTime) defaultMonitor.GetMetricsStore().Flush() orgMonitor.GetMetricsStore().Write("node", "node-1", "cpu", 4242, pointTime) orgMonitor.GetMetricsStore().Flush() original := reporting.GetEngine() reporting.SetEngine(reporting.NewReportEngine(reporting.EngineConfig{ MetricsStoreGetter: defaultMonitor.GetMetricsStore, })) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(mtm) query := url.Values{ "format": []string{"csv"}, "resourceType": []string{"node"}, "resourceId": []string{"node-1"}, "metricType": []string{"cpu"}, "start": []string{pointTime.Add(-5 * time.Minute).Format(time.RFC3339)}, "end": []string{pointTime.Add(5 * time.Minute).Format(time.RFC3339)}, } req := httptest.NewRequest(http.MethodGet, "/api/reporting?"+query.Encode(), nil) req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "org-1")) rr := httptest.NewRecorder() handler.HandleGenerateReport(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) } body := rr.Body.String() if !strings.Contains(body, "4242.00") { t.Fatalf("expected org-specific metric value in report, got %q", body) } if strings.Contains(body, "1111.00") { t.Fatalf("expected report to avoid default monitor metrics, got %q", body) } } func TestSanitizeFilename(t *testing.T) { raw := "\"bad/../name\\\r\n" got := sanitizeFilename(raw) if strings.ContainsAny(got, "\"\\/\r\n") { t.Fatalf("sanitizeFilename did not remove unsafe characters: %q", got) } }