software steht so weit.

jetzt build + docker build und testen
This commit is contained in:
br committed 2026-10-02 13:25:34 +02:00
1 parent 42f0c0accb
commit ca6cdcca63
90 files changed
+10298 -480

No files matched your search

+143 -1
View File
@@ -2,6 +2,7 @@ package policy
import (
"errors"
"net/url"
"reflect"
"testing"
"time"
@@ -296,7 +297,10 @@ func TestStageFor(t *testing.T) {
}{
{"Community aktiv", license.TierCommunity, store.TenantActive, 0, false, StageNormal},
{"Community ohne Mahnwesen", license.TierCommunity, store.TenantActive, 2, false, StageNormal},
{"Community abgelaufen", license.TierCommunity, store.TenantActive, 0, true, StageLanding},
// Community wird automatisch neu lizenziert (5c): kein Ablauf über valid_until …
{"Community mit altem valid_until", license.TierCommunity, store.TenantActive, 0, true, StageNormal},
// … nur der explizite Status expired führt zur Landingpage.
{"Community explizit abgelaufen", license.TierCommunity, store.TenantExpired, 0, false, StageLanding},
{"Pro aktiv", license.TierPro, store.TenantActive, 0, false, StageNormal},
{"Mahnstufe 1", license.TierPro, store.TenantActive, 1, false, StageWatermark},
{"Mahnstufe 2", license.TierPro, store.TenantActive, 2, false, StageFiremark},
@@ -585,3 +589,141 @@ func TestValidID(t *testing.T) {
}
}
}
func headersOf(t *testing.T, res Result, router string) map[string]any {
t.Helper()
mw := res.Config.HTTP.Middlewares["sultan__kunde__node-1__"+router]
if mw == nil {
t.Fatalf("keine Interceptor-Middleware für %s", router)
}
return mw["headers"].(map[string]any)["customRequestHeaders"].(map[string]any)
}
func TestApply_NodeMaintenance(t *testing.T) {
in := input(t, wpLabels())
until := now.Add(2 * time.Hour)
in.Maintenance = &Maintenance{Until: until, PublicReason: "Sicherheitsupdate (CVE-1234) – Sicherheit geht vor."}
res, err := engine().Apply(in)
if err != nil {
t.Fatal(err)
}
if res.Config.HTTP.Routers["kunde__node-1__wp"].Service != InterceptorService {
t.Fatal("Knoten in Wartung nicht zum Interceptor umgeleitet")
}
h := headersOf(t, res, "wp")
if h[HeaderStage] != string(StageMaintenance) || h[HeaderUntil] != until.Format(time.RFC3339) {
t.Errorf("Header: %v", h)
}
// Öffentlicher Grund URL-kodiert (Header sind einzeilig, Traefik reicht Bytes nicht zuverlässig durch).
if h[HeaderPublicReason] != url.QueryEscape("Sicherheitsupdate (CVE-1234) – Sicherheit geht vor.") {
t.Errorf("öffentlicher Grund: %v", h[HeaderPublicReason])
}
}
func TestApply_NodeMaintenanceUnlimitedAndExpired(t *testing.T) {
in := input(t, wpLabels())
in.Maintenance = &Maintenance{} // unbefristet, ohne Grund
res, _ := engine().Apply(in)
h := headersOf(t, res, "wp")
if _, ok := h[HeaderUntil]; ok {
t.Error("unbefristete Wartung mit Ende-Header")
}
if _, ok := h[HeaderPublicReason]; ok {
t.Error("Wartung ohne öffentlichen Grund mit Grund-Header")
}
in.Maintenance = &Maintenance{Until: now} // gerade abgelaufen
res, _ = engine().Apply(in)
if res.Config.HTTP.Routers["kunde__node-1__wp"].Service == InterceptorService {
t.Error("abgelaufene Wartung leitet noch um")
}
}
func TestApply_ServiceMaintenance(t *testing.T) {
labels := wpLabels()
labels["traefik.http.routers.wp.service"] = "wp"
labels["traefik.http.routers.shop.rule"] = "Host(`shop.kunde.de`)"
labels["traefik.http.routers.shop.service"] = "shop"
labels["traefik.http.services.shop.loadbalancer.server.port"] = "8080"
in := input(t, labels)
in.ServiceMaintenance = map[string]Maintenance{"shop": {PublicReason: "Inventur"}}
res, _ := engine().Apply(in)
if res.Config.HTTP.Routers["kunde__node-1__shop"].Service != InterceptorService {
t.Error("Dienst in Wartung nicht umgeleitet")
}
if res.Config.HTTP.Routers["kunde__node-1__wp"].Service == InterceptorService {
t.Error("anderer Dienst wurde umgeleitet")
}
if h := headersOf(t, res, "shop"); h[HeaderPublicReason] != "Inventur" {
t.Errorf("Header: %v", h)
}
}
// Vorrang: Sperre/drain > Ablauf/Paywall > Wartung > Mahnhinweis > normal.
func TestApply_MaintenancePrecedence(t *testing.T) {
tests := []struct {
name string
mutate func(*Input)
stage Stage // Stufe am Router; "" = keine Routen
ackSeen bool
}{
{"Wartung schlägt Mahnstufe 1", func(in *Input) { in.Tenant.DunningLevel = 1 }, StageMaintenance, false},
{"Paywall schlägt Wartung", func(in *Input) { in.Tenant.Status = store.TenantSuspendedPayment }, StagePaywall, false},
{"Ablauf schlägt Wartung", func(in *Input) { in.Tenant.Status = store.TenantExpired }, StageLanding, false},
{"drain schlägt Wartung", func(in *Input) { in.NodeDrained = true }, "", false},
{"Quarantäne schlägt Wartung", func(in *Input) { in.Tenant.Status = store.TenantQuarantined }, "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
in := input(t, wpLabels())
in.Maintenance = &Maintenance{}
tt.mutate(&in)
res, err := engine().Apply(in)
if err != nil {
t.Fatal(err)
}
if tt.stage == "" {
if res.Config.HTTP != nil {
t.Errorf("erwartet keine Routen: %v", keys(res.Config.HTTP.Routers))
}
return
}
if h := headersOf(t, res, "wp"); h[HeaderStage] != string(tt.stage) {
t.Errorf("Stufe %v, erwartet %s", h[HeaderStage], tt.stage)
}
if _, ack := res.Config.HTTP.Routers["sultan__kunde__node-1__wp"]; ack != tt.ackSeen {
t.Errorf("Durchlass-Router vorhanden=%v", ack)
}
})
}
}
// Ungeplant getrennt (Lease noch gültig): Wartungsseite ohne Ende und ohne Grund.
func TestApply_DisconnectedNode(t *testing.T) {
in := input(t, wpLabels())
in.Disconnected = true
res, _ := engine().Apply(in)
h := headersOf(t, res, "wp")
if h[HeaderStage] != string(StageMaintenance) {
t.Errorf("Stufe %v", h[HeaderStage])
}
if _, ok := h[HeaderUntil]; ok {
t.Error("getrennter Knoten mit Ende-Header")
}
// Angemeldete Wartung hat Vorrang (deren Grund/Ende wird gezeigt).
in.Maintenance = &Maintenance{PublicReason: "Reboot nach Update"}
res, _ = engine().Apply(in)
if h := headersOf(t, res, "wp"); h[HeaderPublicReason] != url.QueryEscape("Reboot nach Update") {
t.Errorf("Grund der Wartung fehlt: %v", h)
}
// Paywall bleibt Paywall.
in.Maintenance = nil
in.Tenant.Status = store.TenantSuspendedPayment
res, _ = engine().Apply(in)
if h := headersOf(t, res, "wp"); h[HeaderStage] != string(StagePaywall) {
t.Errorf("Paywall verdrängt: %v", h[HeaderStage])
}
}