145 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|