server läuft
This commit is contained in:
1 parent
c8df9d7651
commit
42f0c0accb
21 files changed
+4185
-10
No files matched your search
@@ -0,0 +1,587 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/license"
|
||||
"sultan/pkg/store"
|
||||
"sultan/pkg/traefik"
|
||||
)
|
||||
|
||||
var now = time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
const interceptor = "http://127.0.0.1:8081"
|
||||
|
||||
func engine() *Engine {
|
||||
return &Engine{InterceptorURL: interceptor, now: func() time.Time { return now }}
|
||||
}
|
||||
|
||||
func lic(tier license.Tier, maxRoutes int, features ...license.Feature) license.License {
|
||||
return license.License{
|
||||
TenantID: "kunde",
|
||||
Tier: tier,
|
||||
MaxRoutes: maxRoutes,
|
||||
AllowedDomains: []string{"kunde.de", "*.kunde.de"},
|
||||
Features: features,
|
||||
ValidUntil: now.AddDate(0, 3, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// cfg baut eine Knoten-Konfiguration aus Labels – wie sie der Server vom
|
||||
// Agenten bekommt.
|
||||
func cfg(t *testing.T, labels map[string]string) *traefik.Configuration {
|
||||
t.Helper()
|
||||
labels["traefik.enable"] = "true"
|
||||
c, err := traefik.FromLabels("100.64.0.42", labels)
|
||||
if err != nil {
|
||||
t.Fatalf("FromLabels: %v", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func wpLabels() map[string]string {
|
||||
return map[string]string{
|
||||
"traefik.http.routers.wp.rule": "Host(`kunde.de`) || Host(`www.kunde.de`)",
|
||||
"traefik.http.routers.wp.entrypoints": "websecure",
|
||||
"traefik.http.routers.wp.tls.certresolver": "dns-resolver",
|
||||
"traefik.http.routers.wp.middlewares": "wp-www",
|
||||
"traefik.http.middlewares.wp-www.redirectregex.regex": `^https?://www\.kunde\.de/(.*)`,
|
||||
"traefik.http.middlewares.wp-www.redirectregex.replacement": "https://kunde.de/${1}",
|
||||
"traefik.http.services.wp.loadbalancer.server.port": "80",
|
||||
}
|
||||
}
|
||||
|
||||
func input(t *testing.T, labels map[string]string) Input {
|
||||
return Input{
|
||||
Tenant: Tenant{ID: "kunde", Status: store.TenantActive},
|
||||
License: lic(license.TierPro, 10),
|
||||
NodeID: "node-1",
|
||||
Config: cfg(t, labels),
|
||||
}
|
||||
}
|
||||
|
||||
func kinds(vs []Violation) []ViolationKind {
|
||||
var out []ViolationKind
|
||||
for _, v := range vs {
|
||||
out = append(out, v.Kind)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestApply_NormalNamespacesEverything(t *testing.T) {
|
||||
in := input(t, wpLabels())
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply: %v", err)
|
||||
}
|
||||
if len(res.Violations) != 0 || res.Stage != StageNormal {
|
||||
t.Errorf("Violations=%v Stage=%q", res.Violations, res.Stage)
|
||||
}
|
||||
want := &traefik.Configuration{HTTP: &traefik.HTTPConfiguration{
|
||||
Routers: map[string]*traefik.Router{
|
||||
"kunde__node-1__wp": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
Middlewares: []string{"kunde__node-1__wp-www"},
|
||||
Service: "kunde__node-1__wp",
|
||||
Rule: "Host(`kunde.de`) || Host(`www.kunde.de`)",
|
||||
TLS: &traefik.RouterTLS{CertResolver: "dns-resolver"},
|
||||
},
|
||||
},
|
||||
Services: map[string]*traefik.Service{
|
||||
"kunde__node-1__wp": {LoadBalancer: &traefik.ServersLoadBalancer{Servers: []traefik.Server{{URL: "http://100.64.0.42:80"}}}},
|
||||
},
|
||||
Middlewares: map[string]traefik.Middleware{
|
||||
"kunde__node-1__wp-www": in.Config.HTTP.Middlewares["wp-www"],
|
||||
},
|
||||
}}
|
||||
if !reflect.DeepEqual(res.Config, want) {
|
||||
t.Errorf("Konfiguration weicht ab\n got: %+v\nwant: %+v", res.Config.HTTP, want.HTTP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_DoesNotMutateInput(t *testing.T) {
|
||||
in := input(t, wpLabels())
|
||||
before := cfg(t, wpLabels())
|
||||
if _, err := engine().Apply(in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(in.Config, before) {
|
||||
t.Error("Eingabe-Konfiguration wurde verändert")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_TwoTenantsSameNamesMergeWithoutConflict(t *testing.T) {
|
||||
a := input(t, wpLabels())
|
||||
b := input(t, wpLabels())
|
||||
b.Tenant.ID, b.License.TenantID, b.NodeID = "andere", "andere", "node-1"
|
||||
b.License.AllowedDomains = []string{"kunde.de", "*.kunde.de"}
|
||||
|
||||
ra, _ := engine().Apply(a)
|
||||
rb, _ := engine().Apply(b)
|
||||
if _, err := traefik.Merge(ra.Config, rb.Config); err != nil {
|
||||
t.Errorf("Merge nach Namensraum-Präfix: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_DomainViolation(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.routers.bank.rule"] = "Host(`bank.de`)"
|
||||
labels["traefik.http.routers.bank.service"] = "wp"
|
||||
res, err := engine().Apply(input(t, labels))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := res.Config.HTTP.Routers["kunde__node-1__bank"]; ok {
|
||||
t.Error("Router mit fremder Domain wurde übernommen")
|
||||
}
|
||||
if _, ok := res.Config.HTTP.Routers["kunde__node-1__wp"]; !ok {
|
||||
t.Error("erlaubter Router fehlt")
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationDomain}) || res.Violations[0].Router != "bank" {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_RuleViolations(t *testing.T) {
|
||||
tests := map[string]ViolationKind{
|
||||
"Host(`kunde.de`) || PathPrefix(`/`)": ViolationCatchAll,
|
||||
"PathPrefix(`/`)": ViolationCatchAll,
|
||||
"HostRegexp(`.+`)": ViolationHostRegexp,
|
||||
"Host(`kunde.de`": ViolationRuleSyntax,
|
||||
"Host(`sub.kunde.de`) || Host(`x.de`)": ViolationDomain,
|
||||
}
|
||||
for rule, want := range tests {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.routers.wp.rule"] = rule
|
||||
res, err := engine().Apply(input(t, labels))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{want}) {
|
||||
t.Errorf("%s: Violations = %+v, erwartet %s", rule, res.Violations, want)
|
||||
}
|
||||
if res.Config.HTTP != nil {
|
||||
t.Errorf("%s: Router trotz Verstoß übernommen: %+v", rule, res.Config.HTTP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_RouteLimit(t *testing.T) {
|
||||
labels := map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80"}
|
||||
for _, r := range []string{"c", "a", "b"} {
|
||||
labels["traefik.http.routers."+r+".rule"] = "Host(`" + r + ".kunde.de`)"
|
||||
}
|
||||
in := input(t, labels)
|
||||
in.License.MaxRoutes = 2
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.Config.HTTP.Routers) != 2 || res.Config.HTTP.Routers["kunde__node-1__c"] != nil {
|
||||
t.Errorf("erwartet a und b (sortiert), bekommen %v", keys(res.Config.HTTP.Routers))
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationRouteLimit}) || res.Violations[0].Router != "c" {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
|
||||
in.License.MaxRoutes = -1
|
||||
res, _ = engine().Apply(in)
|
||||
if len(res.Config.HTTP.Routers) != 3 || len(res.Violations) != 0 {
|
||||
t.Errorf("unbegrenzt: %d Router, %v", len(res.Config.HTTP.Routers), res.Violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_ExternalReferences(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.routers.dash.rule"] = "Host(`dash.kunde.de`)"
|
||||
labels["traefik.http.routers.dash.service"] = "api@internal"
|
||||
labels["traefik.http.routers.wp.middlewares"] = "wp-www,auth@file"
|
||||
|
||||
res, err := engine().Apply(input(t, labels))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Config.HTTP != nil {
|
||||
t.Errorf("Router mit externen Referenzen übernommen: %v", keys(res.Config.HTTP.Routers))
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationExternalRef, ViolationExternalRef}) {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
|
||||
e := engine()
|
||||
e.AllowedExternalRefs = []string{"auth@file"}
|
||||
res, _ = e.Apply(input(t, labels))
|
||||
r := res.Config.HTTP.Routers["kunde__node-1__wp"]
|
||||
if r == nil || !reflect.DeepEqual(r.Middlewares, []string{"kunde__node-1__wp-www", "auth@file"}) {
|
||||
t.Errorf("freigegebene Referenz: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_UnknownLocalReferences(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.routers.x.rule"] = "Host(`x.kunde.de`)"
|
||||
labels["traefik.http.routers.x.service"] = "andere__node-9__shop" // fremder Service per vollem Namen
|
||||
labels["traefik.http.routers.y.rule"] = "Host(`y.kunde.de`)"
|
||||
labels["traefik.http.routers.y.service"] = "wp"
|
||||
labels["traefik.http.routers.y.middlewares"] = "gibtsnicht"
|
||||
|
||||
res, err := engine().Apply(input(t, labels))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationUnknownRef, ViolationUnknownRef}) {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
if len(res.Config.HTTP.Routers) != 1 {
|
||||
t.Errorf("Router: %v", keys(res.Config.HTTP.Routers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_MiddlewareFeatureGate(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.middlewares.auth.basicauth.users"] = "u:$apr1$x"
|
||||
labels["traefik.http.routers.wp.middlewares"] = "wp-www,auth"
|
||||
|
||||
res, _ := engine().Apply(input(t, labels))
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationFeature}) || res.Config.HTTP != nil {
|
||||
t.Errorf("ohne custom_middleware: Violations=%+v", res.Violations)
|
||||
}
|
||||
|
||||
in := input(t, labels)
|
||||
in.License = lic(license.TierPro, 10, license.FeatureCustomMiddleware)
|
||||
res, _ = engine().Apply(in)
|
||||
if len(res.Violations) != 0 || res.Config.HTTP.Middlewares["kunde__node-1__auth"] == nil {
|
||||
t.Errorf("mit custom_middleware: Violations=%+v", res.Violations)
|
||||
}
|
||||
}
|
||||
|
||||
// chain und errors verweisen per Namen auf andere Middlewares/Services und
|
||||
// würden den Namensraum umgehen – nie erlaubt.
|
||||
func TestApply_ForbiddenMiddlewares(t *testing.T) {
|
||||
for _, typ := range []string{"chain.middlewares", "errors.service"} {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.middlewares.m."+typ] = "andere__node-9__x"
|
||||
labels["traefik.http.routers.wp.middlewares"] = "m"
|
||||
in := input(t, labels)
|
||||
in.License = lic(license.TierEnterprise, -1, license.FeatureCustomMiddleware)
|
||||
res, _ := engine().Apply(in)
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationForbiddenMiddleware}) || res.Config.HTTP != nil {
|
||||
t.Errorf("%s: Violations=%+v", typ, res.Violations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_PrunesUnreferenced(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.middlewares.unused.compress"] = "true"
|
||||
labels["traefik.http.services.unused.loadbalancer.server.port"] = "81"
|
||||
labels["traefik.http.routers.wp.service"] = "wp"
|
||||
res, _ := engine().Apply(input(t, labels))
|
||||
if res.Config.HTTP.Middlewares["kunde__node-1__unused"] != nil || res.Config.HTTP.Services["kunde__node-1__unused"] != nil {
|
||||
t.Error("nicht referenzierte Definitionen wurden übernommen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStageFor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tier license.Tier
|
||||
status store.TenantStatus
|
||||
level int
|
||||
expire bool
|
||||
want Stage
|
||||
}{
|
||||
{"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},
|
||||
{"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},
|
||||
{"Mahnstufe 3", license.TierEnterprise, store.TenantActive, 3, false, StagePaywall},
|
||||
{"Mahnstufe 9", license.TierPro, store.TenantActive, 9, false, StagePaywall},
|
||||
{"Zahlung ausgesetzt", license.TierPro, store.TenantSuspendedPayment, 0, false, StagePaywall},
|
||||
{"Status abgelaufen", license.TierPro, store.TenantExpired, 0, false, StageLanding},
|
||||
{"Lizenz abgelaufen", license.TierPro, store.TenantActive, 1, true, StageLanding},
|
||||
{"Quarantäne", license.TierPro, store.TenantQuarantined, 0, false, StageBlocked},
|
||||
{"Quarantäne Community", license.TierCommunity, store.TenantQuarantined, 0, false, StageBlocked},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
l := lic(tt.tier, 5)
|
||||
if tt.expire {
|
||||
l.ValidUntil = now
|
||||
}
|
||||
if got := StageFor(Tenant{ID: "kunde", Status: tt.status, DunningLevel: tt.level}, l, now); got != tt.want {
|
||||
t.Errorf("%s: Stage = %q, erwartet %q", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_Interception(t *testing.T) {
|
||||
tests := []struct {
|
||||
stage Stage
|
||||
level int
|
||||
status store.TenantStatus
|
||||
ack bool // Durchlass per Cookie nach Lander-Hinweis
|
||||
}{
|
||||
{StageWatermark, 1, store.TenantActive, true},
|
||||
{StageFiremark, 2, store.TenantActive, true},
|
||||
{StagePaywall, 0, store.TenantSuspendedPayment, false},
|
||||
{StageLanding, 0, store.TenantExpired, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.stage), func(t *testing.T) {
|
||||
in := input(t, wpLabels())
|
||||
in.Tenant.Status, in.Tenant.DunningLevel = tt.status, tt.level
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Stage != tt.stage {
|
||||
t.Errorf("Stage = %q", res.Stage)
|
||||
}
|
||||
r := res.Config.HTTP.Routers["kunde__node-1__wp"]
|
||||
if r == nil || r.Service != InterceptorService {
|
||||
t.Fatalf("Router nicht zum Interceptor umgeleitet: %+v", r)
|
||||
}
|
||||
if r.Rule != "Host(`kunde.de`) || Host(`www.kunde.de`)" || r.TLS == nil {
|
||||
t.Error("Regel/TLS dürfen sich nicht ändern")
|
||||
}
|
||||
mw := "sultan__kunde__node-1__wp"
|
||||
if !reflect.DeepEqual(r.Middlewares, []string{"kunde__node-1__wp-www", mw}) {
|
||||
t.Errorf("Middlewares = %v", r.Middlewares)
|
||||
}
|
||||
hdr := res.Config.HTTP.Middlewares[mw]["headers"].(map[string]any)["customRequestHeaders"].(map[string]any)
|
||||
want := map[string]any{HeaderStage: string(tt.stage), HeaderTenant: "kunde", HeaderTier: "pro"}
|
||||
if !reflect.DeepEqual(hdr, want) {
|
||||
t.Errorf("Header = %v, erwartet %v", hdr, want)
|
||||
}
|
||||
svc := res.Config.HTTP.Services[InterceptorService]
|
||||
if svc == nil || svc.LoadBalancer.Servers[0].URL != interceptor {
|
||||
t.Errorf("Interceptor-Service fehlt: %+v", svc)
|
||||
}
|
||||
|
||||
ack := res.Config.HTTP.Routers["sultan__kunde__node-1__wp"]
|
||||
if !tt.ack {
|
||||
if ack != nil {
|
||||
t.Errorf("Stufe %s darf keinen Durchlass haben: %+v", tt.stage, ack)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ack == nil {
|
||||
t.Fatal("Durchlass-Router fehlt")
|
||||
}
|
||||
wantRule := "(Host(`kunde.de`) || Host(`www.kunde.de`)) && HeaderRegexp(`Cookie`, `(^|;\\s*)sultan_ack=" + string(tt.stage) + "(;|$)`)"
|
||||
if ack.Rule != wantRule {
|
||||
t.Errorf("Durchlass-Regel = %s\nerwartet %s", ack.Rule, wantRule)
|
||||
}
|
||||
if ack.Service != "kunde__node-1__wp" || !reflect.DeepEqual(ack.Middlewares, []string{"kunde__node-1__wp-www"}) {
|
||||
t.Errorf("Durchlass-Router: %+v", ack)
|
||||
}
|
||||
if ack.TLS == nil || !reflect.DeepEqual(ack.EntryPoints, r.EntryPoints) {
|
||||
t.Error("Durchlass-Router muss TLS/EntryPoints übernehmen")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Hat der Mandant eine Priorität gesetzt, muss der Durchlass darüber liegen;
|
||||
// ohne Priorität gewinnt er in Traefik ohnehin über die längere Regel.
|
||||
func TestApply_AckPriority(t *testing.T) {
|
||||
for _, prio := range []int{0, 50} {
|
||||
labels := wpLabels()
|
||||
if prio > 0 {
|
||||
labels["traefik.http.routers.wp.priority"] = "50"
|
||||
}
|
||||
in := input(t, labels)
|
||||
in.Tenant.DunningLevel = 1
|
||||
res, _ := engine().Apply(in)
|
||||
r := res.Config.HTTP.Routers["kunde__node-1__wp"]
|
||||
ack := res.Config.HTTP.Routers["sultan__kunde__node-1__wp"]
|
||||
wantAck := 0
|
||||
if prio > 0 {
|
||||
wantAck = prio + 1
|
||||
}
|
||||
if r.Priority != prio || ack.Priority != wantAck {
|
||||
t.Errorf("prio=%d: Router %d, Durchlass %d", prio, r.Priority, ack.Priority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDunningLevel(t *testing.T) {
|
||||
due := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
day := 24 * time.Hour
|
||||
tests := []struct {
|
||||
now time.Time
|
||||
want int
|
||||
}{
|
||||
{due.Add(-day), 0},
|
||||
{due, 0},
|
||||
{due.Add(8*day - time.Second), 0},
|
||||
{due.Add(8 * day), 1},
|
||||
{due.Add(25*day - time.Second), 1},
|
||||
{due.Add(25 * day), 2},
|
||||
{due.Add(32 * day), 3},
|
||||
{due.Add(365 * day), 3},
|
||||
}
|
||||
var sched DunningSchedule // Nullwert = Standard 8/25/32 Tage
|
||||
for _, tt := range tests {
|
||||
if got := sched.Level(due, tt.now); got != tt.want {
|
||||
t.Errorf("%v überfällig: Stufe %d, erwartet %d", tt.now.Sub(due), got, tt.want)
|
||||
}
|
||||
}
|
||||
if got := sched.Level(time.Time{}, due); got != 0 {
|
||||
t.Errorf("ohne Fälligkeit: Stufe %d", got)
|
||||
}
|
||||
custom := DunningSchedule{Level1: day, Level2: 2 * day, Level3: 3 * day}
|
||||
if got := custom.Level(due, due.Add(2*day)); got != 2 {
|
||||
t.Errorf("eigener Zeitplan: Stufe %d, erwartet 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_MaintenanceForDownService(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.routers.shop.rule"] = "Host(`shop.kunde.de`)"
|
||||
labels["traefik.http.routers.shop.service"] = "shop"
|
||||
labels["traefik.http.routers.wp.service"] = "wp"
|
||||
labels["traefik.http.services.shop.loadbalancer.server.port"] = "8080"
|
||||
in := input(t, labels)
|
||||
in.DownServices = map[string]bool{"shop": true}
|
||||
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Stage != StageNormal {
|
||||
t.Errorf("Mandanten-Stage = %q, Wartung betrifft nur den Router", res.Stage)
|
||||
}
|
||||
if res.Config.HTTP.Routers["kunde__node-1__shop"].Service != InterceptorService {
|
||||
t.Error("ausgefallener Dienst nicht zum Interceptor umgeleitet")
|
||||
}
|
||||
if res.Config.HTTP.Routers["kunde__node-1__wp"].Service != "kunde__node-1__wp" {
|
||||
t.Error("gesunder Dienst wurde umgeleitet")
|
||||
}
|
||||
hdr := res.Config.HTTP.Middlewares["sultan__kunde__node-1__shop"]["headers"].(map[string]any)["customRequestHeaders"].(map[string]any)
|
||||
if hdr[HeaderStage] != string(StageMaintenance) || hdr[HeaderTier] != "pro" {
|
||||
t.Errorf("Header = %v", hdr)
|
||||
}
|
||||
if res.Config.HTTP.Routers["sultan__kunde__node-1__shop"] != nil {
|
||||
t.Error("Wartung darf keinen Cookie-Durchlass haben")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_BlockedAndDrainedProduceNothing(t *testing.T) {
|
||||
blocked := input(t, wpLabels())
|
||||
blocked.Tenant.Status = store.TenantQuarantined
|
||||
drained := input(t, wpLabels())
|
||||
drained.NodeDrained = true
|
||||
for name, in := range map[string]Input{"Quarantäne": blocked, "gedraint": drained} {
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil || res.Config == nil || res.Config.HTTP != nil {
|
||||
t.Errorf("%s: res=%+v err=%v", name, res.Config, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_Errors(t *testing.T) {
|
||||
needsInterceptor := input(t, wpLabels())
|
||||
needsInterceptor.Tenant.DunningLevel = 1
|
||||
if _, err := (&Engine{now: engine().now}).Apply(needsInterceptor); !errors.Is(err, ErrNoInterceptor) {
|
||||
t.Errorf("ohne InterceptorURL: erwartet ErrNoInterceptor, bekommen %v", err)
|
||||
}
|
||||
|
||||
tests := map[string]func(*Input){
|
||||
"Lizenz eines anderen Mandanten": func(in *Input) { in.License.TenantID = "andere" },
|
||||
"Mandanten-ID mit __": func(in *Input) { in.Tenant.ID, in.License.TenantID = "a__b", "a__b" },
|
||||
"Mandanten-ID endet mit _": func(in *Input) { in.Tenant.ID, in.License.TenantID = "a_", "a_" },
|
||||
"reservierte Mandanten-ID": func(in *Input) { in.Tenant.ID, in.License.TenantID = "sultan", "sultan" },
|
||||
"Knoten-ID leer": func(in *Input) { in.NodeID = "" },
|
||||
"Knoten-ID mit @": func(in *Input) { in.NodeID = "n@x" },
|
||||
}
|
||||
for name, mutate := range tests {
|
||||
in := input(t, wpLabels())
|
||||
mutate(&in)
|
||||
if _, err := engine().Apply(in); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("%s: erwartet ErrInvalidInput, bekommen %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_NilConfig(t *testing.T) {
|
||||
in := input(t, wpLabels())
|
||||
in.Config = nil
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil || res.Config == nil || res.Config.HTTP != nil {
|
||||
t.Errorf("res=%+v err=%v", res.Config, err)
|
||||
}
|
||||
}
|
||||
|
||||
func keys[V any](m map[string]V) []string {
|
||||
var out []string
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Andockstelle für die geplante Middleware-Zielbegrenzung: ohne Prüffunktion
|
||||
// wird alles geduldet; mit Prüffunktion beobachten oder durchsetzen.
|
||||
func TestApply_MiddlewareTargetHook(t *testing.T) {
|
||||
labels := wpLabels()
|
||||
labels["traefik.http.middlewares.auth.forwardauth.address"] = "http://10.0.0.99:9000/auth"
|
||||
labels["traefik.http.routers.wp.middlewares"] = "wp-www,auth"
|
||||
in := input(t, labels)
|
||||
in.License = lic(license.TierPro, 10, license.FeatureCustomMiddleware)
|
||||
|
||||
// Ohne Hook: dulden.
|
||||
res, _ := engine().Apply(in)
|
||||
if len(res.Violations) != 0 || res.Config.HTTP.Routers["kunde__node-1__wp"] == nil {
|
||||
t.Fatalf("ohne Hook: %+v", res.Violations)
|
||||
}
|
||||
|
||||
var calls []string
|
||||
hook := func(mwName string, def traefik.Middleware, backend string) error {
|
||||
calls = append(calls, mwName+"→"+backend)
|
||||
if _, ok := def["forwardauth"]; ok {
|
||||
return errors.New("Ziel außerhalb des Backends")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
observe := engine()
|
||||
observe.MiddlewareTargetCheck = hook
|
||||
res, _ = observe.Apply(in)
|
||||
if res.Config.HTTP.Routers["kunde__node-1__wp"] == nil {
|
||||
t.Error("Beobachtungsmodus darf nicht sperren")
|
||||
}
|
||||
if len(res.Violations) != 1 || res.Violations[0].Kind != ViolationMiddlewareTarget || !res.Violations[0].Observed {
|
||||
t.Errorf("Beobachtungsmodus: %+v", res.Violations)
|
||||
}
|
||||
if !reflect.DeepEqual(calls, []string{"auth→http://100.64.0.42:80", "wp-www→http://100.64.0.42:80"}) {
|
||||
t.Errorf("Hook-Aufrufe = %v", calls)
|
||||
}
|
||||
|
||||
enforce := engine()
|
||||
enforce.MiddlewareTargetCheck = hook
|
||||
enforce.EnforceMiddlewareTargets = true
|
||||
res, _ = enforce.Apply(in)
|
||||
if res.Config.HTTP != nil {
|
||||
t.Error("Durchsetzungsmodus: Router hätte entfernt werden müssen")
|
||||
}
|
||||
if len(res.Violations) != 1 || res.Violations[0].Kind != ViolationMiddlewareTarget || res.Violations[0].Observed {
|
||||
t.Errorf("Durchsetzungsmodus: %+v", res.Violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidID(t *testing.T) {
|
||||
for id, want := range map[string]bool{
|
||||
"kunde": true, "kunde_schmidt": true, "node-proxmox-vm-42": true, "A1": true,
|
||||
"": false, "a__b": false, "_a": false, "a_": false, "-a": false, "a.b": false, "a@b": false, "a b": false,
|
||||
} {
|
||||
if got := ValidID(id); got != want {
|
||||
t.Errorf("ValidID(%q) = %v, erwartet %v", id, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user