Accountant: Inventar, where, Checkmk, Abrechnung
This commit is contained in:
1 parent
9f16148cde
commit
dd93277750
59 files changed
+7522
-113
No files matched your search
@@ -0,0 +1,280 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user