package bulk import ( "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" ) func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func TestRenderBatchUsesFixedCoreAndReplacesSubject(t *testing.T) { var mu sync.Mutex var subjects []string core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/render" { t.Fatalf("unexpected core path %q", r.URL.Path) } if got := r.URL.Query().Get("extent"); got != "partial" { t.Fatalf("template extent = %q", got) } subject := r.URL.Query().Get("subject") mu.Lock() subjects = append(subjects, subject) mu.Unlock() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "subject": subject, "declarationUrl": "https://public.example/declaration", "badgeUrl": "https://public.example/badge.svg", "manifestUrl": "https://public.example/manifest.json", "html": "ok", "markdown": "[ok]", "jsonLd": map[string]any{"@type": "AIUsageDeclaration", "subject": subject}, }) })) defer core.Close() h, err := New(Config{ ListenAddress: ":0", CoreInternalURL: core.URL, DisclosureBaseURL: "https://public.example", GeneratorURL: "https://public.example", PublicName: "Bulk", MaxURLs: 10, Workers: 2, RequestTimeout: 2 * time.Second, }, testLogger()) if err != nil { t.Fatal(err) } body := `{"template":"extent=partial&subject=https%3A%2F%2Fold.example%2Fignored","subjects":["https://content.example/a","https://content.example/b"]}` r := httptest.NewRequest(http.MethodPost, "/api/render-batch", strings.NewReader(body)) r.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } var response batchResponse if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatal(err) } if len(response.Results) != 2 || !response.Results[0].OK || !response.Results[1].OK { t.Fatalf("unexpected results: %#v", response.Results) } mu.Lock() defer mu.Unlock() if len(subjects) != 2 { t.Fatalf("core calls = %d", len(subjects)) } seen := map[string]bool{} for _, subject := range subjects { seen[subject] = true } if !seen["https://content.example/a"] || !seen["https://content.example/b"] || seen["https://old.example/ignored"] { t.Fatalf("subjects sent to core: %#v", subjects) } } func TestRenderBatchRejectsNonHTTPSubjectWithoutCallingCore(t *testing.T) { calls := 0 core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ w.WriteHeader(http.StatusOK) })) defer core.Close() h, err := New(Config{ListenAddress: ":0", CoreInternalURL: core.URL, DisclosureBaseURL: "https://public.example", GeneratorURL: "https://public.example", PublicName: "Bulk", MaxURLs: 10, Workers: 1, RequestTimeout: 2 * time.Second}, testLogger()) if err != nil { t.Fatal(err) } r := httptest.NewRequest(http.MethodPost, "/api/render-batch", strings.NewReader(`{"template":"extent=partial","subjects":["file:///etc/passwd"]}`)) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } var response batchResponse if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatal(err) } if len(response.Results) != 1 || response.Results[0].OK || !strings.Contains(response.Results[0].Error, "http(s)") { t.Fatalf("unexpected response: %#v", response.Results) } if calls != 0 { t.Fatalf("core was called %d times for invalid subject", calls) } } func TestRenderBatchHonoursConfiguredLimit(t *testing.T) { h, err := New(Config{ListenAddress: ":0", CoreInternalURL: "http://127.0.0.1:9", DisclosureBaseURL: "https://public.example", GeneratorURL: "https://public.example", PublicName: "Bulk", MaxURLs: 1, Workers: 1, RequestTimeout: time.Second}, testLogger()) if err != nil { t.Fatal(err) } r := httptest.NewRequest(http.MethodPost, "/api/render-batch", strings.NewReader(`{"template":"extent=partial","subjects":["https://example.org/a","https://example.org/b"]}`)) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusRequestEntityTooLarge { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } } func TestBulkUIUsesAccessibleDisclosurePreview(t *testing.T) { h, err := New(Config{ ListenAddress: ":0", CoreInternalURL: "http://127.0.0.1:9", DisclosureBaseURL: "https://public.example", GeneratorURL: "https://public.example", PublicName: "Bulk", MaxURLs: 10, Workers: 1, RequestTimeout: time.Second, }, testLogger()) if err != nil { t.Fatal(err) } indexReq := httptest.NewRequest(http.MethodGet, "/", nil) indexRes := httptest.NewRecorder() h.ServeHTTP(indexRes, indexReq) if indexRes.Code != http.StatusOK { t.Fatalf("index status %d: %s", indexRes.Code, indexRes.Body.String()) } if !strings.Contains(indexRes.Body.String(), "Kennzeichnung") { t.Fatalf("bulk result table does not expose the disclosure preview column") } jsReq := httptest.NewRequest(http.MethodGet, "/static/bulk.js", nil) jsRes := httptest.NewRecorder() h.ServeHTTP(jsRes, jsReq) if jsRes.Code != http.StatusOK { t.Fatalf("bulk.js status %d: %s", jsRes.Code, jsRes.Body.String()) } body := jsRes.Body.String() for _, required := range []string{"buildDisclosurePreview", "accessibleText", "SVG (nur Grafik)"} { if !strings.Contains(body, required) { t.Fatalf("bulk.js missing accessible preview marker %q", required) } } }