package processing import ( "encoding/json" "strings" "testing" "github.com/google/uuid" ) func TestParseV1ProcessingTypeFullAndSteps(t *testing.T) { storage, resp, err := ParseV1ProcessingType(nil) if err != nil || storage != "full" || resp != "full" { t.Fatalf("nil: storage=%q resp=%v err=%v", storage, resp, err) } storage, resp, err = ParseV1ProcessingType("title") if err != nil || storage != "title" || resp != "title" { t.Fatalf("title: storage=%q resp=%v err=%v", storage, resp, err) } storage, _, err = ParseV1ProcessingType("normalize_only") if err != nil || storage != "normalize_only" { t.Fatalf("normalize_only: storage=%q err=%v", storage, err) } storage, resp, err = ParseV1ProcessingType([]any{"title", "attributes"}) if err != nil || storage != `["title","attributes"]` { t.Fatalf("array: storage=%q resp=%v err=%v", storage, resp, err) } arr, ok := resp.([]string) if !ok || len(arr) != 2 { t.Fatalf("resp=%v", resp) } _, _, err = ParseV1ProcessingType("nope") if err == nil { t.Fatal("expected invalid type error") } } func TestMapJobStatusForV1(t *testing.T) { if got := MapJobStatusForV1("pending"); got != "PENDING" { t.Fatalf("got %q", got) } if got := MapJobStatusForV1("running"); got != "PROCESSING" { t.Fatalf("got %q", got) } if got := MapJobStatusForV1("completed"); got != "COMPLETED" { t.Fatalf("got %q", got) } for _, syn := range []string{"success", "done", "finished", "processed"} { if got := MapJobStatusForV1(syn); got != "COMPLETED" { t.Fatalf("%s -> %q", syn, got) } } if !JobStatusIncludesProducts("completed") || JobStatusIncludesProducts("running") { t.Fatal("JobStatusIncludesProducts mismatch") } } func TestMapV1JobItemStatus(t *testing.T) { if got := MapV1JobItemStatus("processed", true); got != "processed" { t.Fatalf("got %q", got) } if got := MapV1JobItemStatus("failed", false); got != "failed" { t.Fatalf("got %q", got) } if got := MapV1JobItemStatus("", false); got != "not_found" { t.Fatalf("got %q", got) } if got := MapV1JobItemStatus("", true); got != "processed" { t.Fatalf("got %q", got) } } func TestProjectV1ProcessJobItemsPartial(t *testing.T) { items := []V1ProcessJobItem{{ "ean": "123", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "status": "processed", "title": "T", "name": "T", "meta_title": "MT", "description": "D", "attributes": map[string]any{"brand": "X"}, "main_image": "https://example.com/a.jpg", "more_images": []string{"https://example.com/b.jpg"}, "eprel": nil, "category": "cat", "category_name": "Cat", }} out := ProjectV1ProcessJobItems("title", items) if len(out) != 1 { t.Fatalf("len=%d", len(out)) } raw, _ := json.Marshal(out[0]) var got map[string]any _ = json.Unmarshal(raw, &got) if got["title"] != "T" || got["ean"] != "123" { t.Fatalf("got=%v", got) } if got["name"] != "T" { t.Fatalf("name dual-mode alias missing or mismatched: %v", got) } if _, ok := got["attributes"]; ok { t.Fatalf("attributes should be projected out: %v", got) } if got["main_image"] != "https://example.com/a.jpg" { t.Fatalf("images always included: %v", got) } if got["status"] != "processed" || got["id"] != "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" { t.Fatalf("meta should be preserved: %v", got) } if got["processed_product_id"] != "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" { t.Fatalf("processed_product_id should be preserved: %v", got) } if got["raw_product_id"] != "cccccccc-cccc-cccc-cccc-cccccccccccc" { t.Fatalf("raw_product_id should be preserved: %v", got) } } func TestProjectV1ProcessJobItemsTitleDerivesName(t *testing.T) { items := []V1ProcessJobItem{{ "ean": "123", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "status": "processed", "title": "OnlyTitle", "meta_title": "MT", }} out := ProjectV1ProcessJobItems("title", items) if len(out) != 1 { t.Fatalf("len=%d", len(out)) } if out[0]["title"] != "OnlyTitle" { t.Fatalf("title=%v", out[0]["title"]) } if out[0]["name"] != "OnlyTitle" { t.Fatalf("name should mirror title when absent: %v", out[0]["name"]) } } func TestV1PlainDescriptionStandalone(t *testing.T) { got := v1PlainDescription("Line1
Line2 & ok") if strings.Contains(got, "<") { t.Fatalf("html left: %q", got) } if !strings.Contains(got, "Line1") || !strings.Contains(got, "Line2") || !strings.Contains(got, "& ok") { t.Fatalf("got=%q", got) } } func TestV1PlainDescriptionEdgeCases(t *testing.T) { nbsp := "\u00a0" cases := []struct { name string in string want string }{ {name: "empty", in: "", want: ""}, {name: "whitespace", in: " \n\t ", want: ""}, {name: "plain", in: "Hello world", want: "Hello world"}, {name: "html_br", in: "Hello
World
& more

bold", want: "Hello\nWorld\n& more\nbold"}, {name: "one_element_json_array_html", in: `["Hello
World"]`, want: "Hello\nWorld"}, {name: "multi_element_array", in: `["Para one.", "Para two."]`, want: "Para one.\nPara two."}, {name: "nested_array", in: `[["inner
x"]]`, want: "inner\nx"}, {name: "json_quoted_string", in: `"Plain & quoted"`, want: "Plain & quoted"}, {name: "empty_array", in: `[]`, want: ""}, {name: "array_with_nulls", in: `[null, "keep", null, ""]`, want: "keep"}, {name: "array_with_object_ignored", in: `[{"text":"x"}, "ok"]`, want: "ok"}, {name: "invalid_bracket_text", in: `[not json`, want: "[not json"}, {name: "nbsp_and_entities", in: "A" + nbsp + "B C", want: "A B C"}, {name: "literal_unicode_escape", in: `A\u00a0B`, want: "A B"}, {name: "double_encoded_array", in: `"[\"Hello
World\"]"`, want: "Hello\nWorld"}, {name: "whitespace_around_array", in: " [\" spaced \"] ", want: "spaced"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := v1PlainDescription(tc.in) if got != tc.want { t.Fatalf("in=%q got=%q want=%q", tc.in, got, tc.want) } if strings.Contains(got, "<") { t.Fatalf("html left: %q", got) } }) } } func TestApplyV1ProcessItemIDsDualMode(t *testing.T) { processed := mustParseTestUUID(t, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") raw := mustParseTestUUID(t, "cccccccc-cccc-cccc-cccc-cccccccccccc") item := V1ProcessJobItem{"ean": "1", "status": "processed"} applyV1ProcessItemIDs(item, &processed, &raw) if item["id"] != processed.String() { t.Fatalf("id=%v", item["id"]) } if item["processed_product_id"] != processed.String() { t.Fatalf("processed_product_id=%v", item["processed_product_id"]) } if item["raw_product_id"] != raw.String() { t.Fatalf("raw_product_id=%v", item["raw_product_id"]) } missing := V1ProcessJobItem{"ean": "2", "status": "not_found"} applyV1ProcessItemIDs(missing, nil, &raw) if _, ok := missing["id"]; ok { t.Fatalf("id must stay absent without processed row: %v", missing) } if missing["raw_product_id"] != raw.String() { t.Fatalf("raw_product_id on not_found: %v", missing) } } func TestFormatJobStatusResponseAddsItems(t *testing.T) { jobID := mustParseTestUUID(t, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") job := Job{ID: jobID, Status: "completed", TotalProducts: 1} items := []V1ProcessJobItem{{"ean": "1", "status": "processed", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}} out, ok := FormatJobStatusResponse(job, items, true).(map[string]any) if !ok { t.Fatalf("type=%T", FormatJobStatusResponse(job, items, true)) } if out["status"] != "completed" { t.Fatalf("status=%v", out["status"]) } if n, ok := out["total_items"].(int); !ok || n != 1 { t.Fatalf("total_items=%v (%T)", out["total_items"], out["total_items"]) } arr, ok := out["items"].([]V1ProcessJobItem) if !ok || len(arr) != 1 { t.Fatalf("items=%v (%T)", out["items"], out["items"]) } } func mustParseTestUUID(t *testing.T, s string) uuid.UUID { t.Helper() id, err := uuid.Parse(s) if err != nil { t.Fatal(err) } return id }