package lander import ( "bytes" "errors" "log/slog" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" ) func request(t *testing.T, l *Lander, stage, tier, target string, header ...string) *httptest.ResponseRecorder { t.Helper() r := httptest.NewRequest(http.MethodGet, target, nil) r.Host = "kunde.de" if stage != "" { r.Header.Set("X-Sultan-Stage", stage) } r.Header.Set("X-Sultan-Tenant", "kunde") if tier != "" { r.Header.Set("X-Sultan-Tier", tier) } for i := 0; i+1 < len(header); i += 2 { r.Header.Set(header[i], header[i+1]) } rec := httptest.NewRecorder() l.ServeHTTP(rec, r) return rec } func newLander(t *testing.T) *Lander { return &Lander{Logger: slog.New(slog.DiscardHandler), HosterName: "Beispiel-Hosting", HosterURL: "https://hoster.example", ProURL: "https://hoster.example/pro"} } func TestStages(t *testing.T) { tests := []struct { stage string status int refresh string // erwarteter meta-refresh-Inhalt, leer = keiner cookie bool }{ {"watermark", 200, "5;url=/shop?x=1", true}, {"firemark", 200, "10;url=/shop?x=1", true}, {"paywall", 402, "", false}, {"landing", 402, "", false}, {"maintenance", 503, "", false}, } for _, tt := range tests { t.Run(tt.stage, func(t *testing.T) { rec := request(t, newLander(t), tt.stage, "pro", "/shop?x=1") if rec.Code != tt.status { t.Errorf("Status %d, erwartet %d", rec.Code, tt.status) } if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" { t.Errorf("Content-Type %q", ct) } if rec.Header().Get("Cache-Control") != "no-store" { t.Error("Seite darf nicht gecacht werden") } body := rec.Body.String() if tt.refresh != "" && !strings.Contains(body, `http-equiv="refresh" content="`+tt.refresh+`"`) { t.Errorf("meta refresh %q fehlt:\n%s", tt.refresh, body) } if tt.refresh == "" && strings.Contains(body, "refresh") { t.Error("Stufe ohne Durchlass darf nicht weiterleiten") } cookie := rec.Header().Get("Set-Cookie") if tt.cookie { for _, want := range []string{"sultan_ack=" + tt.stage, "Max-Age=3600", "HttpOnly", "SameSite=Lax", "Path=/"} { if !strings.Contains(cookie, want) { t.Errorf("Cookie %q ohne %q", cookie, want) } } } else if cookie != "" { t.Errorf("unerwartetes Cookie %q", cookie) } if !strings.Contains(body, `lang="de"`) || !strings.Contains(body, `lang="en"`) { t.Error("Text muss auf Deutsch und Englisch vorliegen") } }) } } func TestMaintenanceRetryAfterAndProHint(t *testing.T) { l := newLander(t) rec := request(t, l, "maintenance", "community", "/") if rec.Header().Get("Retry-After") != "60" { t.Errorf("Retry-After %q", rec.Header().Get("Retry-After")) } if !strings.Contains(rec.Body.String(), "https://hoster.example/pro") { t.Error("Community-Wartungsseite ohne Pro-Hinweis") } if strings.Contains(request(t, l, "maintenance", "pro", "/").Body.String(), "https://hoster.example/pro") { t.Error("Pro-Kunde bekommt Pro-Werbung") } } func TestPaywallInvitesToHoster(t *testing.T) { body := request(t, newLander(t), "paywall", "pro", "/").Body.String() if !strings.Contains(body, "Beispiel-Hosting") || !strings.Contains(body, "https://hoster.example") { t.Errorf("Einladung des Hosters fehlt:\n%s", body) } } func TestSecureCookieBehindHTTPS(t *testing.T) { rec := request(t, newLander(t), "watermark", "pro", "/", "X-Forwarded-Proto", "https") if !strings.Contains(rec.Header().Get("Set-Cookie"), "Secure") { t.Errorf("Cookie ohne Secure: %q", rec.Header().Get("Set-Cookie")) } } func TestConfigurableDelays(t *testing.T) { l := newLander(t) l.WatermarkDelay, l.FiremarkDelay, l.AckTTL = 2*time.Second, 7*time.Second, 15*time.Minute if body := request(t, l, "watermark", "pro", "/").Body.String(); !strings.Contains(body, `content="2;url=/"`) { t.Errorf("Wartezeit Stufe 1:\n%s", body) } rec := request(t, l, "firemark", "pro", "/") if !strings.Contains(rec.Body.String(), `content="7;url=/"`) || !strings.Contains(rec.Header().Get("Set-Cookie"), "Max-Age=900") { t.Errorf("Stufe 2: %q / %s", rec.Header().Get("Set-Cookie"), rec.Body.String()) } } func TestUnknownOrMissingStage(t *testing.T) { for _, stage := range []string{"", "normal", "kaputt"} { if rec := request(t, newLander(t), stage, "pro", "/"); rec.Code != http.StatusBadRequest { t.Errorf("Stufe %q: Status %d", stage, rec.Code) } } } // Werte aus Headern und URL werden nie roh ausgegeben. func TestEscaping(t *testing.T) { rec := request(t, newLander(t), "watermark", "pro", `/a">`, "X-Sultan-Tenant", "") body := rec.Body.String() if strings.Contains(body, ""+strings.Repeat("a", 400))) body := rec.Body.String() if strings.Contains(body, "