package metricsquality import ( "net/http" "net/http/httptest" "strings" "testing" "time" "scm.bstein.dev/bstein/ananke/internal/metrics" ) // TestExporterEmitsCoreMetrics runs one orchestration or CLI step. // Signature: TestExporterEmitsCoreMetrics(t *testing.T). // Why: keeps exporter metric output stable after moving checks into the // top-level testing module. func TestExporterEmitsCoreMetrics(t *testing.T) { exporter := metrics.New() exporter.UpdateBudget(321) exporter.UpdateSample(metrics.Sample{ Name: "Pyrphoros", Target: "pyrphoros@localhost", OnBattery: true, LowBattery: false, RuntimeSecond: 412, BatteryCharge: 81.5, LoadPercent: 27.4, PowerNominalW: 510, ThresholdSec: 354, Trigger: true, BreachCount: 2, Status: "OB", UpdatedAt: time.Unix(1710000000, 0).UTC(), }) exporter.MarkShutdown("ups-threshold") req := httptest.NewRequest(http.MethodGet, "/metrics", nil) rr := httptest.NewRecorder() exporter.Handler("/metrics").ServeHTTP(rr, req) body := rr.Body.String() mustContain := []string{ "ananke_shutdown_budget_seconds 321", "ananke_shutdown_triggers_total 1", "ananke_ups_on_battery{source=\"Pyrphoros\",target=\"pyrphoros@localhost\"", "ananke_ups_runtime_seconds{source=\"Pyrphoros\",target=\"pyrphoros@localhost\"", "ananke_ups_battery_charge_percent{source=\"Pyrphoros\",target=\"pyrphoros@localhost\"", "ananke_ups_load_percent{source=\"Pyrphoros\",target=\"pyrphoros@localhost\"", "ananke_ups_power_nominal_watts{source=\"Pyrphoros\",target=\"pyrphoros@localhost\"", "ananke_ups_threshold_seconds{source=\"Pyrphoros\",target=\"pyrphoros@localhost\"", } for _, fragment := range mustContain { if !strings.Contains(body, fragment) { t.Fatalf("missing metric fragment %q in output:\n%s", fragment, body) } } } // TestExporterHealthzAndEscaping runs one orchestration or CLI step. // Signature: TestExporterHealthzAndEscaping(t *testing.T). // Why: covers exporter health routing and label escaping from the top-level // testing module so HTTP behavior stays explicit after migration. func TestExporterHealthzAndEscaping(t *testing.T) { exporter := metrics.New() exporter.UpdateSample(metrics.Sample{ Name: `Sta"tera`, Target: `statera\host`, Status: `O"B`, LastError: "x", }) handler := exporter.Handler("/custom") healthReq := httptest.NewRequest(http.MethodGet, "/healthz", nil) healthRR := httptest.NewRecorder() handler.ServeHTTP(healthRR, healthReq) if healthRR.Code != http.StatusOK || strings.TrimSpace(healthRR.Body.String()) != "ok" { t.Fatalf("unexpected health response: code=%d body=%q", healthRR.Code, healthRR.Body.String()) } metricsReq := httptest.NewRequest(http.MethodGet, "/custom", nil) metricsRR := httptest.NewRecorder() handler.ServeHTTP(metricsRR, metricsReq) body := metricsRR.Body.String() if !strings.Contains(body, `source="Sta\\\"tera"`) { t.Fatalf("expected escaped source label, got:\n%s", body) } if !strings.Contains(body, `target="statera\\\\host"`) { t.Fatalf("expected escaped target label, got:\n%s", body) } if !strings.Contains(body, "ananke_ups_error") { t.Fatalf("expected error metric line in output") } } // TestExporterHelperContracts runs one orchestration or CLI step. // Signature: TestExporterHelperContracts(t *testing.T). // Why: preserves direct helper coverage for metric formatting utilities after // removing same-package root tests. func TestExporterHelperContracts(t *testing.T) { if metrics.TestHookBoolNum(true) != 1 || metrics.TestHookBoolNum(false) != 0 { t.Fatalf("unexpected boolNum values") } if got := metrics.TestHookSafeLabelValue(`a"b\c`); got != `a\"b\\c` { t.Fatalf("unexpected escaped string: %q", got) } } // TestExporterEmitsGitOpsMetrics runs one orchestration or CLI step. // Signature: TestExporterEmitsGitOpsMetrics(t *testing.T). // Why: the overview dashboard depends on Ananke-owned Flux object-state metrics // rather than the narrower controller metrics exposed by Flux itself. func TestExporterEmitsGitOpsMetrics(t *testing.T) { exporter := metrics.New() exporter.UpdateGitOpsSnapshot(metrics.GitOpsSnapshot{ UpdatedAt: time.Unix(1710000100, 0).UTC(), ScrapeSuccess: true, FluxSources: []metrics.GitOpsFluxSource{{ Namespace: "flux-system", Name: "flux-system", URL: "ssh://git@example/repo.git", Branch: "main", Revision: "main@sha1:abc123", Ready: true, Reason: "Succeeded", }}, Kustomizations: []metrics.GitOpsKustomization{{ Namespace: "flux-system", Name: "monitoring", Path: "./services/monitoring", SourceNamespace: "flux-system", SourceName: "flux-system", Revision: "main@sha1:abc123", Ready: true, Reason: "ReconciliationSucceeded", }}, HelmReleases: []metrics.GitOpsHelmRelease{{ Namespace: "monitoring", Name: "grafana", Chart: "grafana", Version: "8.5.0", AppVersion: "11.4.0", Revision: "sha256:abc123", Ready: false, Reason: "Progressing", Suspended: true, }}, }) req := httptest.NewRequest(http.MethodGet, "/metrics", nil) rr := httptest.NewRecorder() exporter.Handler("/metrics").ServeHTTP(rr, req) body := rr.Body.String() mustContain := []string{ "ananke_gitops_last_scrape_timestamp_seconds 1710000100", "ananke_gitops_scrape_success 1", `ananke_gitops_flux_source_info{namespace="flux-system",name="flux-system",url="ssh://git@example/repo.git",branch="main",revision="main@sha1:abc123",ready="true",reason="Succeeded"} 1`, `ananke_gitops_kustomization_ready{namespace="flux-system",name="monitoring"} 1`, `ananke_gitops_helmrelease_info{namespace="monitoring",name="grafana",chart="grafana",version="8.5.0",app_version="11.4.0",revision="sha256:abc123",ready="false",reason="Progressing"} 1`, `ananke_gitops_helmrelease_suspended{namespace="monitoring",name="grafana"} 1`, } for _, fragment := range mustContain { if !strings.Contains(body, fragment) { t.Fatalf("missing GitOps metric fragment %q in output:\n%s", fragment, body) } } }