package accountant import ( "encoding/json" "path/filepath" "testing" "time" "sultan/pkg/ledger" ) // whereLedger: zwei Payees; kunde.de zieht von webdocker07 nach webdocker11 um, // olat.kunde.de verschwindet, andere.de liegt auf dem zweiten Payee. func whereLedger(t *testing.T) (*ledger.Ledger, func(time.Time)) { t.Helper() l, err := ledger.Open(ctx, filepath.Join(t.TempDir(), "accountant.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { l.Close() }) now := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC) setNow := func(at time.Time) { now = at } l.SetClock(func() time.Time { return now }) for _, id := range []string{"webproxy11", "webproxy12"} { l.AddInvite(ctx, "tok-"+id, id, now.Add(time.Hour)) if _, err := l.Enroll(ctx, "tok-"+id, ledger.Payee{Hostname: id + ".lan", PublicKey: "key-" + id}); err != nil { t.Fatal(err) } } return l, setNow } func save(t *testing.T, l *ledger.Ledger, payeeID string, rep Report) { t.Helper() if err := StoreReport(ctx, l, payeeID, rep); err != nil { t.Fatal(err) } } func whereReport(node string, routes ...ReportRoute) Report { return Report{ GeneratedAt: time.Now(), Tenants: []ReportTenant{{ID: "kunde", Name: "Kunde A"}, {ID: "andere", Name: "Andere GmbH"}}, Nodes: []ReportNode{{ID: node, Tenant: "kunde", FQDN: node + ".lan.r25dc.de", Connected: true, OS: "Debian 13", DockerVersion: "29.8.2", NetBirdIP: "100.92.10.4"}}, Routes: routes, } } func route(tenant, node, router string, hosts ...string) ReportRoute { return ReportRoute{Tenant: tenant, Node: node, Router: router, Protocol: "http", Hosts: hosts, Workload: "kunde-a-" + router + "-1", Backend: "http://10.0.0.11:8081", BackendFQDN: node + ".lan.r25dc.de:8081", Active: true, ComposeDir: "/srv/kunde-a", ComposeProject: "kunde-a", State: "running", Health: "healthy"} } func TestWhere(t *testing.T) { l, setNow := whereLedger(t) t1 := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC) save(t, l, "webproxy11", whereReport("webdocker07", route("kunde", "webdocker07", "wp", "kunde.de"), route("kunde", "webdocker07", "olat", "olat.kunde.de"))) andere := whereReport("n9", route("andere", "n9", "web", "andere.de")) save(t, l, "webproxy12", andere) t2 := t1.Add(30 * 24 * time.Hour) setNow(t2) save(t, l, "webproxy11", whereReport("webdocker11", route("kunde", "webdocker11", "wp", "kunde.de", "www.kunde.de"))) res, err := Where(ctx, l, "kunde.de") if err != nil { t.Fatal(err) } if len(res.Current) != 1 { t.Fatalf("aktuell: %+v", res.Current) } h := res.Current[0] if h.Payee != "webproxy11" || h.Route.Router != "wp" || h.TenantName != "Kunde A" || h.Node.FQDN != "webdocker11.lan.r25dc.de" || h.Route.ComposeDir != "/srv/kunde-a" || !h.Since.Equal(t2) || !h.ReportAt.Equal(t2) { t.Errorf("Treffer: %+v", h) } if len(h.Previous) != 1 || h.Previous[0].Node != "webdocker07" || !h.Previous[0].Ended.Equal(t2) || !h.Previous[0].FirstSeen.Equal(t1) { t.Errorf("vorher: %+v", h.Previous) } // olat.kunde.de passt (Teiltreffer "kunde.de"), ist aber verschwunden. if len(res.Gone) != 1 || res.Gone[0].Router != "olat" || !res.Gone[0].Ended.Equal(t2) { t.Errorf("verschwunden: %+v", res.Gone) } // Teiltreffer und Mandant. if r, _ := Where(ctx, l, "WWW.Kunde"); len(r.Current) != 1 { t.Errorf("Teiltreffer: %+v", r) } if r, _ := Where(ctx, l, "andere"); len(r.Current) != 1 || r.Current[0].Payee != "webproxy12" { t.Errorf("Mandant: %+v", r) } if r, _ := Where(ctx, l, "gibts-nicht.de"); len(r.Current)+len(r.Gone) != 0 { t.Errorf("kein Treffer erwartet: %+v", r) } if r, _ := Where(ctx, l, "_"); len(r.Current)+len(r.Gone) != 0 { t.Errorf("Platzhalter wirkt als Joker: %+v", r) } } // Der Bericht geht über den Endpunkt in die Historie ein. func TestReport_RecordsHistory(t *testing.T) { a := start(t, "") key := enrolledPayee(t, a, "webproxy11") body, _ := json.Marshal(whereReport("webdocker11", route("kunde", "webdocker11", "wp", "kunde.de"))) postReport(t, a, key, body) l, _ := ledger.Open(ctx, filepath.Join(a.DataDir(), "accountant.db")) defer l.Close() spans, err := l.RouteHistory(ctx, ledger.HistoryFilter{}) if err != nil || len(spans) != 1 || spans[0].Payee != "webproxy11" || spans[0].BackendFQDN != "webdocker11.lan.r25dc.de:8081" { t.Errorf("Historie: %+v, %v", spans, err) } } func TestRoutes(t *testing.T) { l, _ := whereLedger(t) save(t, l, "webproxy11", whereReport("webdocker11", route("kunde", "webdocker11", "wp", "kunde.de"), route("kunde", "webdocker11", "olat", "olat.kunde.de"))) save(t, l, "webproxy12", whereReport("n9", route("andere", "n9", "web", "andere.de"))) all, err := Routes(ctx, l, RoutesFilter{}) if err != nil || len(all) != 3 { t.Fatalf("Routes = %+v, %v", all, err) } if all[0].Route.Hosts[0] != "andere.de" || all[2].Route.Hosts[0] != "olat.kunde.de" { t.Errorf("nicht nach Host sortiert: %v %v %v", all[0].Route.Hosts, all[1].Route.Hosts, all[2].Route.Hosts) } if r, _ := Routes(ctx, l, RoutesFilter{Tenant: "kunde"}); len(r) != 2 { t.Errorf("--tenant: %+v", r) } if r, _ := Routes(ctx, l, RoutesFilter{Payee: "webproxy12"}); len(r) != 1 || r[0].Since.IsZero() { t.Errorf("--payee: %+v", r) } }