All checks were successful
release-tag / release-image (push) Successful in 1m47s
578 lines
21 KiB
Go
578 lines
21 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"io"
|
||
"log/slog"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"net/netip"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/b1tsblog/license-platform/sdk/go/licenseclient"
|
||
)
|
||
|
||
func testHandler(t *testing.T) http.Handler {
|
||
t.Helper()
|
||
return testHandlerConfig(t, Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de"})
|
||
}
|
||
|
||
func testHandlerConfig(t *testing.T, cfg Config) http.Handler {
|
||
t.Helper()
|
||
h, err := New(context.Background(), cfg, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return h
|
||
}
|
||
|
||
func TestPresetBadgeFrench(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/badge/research.svg?lang=fr", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "image/svg+xml") {
|
||
t.Fatalf("content type %q", ct)
|
||
}
|
||
if !strings.Contains(w.Body.String(), "Aide à la recherche") {
|
||
t.Fatal("missing localized preset")
|
||
}
|
||
}
|
||
|
||
func TestValidate(t *testing.T) {
|
||
body := `{"@context":"https://example.org/context/v1","@type":"AIUsageDeclaration","schemaVersion":"1.1","language":"de","components":{"text":{"aiExtent":"assisted","activities":["research"],"humanReview":"editorial"}},"assurance":"selfDeclared"}`
|
||
r := httptest.NewRequest(http.MethodPost, "/v1/validate", strings.NewReader(body))
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
var result map[string]any
|
||
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result["valid"] != true {
|
||
t.Fatalf("unexpected response: %#v", result)
|
||
}
|
||
}
|
||
|
||
func TestCommunityRejectsCustomBadge(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?lang=en&badgeMessage=Custom", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusForbidden {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), "pro_feature_required") {
|
||
t.Fatalf("unexpected response: %s", w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestCapabilities(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/v1/capabilities", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d", w.Code)
|
||
}
|
||
if !strings.Contains(w.Body.String(), `"edition":"community"`) {
|
||
t.Fatalf("unexpected response: %s", w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestPublicLicenseRedactsInternalMetadata(t *testing.T) {
|
||
status := publicLicense(licenseclient.Status{
|
||
Edition: "pro", Licensed: true, LicenseID: "lic-secret", Customer: "Customer Name", Product: "product",
|
||
Features: []string{"custom_text"}, Limits: map[string]int64{"seats": 3}, ExpiresAt: "2030-01-01T00:00:00Z",
|
||
Mode: "online", Source: "server", LastChecked: "2026-07-20T12:00:00Z", LeaseExpires: "2026-07-21T12:00:00Z",
|
||
Reason: "internal diagnostic", ServerURL: "https://licenses.internal.example",
|
||
})
|
||
data, err := json.Marshal(status)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
body := string(data)
|
||
for _, forbidden := range []string{"lic-secret", "Customer Name", "licenses.internal.example", "internal diagnostic", "lastChecked", "leaseExpiresAt", `"mode"`, `"source"`} {
|
||
if strings.Contains(body, forbidden) {
|
||
t.Fatalf("public licence status leaks %q: %s", forbidden, body)
|
||
}
|
||
}
|
||
for _, expected := range []string{`"edition":"pro"`, `"licensed":true`, `"custom_text"`, `"seats":3`} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("public licence status missing %q: %s", expected, body)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMarketingPageGerman(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/product?lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{"KI-Nutzung transparent kennzeichnen", "Docker Compose", "JSON-LD"} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("marketing page missing %q", expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMarketingPageLanguageNegotiation(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/product", nil)
|
||
r.Header.Set("Accept-Language", "fr-FR,fr;q=0.9,en;q=0.8")
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), "Déclarez l’usage de l’IA") {
|
||
t.Fatal("missing French marketing content")
|
||
}
|
||
}
|
||
|
||
func TestInstallAlias(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/install?lang=en", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusTemporaryRedirect {
|
||
t.Fatalf("status %d", w.Code)
|
||
}
|
||
if location := w.Header().Get("Location"); location != "/product?lang=en#install" {
|
||
t.Fatalf("unexpected redirect %q", location)
|
||
}
|
||
}
|
||
|
||
func TestPricingRouteRemoved(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/pricing", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusNotFound {
|
||
t.Fatalf("status %d", w.Code)
|
||
}
|
||
}
|
||
|
||
func TestDeclarationLanguageSwitcherPreservesQuery(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/declaration?mode=article&textExtent=partial&textReview=expert&coverImageExtent=partial&coverImageReview=editorial&lang=de&assurance=selfDeclared", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
`id="declaration-language"`,
|
||
`lang=en`,
|
||
`textExtent=partial`,
|
||
`textReview=expert`,
|
||
`coverImageExtent=partial`,
|
||
`hreflang="en"`,
|
||
`hreflang="x-default"`,
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("language switcher missing %q", expected)
|
||
}
|
||
}
|
||
if !strings.Contains(body, `<option value="/declaration?`) || !strings.Contains(body, `lang=de`) || !strings.Contains(body, ` selected`) {
|
||
t.Fatal("current language is not selected")
|
||
}
|
||
}
|
||
|
||
func TestArticleBadgeUsesCalmVioletDefault(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?mode=article&textExtent=none&textReview=none&imageExtent=full&imageReview=editorial&lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("unexpected status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), `fill="#7c3aed"`) {
|
||
t.Fatalf("article badge does not use violet default: %s", w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestGeneratorExposesAssuranceSelection(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/?lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
`id="assurance"`,
|
||
`value="selfDeclared"`,
|
||
`value="technicallyRecorded"`,
|
||
`value="signed"`,
|
||
`value="verified"`,
|
||
`Nachweisgrundlage`,
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("generator missing %q", expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDeclarationUsesSelectedAssurance(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/declaration?preset=research&lang=de&assurance=technicallyRecorded", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
`"assurance":"technicallyRecorded"`,
|
||
`Als Nachweisgrundlage ist eine technische Protokollierung im Erstellungs- oder Veröffentlichungsprozess angegeben.`,
|
||
`<colgroup>`,
|
||
`scope="col"`,
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("declaration missing %q", expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSignedAssuranceExplainsScope(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/declaration?preset=summary&lang=de&assurance=signed", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), "Die Signatur bestätigt nicht automatisch die inhaltliche Richtigkeit der Angaben.") {
|
||
t.Fatal("signed assurance scope is not explained")
|
||
}
|
||
}
|
||
|
||
func TestGeneratorShowsAIACTGuardrails(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/?lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
`id="legal-deepfake"`,
|
||
`id="legal-public-interest"`,
|
||
`id="legal-creative"`,
|
||
`id="responsible-role"`,
|
||
`id="responsible"`,
|
||
`id="responsible-url"`,
|
||
"formale Prüfung",
|
||
"Platzierung nach Art. 50",
|
||
"erfüllt aber nicht automatisch die Anbieterpflicht",
|
||
"Emoji-Variante",
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("generator missing AI Act guardrail %q", expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDeclarationExplainsReviewAndMachineMarkingLimits(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/declaration?preset=full&review=editorial&lang=de&responsible=Beispielredaktion&responsibleUrl=https%3A%2F%2Fexample.org%2Fimpressum", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
"Beispielredaktion",
|
||
"begründet für sich allein keine Ausnahme",
|
||
"wirksame, zuverlässige, robuste und interoperable maschinenlesbare Markierung",
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("declaration missing AI Act guardrail %q", expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBackgroundPageGerman(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/background?lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
"Warum KI-Nutzung gekennzeichnet wird",
|
||
"Artikel 50 gilt ab 2. August 2026",
|
||
"keine allgemeine Kennzeichnungspflicht",
|
||
"KI-generierter Text zu Angelegenheiten von öffentlichem Interesse",
|
||
"Leitlinien zu Transparenzpflichten",
|
||
`hreflang="en"`,
|
||
`id="background-language"`,
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("background page missing %q", expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBackgroundPageFrenchNegotiation(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/background", nil)
|
||
r.Header.Set("Accept-Language", "fr-FR,fr;q=0.9,en;q=0.8")
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), "Pourquoi signaler l’utilisation de l’IA") {
|
||
t.Fatal("missing French background content")
|
||
}
|
||
}
|
||
|
||
func TestNavigationUsesInternalBackgroundPage(t *testing.T) {
|
||
for _, path := range []string{"/?lang=de", "/product?lang=de"} {
|
||
r := httptest.NewRequest(http.MethodGet, path, nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("%s returned %d", path, w.Code)
|
||
}
|
||
if !strings.Contains(w.Body.String(), `href="/background?lang=de"`) {
|
||
t.Fatalf("%s does not link to the internal background page", path)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLegalPagesUseConfiguredOperatorData(t *testing.T) {
|
||
cfg := Config{
|
||
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de",
|
||
LegalName: "Beispiel GmbH", LegalAddress: "Musterstraße 1\n10115 Berlin", LegalEmail: "datenschutz@example.org",
|
||
HostingProvider: "Beispiel Hosting GmbH", LogRetention: "7 Tage", ConsumerDisputeStatus: "not_participating",
|
||
}
|
||
h := testHandlerConfig(t, cfg)
|
||
for _, tc := range []struct {
|
||
path, expected string
|
||
}{
|
||
{"/impressum?lang=de", "Beispiel GmbH"},
|
||
{"/datenschutz?lang=de", "Die Client-IP wird nur protokolliert"},
|
||
{"/barrierefreiheit?lang=de", "Noch nicht durch eine unabhängige Stelle geprüft"},
|
||
} {
|
||
r := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
||
w := httptest.NewRecorder()
|
||
h.ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("%s returned %d: %s", tc.path, w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), tc.expected) {
|
||
t.Fatalf("%s missing %q", tc.path, tc.expected)
|
||
}
|
||
if !strings.Contains(w.Body.String(), `/impressum?lang=de`) || !strings.Contains(w.Body.String(), `/datenschutz?lang=de`) {
|
||
t.Fatalf("%s missing legal footer links", tc.path)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLegalStrictRejectsMissingConfiguration(t *testing.T) {
|
||
_, err := New(context.Background(), Config{
|
||
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", LegalStrict: true,
|
||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err == nil || !strings.Contains(err.Error(), "LEGAL_NAME") {
|
||
t.Fatalf("expected strict legal configuration error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestSecurityHeadersAndRequestIDValidation(t *testing.T) {
|
||
cfg := Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", EnableHSTS: true}
|
||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||
r.Header.Set("X-Request-ID", "invalid request id\n")
|
||
w := httptest.NewRecorder()
|
||
testHandlerConfig(t, cfg).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
csp := w.Header().Get("Content-Security-Policy")
|
||
if !strings.Contains(csp, "script-src 'self' 'nonce-") || strings.Contains(csp, "'unsafe-inline'") {
|
||
t.Fatalf("unexpected CSP %q", csp)
|
||
}
|
||
if got := w.Header().Get("Strict-Transport-Security"); !strings.Contains(got, "max-age=") || strings.Contains(strings.ToLower(got), "includesubdomains") {
|
||
t.Fatalf("unexpected HSTS: %q", got)
|
||
}
|
||
if got := w.Header().Get("X-Request-ID"); got == "" || strings.Contains(got, " ") {
|
||
t.Fatalf("invalid response request id %q", got)
|
||
}
|
||
}
|
||
|
||
func TestMetricsDisabledByDefaultAndTokenProtected(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusNotFound {
|
||
t.Fatalf("disabled metrics returned %d", w.Code)
|
||
}
|
||
|
||
cfg := Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", MetricsEnabled: true, MetricsToken: "a-long-random-test-token"}
|
||
h := testHandlerConfig(t, cfg)
|
||
w = httptest.NewRecorder()
|
||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
||
if w.Code != http.StatusUnauthorized {
|
||
t.Fatalf("unprotected metrics returned %d", w.Code)
|
||
}
|
||
r = httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
||
r.Header.Set("Authorization", "Bearer a-long-random-test-token")
|
||
w = httptest.NewRecorder()
|
||
h.ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "ai_disclosure_http_requests_total") {
|
||
t.Fatalf("protected metrics returned %d: %s", w.Code, w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestMetricsCannotBeEnabledWithoutToken(t *testing.T) {
|
||
_, err := New(context.Background(), Config{
|
||
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", MetricsEnabled: true,
|
||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err == nil || !strings.Contains(err.Error(), "METRICS_TOKEN") {
|
||
t.Fatalf("expected metrics token validation error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestInvalidBooleanEnvironmentValueFailsClosed(t *testing.T) {
|
||
t.Setenv("LEGAL_STRICT", "definitely")
|
||
cfg := ConfigFromEnv()
|
||
_, err := New(context.Background(), cfg, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err == nil || !strings.Contains(err.Error(), "LEGAL_STRICT must be true or false") {
|
||
t.Fatalf("expected environment validation error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestLegalEmailMustBePlainAddress(t *testing.T) {
|
||
_, err := New(context.Background(), Config{
|
||
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", LegalEmail: "Name <legal@example.org>",
|
||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err == nil || !strings.Contains(err.Error(), "LEGAL_EMAIL") {
|
||
t.Fatalf("expected legal email validation error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestExternalURLsMustUseHTTPOrHTTPS(t *testing.T) {
|
||
_, err := New(context.Background(), Config{
|
||
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", ContactURL: "javascript:alert(1)",
|
||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err == nil || !strings.Contains(err.Error(), "CONTACT_URL") {
|
||
t.Fatalf("expected contact URL validation error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestValidateRejectsOversizedBody(t *testing.T) {
|
||
body := `{"padding":"` + strings.Repeat("x", (1<<20)+1) + `"}`
|
||
r := httptest.NewRequest(http.MethodPost, "/v1/validate", strings.NewReader(body))
|
||
r.Header.Set("Content-Type", "application/json")
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusRequestEntityTooLarge {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestValidateRejectsNonJSONContentType(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodPost, "/v1/validate", strings.NewReader(`{}`))
|
||
r.Header.Set("Content-Type", "text/plain")
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusUnsupportedMediaType {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestClientIPUsesNearestUntrustedForwardedAddress(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "https://example.org/", nil)
|
||
r.RemoteAddr = "10.0.0.2:443"
|
||
r.Header.Set("X-Forwarded-For", "198.51.100.200, 203.0.113.50, 10.0.0.3")
|
||
trusted := []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}
|
||
if got := clientIP(r, true, trusted); got != "203.0.113.50" {
|
||
t.Fatalf("unexpected client IP %q", got)
|
||
}
|
||
}
|
||
|
||
func TestTrustProxyRequiresExplicitCIDRs(t *testing.T) {
|
||
_, err := New(context.Background(), Config{
|
||
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", TrustProxy: true,
|
||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
if err == nil || !strings.Contains(err.Error(), "TRUSTED_PROXY_CIDRS") {
|
||
t.Fatalf("expected trusted proxy validation error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestEmojiThemeBadgeEndpoint(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/badge/summary.svg?lang=de&theme=emoji", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{
|
||
`width="64" height="64"`,
|
||
`data-theme="emoji"`,
|
||
`data-icon="summary"`,
|
||
`fill="#b7791f"`,
|
||
`Inhaltliche Zusammenfassung`,
|
||
} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("emoji badge missing %q: %s", expected, body)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestGeneratorOffersEmojiTheme(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/?lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
const expected = `<option value="emoji">Emoji (quadratisch)</option>`
|
||
if !strings.Contains(w.Body.String(), expected) {
|
||
t.Fatalf("generator missing %q", expected)
|
||
}
|
||
}
|
||
|
||
func TestPublicInterestBadgeUsesExplicitDisclosureWithoutExemption(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?mode=article&textExtent=full&textReview=none&legalContext=publicInterestText&lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), "KI-generierte / bearbeitete Inhalte") {
|
||
t.Fatalf("missing explicit legal disclosure wording: %s", w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestPublicInterestBadgeCanRemainVoluntaryWithSubstantiveReviewAndResponsibility(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?mode=article&textExtent=partial&textReview=expert&legalContext=publicInterestText&responsible=Beispielredaktion&lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
if !strings.Contains(w.Body.String(), "Artikeltransparenz") {
|
||
t.Fatalf("expected voluntary article wording where possible exception is documented: %s", w.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestDeepfakeDeclarationShowsCautiousAssessment(t *testing.T) {
|
||
r := httptest.NewRequest(http.MethodGet, "/declaration?mode=article&imageExtent=full&imageReview=none&legalContext=deepfake&lang=de", nil)
|
||
w := httptest.NewRecorder()
|
||
testHandler(t).ServeHTTP(w, r)
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||
}
|
||
body := w.Body.String()
|
||
for _, expected := range []string{"Vorsichtige Art.-50-Einschätzung", "spricht vieles dafür", "Deepfake / realitätsähnliche KI-Manipulation"} {
|
||
if !strings.Contains(body, expected) {
|
||
t.Fatalf("missing %q: %s", expected, body)
|
||
}
|
||
}
|
||
}
|