package accountant import ( "bytes" "crypto/ed25519" "encoding/json" "net/http" "path/filepath" "testing" "time" "sultan/pkg/auth" "sultan/pkg/crypto" "sultan/pkg/ledger" ) // enrolledPayee registriert einen Payee und liefert seinen Schlüssel. func enrolledPayee(t *testing.T, a *Accountant, id string) ed25519.PrivateKey { t.Helper() key, _ := auth.GenerateKey() tok := invite(t, a, id) if resp := enroll(t, a, key, EnrollRequest{Token: tok.Secret, Hostname: id + ".lan"}); resp.StatusCode != http.StatusCreated { t.Fatalf("Enrollment: %d", resp.StatusCode) } return key } func postReport(t *testing.T, a *Accountant, key ed25519.PrivateKey, body []byte) *http.Response { t.Helper() req, _ := http.NewRequest(http.MethodPost, "https://"+a.Addr()+PathReport, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") if key != nil { if err := (&auth.Signer{Key: key}).Sign(req); err != nil { t.Fatal(err) } } hc := &http.Client{Transport: &http.Transport{TLSClientConfig: crypto.PinnedTLSConfig(a.Fingerprint())}} resp, err := hc.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } func latest(t *testing.T, a *Accountant, payeeID string) (Report, time.Time, bool) { t.Helper() l, err := ledger.Open(ctx, filepath.Join(a.DataDir(), "accountant.db")) if err != nil { t.Fatal(err) } defer l.Close() body, at, err := l.LatestReport(ctx, payeeID) if err != nil { return Report{}, time.Time{}, false } var r Report if err := json.Unmarshal(body, &r); err != nil { t.Fatalf("gespeicherter Bericht unlesbar: %v", err) } return r, at, true } func sampleReport() Report { return Report{ Version: "v0.1.0", GeneratedAt: time.Date(2026, 10, 17, 9, 0, 0, 0, time.UTC), Tenants: []ReportTenant{{ID: "kunde", Name: "Kunde A", Tier: "pro", Status: "active"}}, Nodes: []ReportNode{{ID: "webdocker11", Tenant: "kunde", Hostname: "webdocker11", FQDN: "webdocker11.lan.r25dc.de", IP: "10.0.0.11", Status: "online", Connected: true}}, Routes: []ReportRoute{{Tenant: "kunde", Node: "webdocker11", Workload: "kunde-a-wordpress-1", Router: "wp", Protocol: "http", Hosts: []string{"kunde.de"}, Backend: "http://10.0.0.11:8081", BackendFQDN: "webdocker11.lan.r25dc.de:8081", Active: true, ComposeDir: "/srv/kunde-a"}}, } } // Ein registrierter Payee meldet seinen Schnappschuss; gespeichert wird der // letzte. func TestReport_StoresLatest(t *testing.T) { a := start(t, "") key := enrolledPayee(t, a, "webproxy11") first := sampleReport() body, _ := json.Marshal(first) if resp := postReport(t, a, key, body); resp.StatusCode != http.StatusNoContent { t.Fatalf("Status %d", resp.StatusCode) } got, _, ok := latest(t, a, "webproxy11") if !ok || got.Routes[0].BackendFQDN != "webdocker11.lan.r25dc.de:8081" || got.Nodes[0].FQDN != "webdocker11.lan.r25dc.de" { t.Errorf("gespeichert: %+v", got) } second := sampleReport() second.Routes = nil body, _ = json.Marshal(second) postReport(t, a, key, body) if got, _, _ := latest(t, a, "webproxy11"); len(got.Routes) != 0 { t.Errorf("Bericht nicht ersetzt: %+v", got.Routes) } } func TestReport_Errors(t *testing.T) { a := start(t, "") key := enrolledPayee(t, a, "webproxy11") stranger, _ := auth.GenerateKey() valid, _ := json.Marshal(sampleReport()) tests := []struct { name string key ed25519.PrivateKey body []byte want int }{ {"unsigniert", nil, valid, http.StatusUnauthorized}, {"nicht registrierter Schlüssel", stranger, valid, http.StatusUnauthorized}, {"kein JSON", key, []byte("kaputt"), http.StatusBadRequest}, {"kein Zeitpunkt", key, []byte(`{"tenants":[]}`), http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if resp := postReport(t, a, tt.key, tt.body); resp.StatusCode != tt.want { t.Errorf("Status %d, erwartet %d", resp.StatusCode, tt.want) } }) } if _, _, ok := latest(t, a, "webproxy11"); ok { t.Error("fehlerhafter Bericht gespeichert") } } // Große Hoster: Berichte bis 8 MiB werden angenommen. func TestReport_LargeBody(t *testing.T) { a := start(t, "") key := enrolledPayee(t, a, "webproxy11") r := sampleReport() for i := 0; i < 6000; i++ { r.Routes = append(r.Routes, r.Routes[0]) } body, _ := json.Marshal(r) if len(body) < 1<<20 { t.Fatalf("Testbericht nur %d Bytes", len(body)) } if resp := postReport(t, a, key, body); resp.StatusCode != http.StatusNoContent { t.Errorf("großer Bericht: Status %d", resp.StatusCode) } }