Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
193 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|