Files

145 lines
4.4 KiB
Go

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)
}
}