package extract_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/netcell-it/deklarix/internal/extract" "github.com/netcell-it/deklarix/internal/rules" ) // toolUseResponse baut eine minimale Anthropic-Messages-API-Antwort mit // genau einem tool_use-Block, wie sie extract.Client erwartet. func toolUseResponse(t *testing.T, toolName string, args any) []byte { t.Helper() input, err := json.Marshal(args) if err != nil { t.Fatalf("marshal args: %v", err) } body := map[string]any{ "content": []map[string]any{ {"type": "tool_use", "name": toolName, "input": json.RawMessage(input)}, }, } data, err := json.Marshal(body) if err != nil { t.Fatalf("marshal response: %v", err) } return data } func newTestClient(t *testing.T, handler http.HandlerFunc) *extract.Client { t.Helper() server := httptest.NewServer(handler) t.Cleanup(server.Close) c, err := extract.NewClient("test-key", extract.WithBaseURL(server.URL)) if err != nil { t.Fatalf("NewClient: %v", err) } return c } func TestNewClientRejectsEmptyAPIKey(t *testing.T) { if _, err := extract.NewClient(""); err == nil { t.Fatal("expected error for empty apiKey, got nil") } } func TestExtractRejectsEmptyInput(t *testing.T) { called := false c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { called = true }) _, err := c.Extract(context.Background(), extract.Input{Platform: "instagram"}) if err == nil { t.Fatal("expected error for empty caption+image, got nil") } if called { t.Fatal("expected no HTTP call for invalid input, but the server was called") } } func TestExtractSuccess(t *testing.T) { var gotBody map[string]any c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/messages" { t.Errorf("unexpected path: %s", r.URL.Path) } if got := r.Header.Get("x-api-key"); got != "test-key" { t.Errorf("x-api-key = %q, want test-key", got) } if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { t.Fatalf("decode request body: %v", err) } w.WriteHeader(http.StatusOK) w.Write(toolUseResponse(t, "extrahiere_fakten", map[string]any{ "gegenleistung": "bezahlt", "kennzeichnung_vorhanden": true, "kennzeichnung_wortlaut": "Werbung", "kennzeichnung_vor_kuerzung": false, })) }) got, err := c.Extract(context.Background(), extract.Input{ Platform: "instagram", Jurisdiction: "DE", Caption: "Schaut euch dieses Produkt an! Werbung wegen ...", ImageMediaType: "image/jpeg", ImageData: []byte("fake-jpeg-bytes"), }) if err != nil { t.Fatalf("Extract: %v", err) } want := rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationPaid, DisclosurePresent: true, DisclosureWording: "Werbung", DisclosureBeforeCut: false, } if got != want { t.Fatalf("Extract() = %+v, want %+v", got, want) } toolChoice, _ := gotBody["tool_choice"].(map[string]any) if toolChoice["name"] != "extrahiere_fakten" { t.Errorf("tool_choice.name = %v, want extrahiere_fakten", toolChoice["name"]) } if gotBody["system"] == nil || gotBody["system"] == "" { t.Error("expected a non-empty system prompt in the request") } } func TestExtractPassesThroughUnclear(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(toolUseResponse(t, "extrahiere_fakten", map[string]any{ "gegenleistung": "unklar", "kennzeichnung_vorhanden": false, "kennzeichnung_wortlaut": "", "kennzeichnung_vor_kuerzung": false, })) }) got, err := c.Extract(context.Background(), extract.Input{Platform: "tiktok", Caption: "..."}) if err != nil { t.Fatalf("Extract: %v", err) } if got.Consideration != rules.ConsiderationUnclear { t.Fatalf("Consideration = %q, want unklar", got.Consideration) } } func TestExtractRejectsInvalidConsiderationValue(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(toolUseResponse(t, "extrahiere_fakten", map[string]any{ "gegenleistung": "vielleicht", // nicht im Enum "kennzeichnung_vorhanden": false, "kennzeichnung_wortlaut": "", "kennzeichnung_vor_kuerzung": false, })) }) if _, err := c.Extract(context.Background(), extract.Input{Platform: "instagram", Caption: "x"}); err == nil { t.Fatal("expected error for out-of-enum gegenleistung value, got nil") } } func TestExtractRejectsMissingToolUseBlock(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"content":[{"type":"text","text":"kein tool_use hier"}]}`)) }) if _, err := c.Extract(context.Background(), extract.Input{Platform: "instagram", Caption: "x"}); err == nil { t.Fatal("expected error when response has no tool_use block, got nil") } } func TestExtractRejectsMalformedToolInput(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"content":[{"type":"tool_use","name":"extrahiere_fakten","input":"not-an-object"}]}`)) }) if _, err := c.Extract(context.Background(), extract.Input{Platform: "instagram", Caption: "x"}); err == nil { t.Fatal("expected error for malformed tool input, got nil") } } func TestExtractPropagatesAPIError(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte(`{"error":{"type":"rate_limit_error","message":"too many requests"}}`)) }) _, err := c.Extract(context.Background(), extract.Input{Platform: "instagram", Caption: "x"}) if err == nil { t.Fatal("expected error for non-200 status, got nil") } if got := err.Error(); !strings.Contains(got, "too many requests") { t.Fatalf("error %q does not mention API message", got) } }