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) } }