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, "