Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSecurityHeaders(t *testing.T) {
|
|
h := SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
|
|
want := map[string]string{
|
|
"X-Content-Type-Options": "nosniff",
|
|
"Referrer-Policy": "no-referrer",
|
|
"X-Frame-Options": "DENY",
|
|
}
|
|
for k, v := range want {
|
|
if got := rec.Header().Get(k); got != v {
|
|
t.Errorf("%s = %q, want %q", k, got, v)
|
|
}
|
|
}
|
|
if hsts := rec.Header().Get("Strict-Transport-Security"); !strings.Contains(hsts, "max-age=") {
|
|
t.Errorf("Strict-Transport-Security = %q", hsts)
|
|
}
|
|
csp := rec.Header().Get("Content-Security-Policy")
|
|
for _, directive := range []string{"default-src 'self'", "frame-ancestors 'none'", "form-action 'self'"} {
|
|
if !strings.Contains(csp, directive) {
|
|
t.Errorf("CSP %q fehlt %q", csp, directive)
|
|
}
|
|
}
|
|
if strings.Contains(csp, "unsafe-inline") || strings.Contains(csp, "unsafe-eval") {
|
|
t.Errorf("CSP darf kein unsafe-* enthalten: %q", csp)
|
|
}
|
|
}
|
|
|
|
func TestNoStore(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
NoStore(rec)
|
|
if got := rec.Header().Get("Cache-Control"); !strings.Contains(got, "no-store") {
|
|
t.Errorf("Cache-Control = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestClientIPIgnoresForwardedHeader(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.RemoteAddr = "10.1.20.34:51234"
|
|
r.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
// Das Portal terminiert TLS selbst; ein gefälschter Header darf das
|
|
// Audit-Log nicht verfälschen.
|
|
if got := ClientIP(r); got != "10.1.20.34" {
|
|
t.Errorf("ClientIP = %q, want 10.1.20.34", got)
|
|
}
|
|
}
|
|
|
|
func TestClientIPHandlesIPv6AndMalformed(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.RemoteAddr = "[2001:db8::1]:443"
|
|
if got := ClientIP(r); got != "2001:db8::1" {
|
|
t.Errorf("ClientIP = %q", got)
|
|
}
|
|
r.RemoteAddr = "kaputt"
|
|
if got := ClientIP(r); got != "kaputt" {
|
|
t.Errorf("ClientIP = %q, malformed muss unverändert durchgereicht werden", got)
|
|
}
|
|
}
|
|
|
|
func TestStringsAreComplete(t *testing.T) {
|
|
required := []string{
|
|
"login_title", "login_username", "login_password", "login_submit",
|
|
"login_username_hint", "error_generic_login", "error_password_expired",
|
|
"error_rate_limited", "error_session_expired", "overview_title",
|
|
"overview_no_certs", "overview_expiry_warning", "download_ovpn",
|
|
"download_viscosity", "logout", "guides_title", "error_service_unavailable",
|
|
"error_forbidden", "logout_reminder", "valid_until", "vpn_instance",
|
|
}
|
|
for _, k := range required {
|
|
v, ok := S[k]
|
|
if !ok || strings.TrimSpace(v) == "" {
|
|
t.Errorf("String %q fehlt oder ist leer", k)
|
|
}
|
|
}
|
|
if T("gibt_es_nicht") != "gibt_es_nicht" {
|
|
t.Error("T muss bei unbekanntem Schlüssel den Schlüssel liefern, nie einen leeren String")
|
|
}
|
|
}
|