281 lines
9.9 KiB
Go
281 lines
9.9 KiB
Go
package ledger
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sultan/pkg/store"
|
|
)
|
|
|
|
var ctx = context.Background()
|
|
|
|
func open(t *testing.T) *Ledger {
|
|
t.Helper()
|
|
l, err := Open(ctx, filepath.Join(t.TempDir(), "accountant.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { l.Close() })
|
|
return l
|
|
}
|
|
|
|
func payee(id, key string) Payee {
|
|
return Payee{ID: id, Hostname: id + ".lan", PublicKey: key, IPAddress: "10.0.0.1"}
|
|
}
|
|
|
|
// Einladung → Enrollment legt den Payee an; die Einladung bestimmt die ID.
|
|
func TestEnroll(t *testing.T) {
|
|
l := open(t)
|
|
now := time.Date(2026, 10, 16, 9, 0, 0, 0, time.UTC)
|
|
l.now = func() time.Time { return now }
|
|
if err := l.AddInvite(ctx, "sltn_enroll_a", "webproxy11", now.Add(time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := payee("ignoriert", "key-a")
|
|
got, err := l.Enroll(ctx, "sltn_enroll_a", p)
|
|
if err != nil {
|
|
t.Fatalf("Enroll: %v", err)
|
|
}
|
|
want := Payee{ID: "webproxy11", Hostname: "ignoriert.lan", PublicKey: "key-a", IPAddress: "10.0.0.1", EnrolledAt: now}
|
|
if got != want {
|
|
t.Errorf("Enroll = %+v, erwartet %+v", got, want)
|
|
}
|
|
list, err := l.ListPayees(ctx)
|
|
if err != nil || len(list) != 1 || list[0] != want {
|
|
t.Errorf("ListPayees = %+v, %v", list, err)
|
|
}
|
|
if byKey, err := l.GetPayeeByPublicKey(ctx, "key-a"); err != nil || byKey != want {
|
|
t.Errorf("GetPayeeByPublicKey = %+v, %v", byKey, err)
|
|
}
|
|
if _, err := l.GetPayeeByPublicKey(ctx, "fremd"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Errorf("unbekannter Schlüssel: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnroll_Errors(t *testing.T) {
|
|
l := open(t)
|
|
now := time.Date(2026, 10, 16, 9, 0, 0, 0, time.UTC)
|
|
l.now = func() time.Time { return now }
|
|
l.AddInvite(ctx, "tok-1", "webproxy11", now.Add(time.Hour))
|
|
l.AddInvite(ctx, "tok-2", "webproxy12", now.Add(time.Hour))
|
|
l.AddInvite(ctx, "tok-alt", "webproxy13", now.Add(time.Minute))
|
|
if _, err := l.Enroll(ctx, "tok-1", payee("x", "key-1")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := l.Enroll(ctx, "gibts-nicht", payee("x", "key-9")); !errors.Is(err, store.ErrNotFound) {
|
|
t.Errorf("unbekannte Einladung: %v", err)
|
|
}
|
|
if _, err := l.Enroll(ctx, "tok-1", payee("x", "key-8")); !errors.Is(err, store.ErrTokenUsed) {
|
|
t.Errorf("verbrauchte Einladung: %v", err)
|
|
}
|
|
// Schlüssel schon vergeben: Einladung bleibt unverbraucht.
|
|
if _, err := l.Enroll(ctx, "tok-2", payee("x", "key-1")); !errors.Is(err, store.ErrExists) {
|
|
t.Errorf("doppelter Schlüssel: %v", err)
|
|
}
|
|
if _, err := l.Enroll(ctx, "tok-2", payee("x", "key-2")); err != nil {
|
|
t.Errorf("Einladung nach Fehlschlag verbraucht: %v", err)
|
|
}
|
|
now = now.Add(2 * time.Minute)
|
|
if _, err := l.Enroll(ctx, "tok-alt", payee("x", "key-3")); !errors.Is(err, store.ErrTokenExpired) {
|
|
t.Errorf("abgelaufene Einladung: %v", err)
|
|
}
|
|
if _, err := l.Enroll(ctx, "tok-x", Payee{ID: "x", PublicKey: "k"}); err == nil {
|
|
t.Error("Payee ohne Hostname akzeptiert")
|
|
}
|
|
}
|
|
|
|
func TestAddInvite_Validation(t *testing.T) {
|
|
l := open(t)
|
|
future := time.Now().Add(time.Hour)
|
|
l.AddInvite(ctx, "tok-1", "webproxy11", future)
|
|
l.Enroll(ctx, "tok-1", payee("x", "key-1"))
|
|
tests := []struct {
|
|
name, token, id string
|
|
expires time.Time
|
|
want error
|
|
}{
|
|
{"ungültige ID", "t", "Web Proxy", future, store.ErrInvalid},
|
|
{"doppelter Unterstrich", "t", "a__b", future, store.ErrInvalid},
|
|
{"ohne Token", "", "webproxy12", future, store.ErrInvalid},
|
|
{"Ablauf in der Vergangenheit", "t", "webproxy12", time.Now().Add(-time.Minute), store.ErrInvalid},
|
|
{"Payee schon registriert", "t", "webproxy11", future, store.ErrExists},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if err := l.AddInvite(ctx, tt.token, tt.id, tt.expires); !errors.Is(err, tt.want) {
|
|
t.Errorf("AddInvite = %v, erwartet %v", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeletePayee(t *testing.T) {
|
|
l := open(t)
|
|
l.AddInvite(ctx, "tok-1", "webproxy11", time.Now().Add(time.Hour))
|
|
l.Enroll(ctx, "tok-1", payee("x", "key-1"))
|
|
if err := l.DeletePayee(ctx, "webproxy11"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if list, _ := l.ListPayees(ctx); len(list) != 0 {
|
|
t.Errorf("nach Löschen: %+v", list)
|
|
}
|
|
if err := l.DeletePayee(ctx, "webproxy11"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Errorf("zweites Löschen: %v", err)
|
|
}
|
|
// Neu einladen (z. B. nach Verlust von payee.key).
|
|
if err := l.AddInvite(ctx, "tok-2", "webproxy11", time.Now().Add(time.Hour)); err != nil {
|
|
t.Errorf("neue Einladung nach Löschen: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSettings(t *testing.T) {
|
|
l := open(t)
|
|
if v, err := l.Setting(ctx, "public_urls"); err != nil || v != "" {
|
|
t.Errorf("leer: %q, %v", v, err)
|
|
}
|
|
l.SetSetting(ctx, "public_urls", `["https://a:8444"]`)
|
|
l.SetSetting(ctx, "public_urls", `["https://b:8444"]`)
|
|
if v, _ := l.Setting(ctx, "public_urls"); v != `["https://b:8444"]` {
|
|
t.Errorf("Setting = %q", v)
|
|
}
|
|
}
|
|
|
|
// Der jeweils letzte Bericht je Payee; ListPayees zeigt seinen Zeitpunkt,
|
|
// Löschen des Payees entfernt ihn mit.
|
|
func TestReports(t *testing.T) {
|
|
l := open(t)
|
|
now := time.Date(2026, 10, 17, 9, 0, 0, 0, time.UTC)
|
|
l.now = func() time.Time { return now }
|
|
l.AddInvite(ctx, "tok-1", "webproxy11", now.Add(time.Hour))
|
|
l.Enroll(ctx, "tok-1", payee("x", "key-1"))
|
|
|
|
if _, _, err := l.LatestReport(ctx, "webproxy11"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Errorf("ohne Bericht: %v", err)
|
|
}
|
|
if err := l.SaveReport(ctx, "webproxy11", []byte(`{"v":1}`), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = now.Add(time.Minute)
|
|
if err := l.SaveReport(ctx, "webproxy11", []byte(`{"v":2}`), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, at, err := l.LatestReport(ctx, "webproxy11")
|
|
if err != nil || string(body) != `{"v":2}` || !at.Equal(now) {
|
|
t.Errorf("LatestReport = %s, %v, %v", body, at, err)
|
|
}
|
|
list, _ := l.ListPayees(ctx)
|
|
if len(list) != 1 || !list[0].LastReport.Equal(now) {
|
|
t.Errorf("ListPayees = %+v", list)
|
|
}
|
|
if err := l.SaveReport(ctx, "gibts-nicht", []byte(`{}`), nil); !errors.Is(err, store.ErrNotFound) {
|
|
t.Errorf("Bericht für unbekannten Payee: %v", err)
|
|
}
|
|
l.DeletePayee(ctx, "webproxy11")
|
|
if _, _, err := l.LatestReport(ctx, "webproxy11"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Errorf("Bericht nach Löschen des Payees: %v", err)
|
|
}
|
|
}
|
|
|
|
func loc(tenant, node, router string, hosts ...string) RouteRecord {
|
|
return RouteRecord{Tenant: tenant, Node: node, Protocol: "http", Router: router, Hosts: hosts,
|
|
Backend: "http://10.0.0.11:8081", BackendFQDN: node + ".lan:8081", Workload: router, ComposeDir: "/srv/" + router}
|
|
}
|
|
|
|
// Jeder Bericht führt die Historie fort: neue Orte öffnen, fehlende enden,
|
|
// bestehende behalten first_seen und übernehmen geänderte Angaben.
|
|
func TestRouteHistory(t *testing.T) {
|
|
l := open(t)
|
|
t1 := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
|
|
now := t1
|
|
l.now = func() time.Time { return now }
|
|
for _, id := range []string{"webproxy11", "webproxy12"} {
|
|
l.AddInvite(ctx, "tok-"+id, id, now.Add(time.Hour))
|
|
l.Enroll(ctx, "tok-"+id, payee("x", "key-"+id))
|
|
}
|
|
|
|
if err := l.SaveReport(ctx, "webproxy11", []byte(`{}`), []RouteRecord{
|
|
loc("kunde", "webdocker07", "wp", "kunde.de"), loc("kunde", "webdocker07", "olat", "olat.kunde.de"),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l.SaveReport(ctx, "webproxy12", []byte(`{}`), []RouteRecord{loc("andere", "n9", "web", "andere.de")})
|
|
|
|
now = t1.Add(24 * time.Hour)
|
|
moved := loc("kunde", "webdocker11", "wp", "kunde.de", "www.kunde.de") // Umzug + neuer Host
|
|
olat := loc("kunde", "webdocker07", "olat", "olat.kunde.de", "lms.kunde.de")
|
|
l.SaveReport(ctx, "webproxy11", []byte(`{}`), []RouteRecord{moved, olat})
|
|
|
|
all, err := l.RouteHistory(ctx, HistoryFilter{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
type key struct{ payee, node, router string }
|
|
got := map[key]RouteSpan{}
|
|
for _, s := range all {
|
|
got[key{s.Payee, s.Node, s.Router}] = s
|
|
}
|
|
if len(all) != 4 {
|
|
t.Fatalf("%d Einträge: %+v", len(all), all)
|
|
}
|
|
old := got[key{"webproxy11", "webdocker07", "wp"}]
|
|
if !old.FirstSeen.Equal(t1) || !old.Ended.Equal(now) || !old.LastSeen.Equal(t1) {
|
|
t.Errorf("alter Ort: %+v", old)
|
|
}
|
|
cur := got[key{"webproxy11", "webdocker11", "wp"}]
|
|
if !cur.FirstSeen.Equal(now) || !cur.Ended.IsZero() || strings.Join(cur.Hosts, ",") != "kunde.de,www.kunde.de" {
|
|
t.Errorf("neuer Ort: %+v", cur)
|
|
}
|
|
o := got[key{"webproxy11", "webdocker07", "olat"}]
|
|
if !o.FirstSeen.Equal(t1) || !o.LastSeen.Equal(now) || !o.Ended.IsZero() || len(o.Hosts) != 2 {
|
|
t.Errorf("geänderte Hosts behalten first_seen: %+v", o)
|
|
}
|
|
if other := got[key{"webproxy12", "n9", "web"}]; !other.Ended.IsZero() {
|
|
t.Errorf("Bericht eines Payees beendet Orte eines anderen: %+v", other)
|
|
}
|
|
|
|
// Wieder da: neuer Zeitraum, der alte bleibt beendet.
|
|
now = t1.Add(48 * time.Hour)
|
|
l.SaveReport(ctx, "webproxy11", []byte(`{}`), []RouteRecord{moved, olat, loc("kunde", "webdocker07", "wp", "kunde.de")})
|
|
spans, _ := l.RouteHistory(ctx, HistoryFilter{Host: "kunde.de"})
|
|
var open07 int
|
|
for _, s := range spans {
|
|
if s.Node == "webdocker07" && s.Router == "wp" && s.Ended.IsZero() {
|
|
open07++
|
|
}
|
|
}
|
|
if open07 != 1 {
|
|
t.Errorf("Wiederkehr: %+v", spans)
|
|
}
|
|
|
|
// Filter: Host (Teiltreffer), Mandant, Payee; nur offene.
|
|
if s, _ := l.RouteHistory(ctx, HistoryFilter{Host: "lms"}); len(s) != 1 || s[0].Router != "olat" {
|
|
t.Errorf("Host-Teiltreffer: %+v", s)
|
|
}
|
|
if s, _ := l.RouteHistory(ctx, HistoryFilter{Tenant: "andere"}); len(s) != 1 {
|
|
t.Errorf("Mandant: %+v", s)
|
|
}
|
|
if s, _ := l.RouteHistory(ctx, HistoryFilter{Payee: "webproxy12"}); len(s) != 1 {
|
|
t.Errorf("Payee: %+v", s)
|
|
}
|
|
if s, _ := l.RouteHistory(ctx, HistoryFilter{OpenOnly: true, Tenant: "kunde"}); len(s) != 3 {
|
|
t.Errorf("nur offene: %d %+v", len(s), s)
|
|
}
|
|
}
|
|
|
|
// Ein Payee wird gelöscht: Die Historie bleibt (Abrechnung, Verlauf).
|
|
func TestRouteHistory_SurvivesPayeeDelete(t *testing.T) {
|
|
l := open(t)
|
|
l.AddInvite(ctx, "tok", "webproxy11", time.Now().Add(time.Hour))
|
|
l.Enroll(ctx, "tok", payee("x", "k"))
|
|
l.SaveReport(ctx, "webproxy11", []byte(`{}`), []RouteRecord{loc("kunde", "n1", "wp", "kunde.de")})
|
|
l.DeletePayee(ctx, "webproxy11")
|
|
if s, _ := l.RouteHistory(ctx, HistoryFilter{}); len(s) != 1 {
|
|
t.Errorf("Historie nach Löschen: %+v", s)
|
|
}
|
|
}
|