Files
ai-disclosure-standard/internal/app/server_test.go
jbergner 6e152a5121
Some checks failed
release-tag / release-image (push) Failing after 1m38s
2.0.2 Update und Anpassungen
2026-07-24 10:08:19 +02:00

430 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
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(), "licensed_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 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 lusage de lIA") {
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 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 lutilisation de lIA") {
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 TestGeneratorExposesArticle50Context(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())
}
for _, expected := range []string{
`id="public-interest-text"`, `id="deepfake"`, `id="substantial-review"`,
`id="editorial-responsibility-confirmed"`, `id="first-exposure-disclosure"`,
`Regulatorischer Kontext (EU AI Act, Artikel 50)`,
} {
if !strings.Contains(w.Body.String(), expected) {
t.Fatalf("generator missing %q", expected)
}
}
}
func TestArticle50AssessmentEndpoint(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/v1/article50-assessment.json?preset=full&lang=de&publicInterestText=true", nil)
w := httptest.NewRecorder()
testHandler(t).ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
for _, expected := range []string{`"code":"public_interest_text_disclosure_relevant"`, `"potentiallyApplicable":true`, `Nicht bindende Entscheidungshilfe`} {
if !strings.Contains(w.Body.String(), expected) {
t.Fatalf("assessment response missing %q: %s", expected, w.Body.String())
}
}
}
func TestRegulatoryDeclarationUsesExplicitBadge(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?mode=article&textExtent=full&textReview=none&lang=de&publicInterestText=true", 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-generierter Text") || !strings.Contains(w.Body.String(), `fill="#b45309"`) {
t.Fatalf("regulatory badge not explicit/amber: %s", w.Body.String())
}
}
func TestDeclarationRendersRegulatoryAssessment(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/declaration?mode=article&textExtent=partial&textReview=expert&lang=de&publicInterestText=true&substantialHumanReview=true&editorialResponsibilityConfirmed=true&responsible=Example%20Redaktion", nil)
w := httptest.NewRecorder()
testHandler(t).ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
for _, expected := range []string{`Technische Artikel-50-Einordnung`, `Mögliche Ausnahme für redaktionell kontrollierten Text`, `Example Redaktion`, `"regulatoryContext"`} {
if !strings.Contains(w.Body.String(), expected) {
t.Fatalf("declaration missing %q", expected)
}
}
}
func TestCommunityRejectsBulkAPI(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/v1/bulk/declarations", strings.NewReader(`{"items":[{"parameters":{"preset":"research","lang":"de"}}]}`))
r.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
testHandler(t).ServeHTTP(w, r)
if w.Code != http.StatusForbidden || !strings.Contains(w.Body.String(), "bulk_api") {
t.Fatalf("unexpected bulk response %d: %s", w.Code, w.Body.String())
}
}
func TestBulkModeRequiresLicensedCapabilityForReadiness(t *testing.T) {
h := testHandlerConfig(t, Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", ServiceMode: "bulk"})
r := httptest.NewRequest(http.MethodGet, "/readyz", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusServiceUnavailable {
t.Fatalf("bulk readiness status=%d body=%s", w.Code, w.Body.String())
}
r = httptest.NewRequest(http.MethodGet, "/", nil)
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("bulk workspace status=%d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "Bulk Workspace") || !strings.Contains(w.Body.String(), "bulk_api") {
t.Fatalf("bulk workspace missing expected content: %s", w.Body.String())
}
}
func TestBulkAPIKeyAuthorization(t *testing.T) {
s := &Server{cfg: Config{BulkRequireAPIKey: true, BulkAPIKey: "very-secret"}}
r := httptest.NewRequest(http.MethodPost, "/v1/bulk/declarations", nil)
if s.bulkAuthorized(r) {
t.Fatal("request without API key must not be authorized")
}
r = httptest.NewRequest(http.MethodPost, "/v1/bulk/declarations", nil)
r.Header.Set("Authorization", "Bearer very-secret")
if !s.bulkAuthorized(r) {
t.Fatal("bearer API key should be authorized")
}
r = httptest.NewRequest(http.MethodPost, "/v1/bulk/declarations", nil)
r.Header.Set("X-API-Key", "very-secret")
if !s.bulkAuthorized(r) {
t.Fatal("X-API-Key should be authorized")
}
}
func TestRequireLicenseFailsClosedForApplicationRequests(t *testing.T) {
h := testHandlerConfig(t, Config{
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", RequireLicense: true,
})
for _, path := range []string{"/", "/v1/validate"} {
r := httptest.NewRequest(http.MethodGet, path, nil)
if path == "/v1/validate" {
r = httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusServiceUnavailable {
t.Fatalf("%s status %d, want 503", path, w.Code)
}
}
r := httptest.NewRequest(http.MethodGet, "/healthz", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("health status %d, want 200", w.Code)
}
}
func TestConfigurableAPICORS(t *testing.T) {
h := testHandlerConfig(t, Config{
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", APIAllowedOrigin: "https://publisher.example",
})
r := httptest.NewRequest(http.MethodGet, "/v1/capabilities", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://publisher.example" {
t.Fatalf("CORS origin = %q", got)
}
}
func TestGeneratedURLsCanTargetSeparatePublicInstance(t *testing.T) {
s := &Server{cfg: Config{BaseURL: "http://bulk.internal", OutputBaseURL: "https://ai.example.org"}}
declarationURL, manifestURL, badgeURL := s.generatedURLs(url.Values{"lang": {"de"}})
for _, got := range []string{declarationURL, manifestURL, badgeURL} {
if !strings.HasPrefix(got, "https://ai.example.org/") {
t.Fatalf("generated URL %q does not use OUTPUT_BASE_URL", got)
}
}
}