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, "