opnsense-portal/internal/web/handlers_static_test.go
2026-08-14 09:25:52 +02:00

193 lines
6.3 KiB
Go

package web
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type fakeHealth struct{ rep HealthReport }
func (f *fakeHealth) Check(ctx context.Context) HealthReport { return f.rep }
func TestGuidesOrderPutsDetectedPlatformFirst(t *testing.T) {
const winUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
g := Guides(winUA)
if len(g) < 4 {
t.Fatalf("es müssen Anleitungen für alle Plattformen existieren, got %d", len(g))
}
if !strings.Contains(g[0].Platform, "Windows") || !g[0].Suggested {
t.Errorf("erste Anleitung = %+v, want Windows und Suggested", g[0])
}
// Kein Filter: alle Plattformen bleiben erreichbar.
for _, want := range []string{"Windows", "macOS", "iOS", "Android"} {
var found bool
for _, x := range g {
if strings.Contains(x.Platform, want) {
found = true
}
}
if !found {
t.Errorf("Anleitung für %s fehlt", want)
}
}
}
func TestGuidesDetectsOtherPlatforms(t *testing.T) {
cases := map[string]string{
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)": "macOS",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)": "iOS",
"Mozilla/5.0 (Linux; Android 14; Pixel 8)": "Android",
}
for ua, want := range cases {
g := Guides(ua)
if !strings.Contains(g[0].Platform, want) {
t.Errorf("UA %q -> %q, want %q zuerst", ua, g[0].Platform, want)
}
}
// Unbekannter User-Agent: Reihenfolge stabil, nichts als Vorschlag markiert.
g := Guides("irgendwas")
if g[0].Suggested {
t.Error("bei unbekanntem User-Agent darf nichts vorgeschlagen werden")
}
}
func TestGuidesNeverLosesEntries(t *testing.T) {
for _, ua := range []string{"", "Windows", "iPhone", "Android", "Macintosh", "unbekannt"} {
if got, want := len(Guides(ua)), len(baseGuides); got != want {
t.Errorf("UA %q liefert %d Anleitungen, want %d — es darf nie gefiltert werden", ua, got, want)
}
}
}
func TestGuidesPageRequiresSession(t *testing.T) {
srv, _ := newTestServer(t, Deps{Auth: &fakeAuth{}})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/anleitungen", nil))
if rec.Code != http.StatusSeeOther {
t.Fatalf("Code = %d, want 303", rec.Code)
}
}
func TestGuidesPageRenders(t *testing.T) {
srv, _, cookie, _ := loggedInServer(t, &certsWithExport{})
rec := getWithCookie(srv.Handler(), "/anleitungen", cookie)
if rec.Code != http.StatusOK {
t.Fatalf("Code = %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{S["guides_title"], "OpenVPN", "Tunnelblick", "OpenVPN Connect"} {
if !strings.Contains(body, want) {
t.Errorf("Anleitungsseite enthält %q nicht", want)
}
}
}
func TestHealthzHealthy(t *testing.T) {
srv, _ := newTestServer(t, Deps{
Auth: &fakeAuth{},
Health: &fakeHealth{rep: HealthReport{OK: true, OPNsense: true, Directory: true}},
})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if rec.Code != http.StatusOK {
t.Fatalf("Code = %d, want 200", rec.Code)
}
var got HealthReport
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("Antwort ist kein JSON: %v (%s)", err, rec.Body.String())
}
if !got.OK || !got.OPNsense || !got.Directory {
t.Errorf("Report = %+v", got)
}
}
func TestHealthzUnhealthyReturns503(t *testing.T) {
srv, _ := newTestServer(t, Deps{
Auth: &fakeAuth{},
Health: &fakeHealth{rep: HealthReport{OK: false, OPNsense: false, Directory: true}},
})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("Code = %d, want 503", rec.Code)
}
}
func TestHealthzNeedsNoAuthAndLeaksNoDetails(t *testing.T) {
srv, _ := newTestServer(t, Deps{
Auth: &fakeAuth{},
Health: &fakeHealth{rep: HealthReport{OK: true, OPNsense: true, Directory: true}},
})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if rec.Code != http.StatusOK {
t.Fatalf("/healthz muss ohne Anmeldung antworten, Code = %d", rec.Code)
}
body := strings.ToLower(rec.Body.String())
for _, forbidden := range []string{"firma.local", "dc01", "https://", "password", "secret"} {
if strings.Contains(body, forbidden) {
t.Errorf("/healthz verrät %q: %s", forbidden, rec.Body.String())
}
}
}
func TestHealthzWithoutCheckerStillAnswers(t *testing.T) {
srv, _ := newTestServer(t, Deps{Auth: &fakeAuth{}})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if rec.Code != http.StatusOK {
t.Fatalf("Code = %d — ohne Checker gilt das Portal selbst als gesund", rec.Code)
}
}
func TestUnknownPathReturns404(t *testing.T) {
srv, _ := newTestServer(t, Deps{Auth: &fakeAuth{}})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/gibtsnicht", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("Code = %d, want 404", rec.Code)
}
}
func TestAssetsAreServed(t *testing.T) {
srv, _ := newTestServer(t, Deps{Auth: &fakeAuth{}})
for path, wantType := range map[string]string{
"/assets/style.css": "text/css",
"/assets/app.js": "text/javascript",
} {
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusOK {
t.Errorf("%s: Code = %d", path, rec.Code)
continue
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, wantType) {
t.Errorf("%s: Content-Type = %q, want %q", path, ct, wantType)
}
}
}
func TestAssetPathTraversalIsRejected(t *testing.T) {
srv, _ := newTestServer(t, Deps{Auth: &fakeAuth{}})
for _, p := range []string{"/assets/../server.go", "/assets/%2e%2e%2fserver.go", "/assets/"} {
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, p, nil))
if rec.Code == http.StatusOK && strings.Contains(rec.Body.String(), "package web") {
t.Errorf("%s hat Quelltext ausgeliefert", p)
}
}
}
func TestLogoIsNotServedWhenUnconfigured(t *testing.T) {
srv, _ := newTestServer(t, Deps{Auth: &fakeAuth{}})
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/assets/logo", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("ohne logo_file muss /assets/logo 404 liefern, Code = %d", rec.Code)
}
}