package accountant import ( "encoding/json" "io" "log/slog" "net/http" "os" "path/filepath" "regexp" "strings" "testing" "time" "sultan/pkg/ledger" ) var cmkNow = time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC) func cmkReport(routes ...ReportRoute) Report { return Report{ GeneratedAt: cmkNow, Nodes: []ReportNode{ {ID: "webdocker11", Tenant: "kunde", Connected: true}, {ID: "alt", Tenant: "kunde", Connected: false}, {ID: "wartung", Tenant: "kunde", Connected: false, Maintenance: true}, }, Routes: routes, } } func okRoute(host string) ReportRoute { return ReportRoute{Tenant: "kunde", Node: "webdocker11", Workload: "wp", Router: "wp", Protocol: "http", Hosts: []string{host}, Backend: "http://10.0.0.11:8081", BackendFQDN: "webdocker11.lan:8081", Active: true, ComposeDir: "/srv/kunde-a", Health: "healthy", State: "running"} } func findURL(t *testing.T, d CheckmkData, service string) CheckmkURL { t.Helper() for _, u := range d.URLs { if u.Service == service { return u } } t.Fatalf("Service %q fehlt in %+v", service, d.URLs) return CheckmkURL{} } // Zustände: OK, WARN bei Problemen der Route, CRIT bei veraltetem Bericht; // Wartung dämpft auf OK. func TestBuildCheckmk_States(t *testing.T) { inactive := okRoute("fremd.de") inactive.Active = false down := okRoute("olat.kunde.de") down.Down = true sick := okRoute("krank.kunde.de") sick.Health = "unhealthy" gone := okRoute("weg.kunde.de") gone.Node = "alt" maint := okRoute("pause.kunde.de") maint.Node, maint.Down = "wartung", true multi := okRoute("kunde.de") multi.Hosts = []string{"kunde.de", "www.kunde.de"} payees := []ledger.Payee{ {ID: "frisch", Hostname: "webproxy11", LastReport: cmkNow.Add(-time.Minute)}, {ID: "lahm", Hostname: "webproxy12", LastReport: cmkNow.Add(-3 * time.Hour)}, {ID: "tot", Hostname: "webproxy13", LastReport: cmkNow.Add(-7 * time.Hour)}, {ID: "neu", Hostname: "webproxy14"}, } reports := map[string]Report{ "frisch": cmkReport(okRoute("ok.kunde.de"), inactive, down, sick, gone, maint, multi), "lahm": cmkReport(okRoute("lahm.kunde.de")), "tot": cmkReport(okRoute("tot.kunde.de")), } d := buildCheckmk(payees, reports, cmkNow, 2*time.Hour, 6*time.Hour) wantPayees := map[string]int{"frisch": 0, "lahm": 1, "tot": 2, "neu": 1} for _, p := range d.Payees { if p.State != wantPayees[p.ID] { t.Errorf("Payee %s: Status %d, erwartet %d (%+v)", p.ID, p.State, wantPayees[p.ID], p) } } if len(d.Payees) != 4 { t.Errorf("Payees: %+v", d.Payees) } tests := []struct { service, problem string state int }{ {"Sultan URL ok.kunde.de", "", 0}, {"Sultan URL fremd.de", "von Policy verworfen", 1}, {"Sultan URL olat.kunde.de", "gestoppt", 1}, {"Sultan URL krank.kunde.de", "unhealthy", 1}, {"Sultan URL weg.kunde.de", "Knoten getrennt", 1}, {"Sultan URL pause.kunde.de", "Wartung", 0}, {"Sultan URL kunde.de", "", 0}, {"Sultan URL www.kunde.de", "", 0}, {"Sultan URL lahm.kunde.de", "Bericht", 1}, {"Sultan URL tot.kunde.de", "Bericht", 2}, } for _, tt := range tests { u := findURL(t, d, tt.service) if u.State != tt.state || (tt.problem == "") != (u.Problem == "") || !strings.Contains(u.Problem, tt.problem) { t.Errorf("%s: Status %d Problem %q, erwartet %d %q", tt.service, u.State, u.Problem, tt.state, tt.problem) } } u := findURL(t, d, "Sultan URL ok.kunde.de") if u.URL != "https://ok.kunde.de" || u.BackendURL != "http://webdocker11.lan:8081" || u.Payee != "frisch" || u.Tenant != "kunde" || u.ComposeDir != "/srv/kunde-a" { t.Errorf("Felder: %+v", u) } for i := 1; i < len(d.URLs); i++ { if d.URLs[i-1].Service > d.URLs[i].Service { t.Errorf("nicht sortiert: %s vor %s", d.URLs[i-1].Service, d.URLs[i].Service) } } } // Derselbe Host zweimal (z. B. zwei Payees): eindeutige Servicenamen. func TestBuildCheckmk_DuplicateHosts(t *testing.T) { payees := []ledger.Payee{{ID: "a", LastReport: cmkNow}, {ID: "b", LastReport: cmkNow}} reports := map[string]Report{"a": cmkReport(okRoute("kunde.de")), "b": cmkReport(okRoute("kunde.de"))} d := buildCheckmk(payees, reports, cmkNow, 2*time.Hour, 6*time.Hour) if len(d.URLs) != 2 || d.URLs[0].Service == d.URLs[1].Service { t.Errorf("Servicenamen nicht eindeutig: %+v", d.URLs) } } // Werte vom Kunden-Host sind Fremdeingaben: einzeilig, ASCII, ohne | und ", // HTML-escaped; Unescaped nur bereinigt. func TestBuildCheckmk_Sanitizes(t *testing.T) { evil := okRoute("kunde.de") evil.ComposeDir = "/srv/\n2 \"Fake\" - OK |x" evil.Tenant = "küche" d := buildCheckmk([]ledger.Payee{{ID: "a", Hostname: "h", LastReport: cmkNow}}, map[string]Report{"a": cmkReport(evil)}, cmkNow, 2*time.Hour, 6*time.Hour) u := d.URLs[0] for _, s := range []string{u.ComposeDir, u.Unescaped.ComposeDir, d.Payees[0].Hostname} { if strings.ContainsAny(s, "\n\r|\"") { t.Errorf("nicht bereinigt: %q", s) } } if strings.Contains(u.ComposeDir, "