All checks were successful
release-tag / release-image (push) Successful in 1m52s
430 lines
15 KiB
Go
430 lines
15 KiB
Go
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(), "AI ASSISTED") {
|
|
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{"Declare how content was made.", "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(), "Declare how content was made.") {
|
|
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"`,
|
|
`Declaration Status`,
|
|
} {
|
|
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"`,
|
|
`Status: Declared; die Angaben sind zusätzlich technisch protokolliert. Daraus folgt keine automatische Verifikation.`,
|
|
`<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{
|
|
"Content needs context.",
|
|
"Regulatorischer Kontext: EU AI Act, Artikel 50",
|
|
"UCNG ist kein offizielles System der Europäischen Union",
|
|
"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(), "Content needs context.") {
|
|
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"`,
|
|
`Optionaler 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 technische 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(), "AI GENERATED") || !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{`Regulatorische 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)
|
|
}
|
|
}
|
|
}
|