feat(web): Sessions, Rate-Limiting, Security-Header, Templates und eingebettete Assets

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 09:20:38 +02:00
parent e49882b8a8
commit bb2bc223b9
16 changed files with 1292 additions and 0 deletions

View file

@ -0,0 +1,89 @@
package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func testRenderer(t *testing.T) *Renderer {
t.Helper()
rn, err := NewRenderer(Brand{Title: "VPN-Portal Firma GmbH", Version: "1.2.3",
SupportContact: "it@firma.de"})
if err != nil {
t.Fatalf("NewRenderer: %v", err)
}
return rn
}
func TestRenderLoginPage(t *testing.T) {
rn := testRenderer(t)
rec := httptest.NewRecorder()
rn.Render(rec, httptest.NewRequest(http.MethodGet, "/login", nil), http.StatusOK,
"login", PageData{Brand: rn.brand})
if rec.Code != http.StatusOK {
t.Fatalf("Code = %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{
"<!DOCTYPE html>", `lang="de"`, "VPN-Portal Firma GmbH",
S["login_submit"], S["login_username_hint"], "1.2.3",
`name="password"`, `type="password"`,
} {
if !strings.Contains(body, want) {
t.Errorf("Login-Seite enthält %q nicht", want)
}
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
t.Errorf("Content-Type = %q", ct)
}
}
func TestRenderEscapesUserContent(t *testing.T) {
rn := testRenderer(t)
rec := httptest.NewRecorder()
rn.Render(rec, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusOK,
"error", PageData{
Brand: rn.brand,
Flash: `<script>alert(1)</script>`,
Data: errorPageData{Title: "T", Message: `"><img src=x onerror=alert(1)>`},
})
body := rec.Body.String()
if strings.Contains(body, "<script>alert(1)</script>") {
t.Error("Flash-Text wurde nicht escapt")
}
if strings.Contains(body, "onerror=alert(1)>") {
t.Error("Fehlermeldung wurde nicht escapt")
}
}
func TestRenderSetsStatusCode(t *testing.T) {
rn := testRenderer(t)
rec := httptest.NewRecorder()
rn.Render(rec, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusServiceUnavailable,
"error", PageData{Brand: rn.brand, Data: errorPageData{Title: "x", Message: "y"}})
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("Code = %d, want 503", rec.Code)
}
}
func TestAllTemplatesParse(t *testing.T) {
rn := testRenderer(t)
for _, page := range pageNames {
if rn.pages[page] == nil {
t.Errorf("Template %q wurde nicht geladen", page)
}
}
}
func TestRenderUnknownPageDoesNotPanic(t *testing.T) {
rn := testRenderer(t)
rec := httptest.NewRecorder()
rn.Render(rec, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusOK,
"gibtsnicht", PageData{Brand: rn.brand})
if rec.Code != http.StatusInternalServerError {
t.Fatalf("Code = %d, want 500", rec.Code)
}
}