//go:build integration package ingredient import ( "context" "encoding/json" "testing" "github.com/food-ai/backend/internal/locale" "github.com/food-ai/backend/internal/testutil" ) func TestIngredientRepository_Upsert_Insert(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 1001 cat := "produce" unit := "g" cal := 52.0 m := &IngredientMapping{ CanonicalName: "apple", SpoonacularID: &id, Aliases: json.RawMessage(`["apple", "apples"]`), Category: &cat, DefaultUnit: &unit, CaloriesPer100g: &cal, } got, err := repo.Upsert(ctx, m) if err != nil { t.Fatalf("upsert: %v", err) } if got.ID == "" { t.Error("expected non-empty ID") } if got.CanonicalName != "apple" { t.Errorf("canonical_name: want apple, got %s", got.CanonicalName) } if *got.CaloriesPer100g != 52.0 { t.Errorf("calories: want 52.0, got %v", got.CaloriesPer100g) } } func TestIngredientRepository_Upsert_ConflictUpdates(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 2001 cat := "produce" unit := "g" first := &IngredientMapping{ CanonicalName: "banana", SpoonacularID: &id, Aliases: json.RawMessage(`["banana"]`), Category: &cat, DefaultUnit: &unit, } got1, err := repo.Upsert(ctx, first) if err != nil { t.Fatalf("first upsert: %v", err) } cal := 89.0 second := &IngredientMapping{ CanonicalName: "banana_updated", SpoonacularID: &id, Aliases: json.RawMessage(`["banana", "bananas"]`), Category: &cat, DefaultUnit: &unit, CaloriesPer100g: &cal, } got2, err := repo.Upsert(ctx, second) if err != nil { t.Fatalf("second upsert: %v", err) } if got1.ID != got2.ID { t.Errorf("ID changed on conflict update: %s != %s", got1.ID, got2.ID) } if got2.CanonicalName != "banana_updated" { t.Errorf("canonical_name not updated: got %s", got2.CanonicalName) } if got2.CaloriesPer100g == nil || *got2.CaloriesPer100g != 89.0 { t.Errorf("calories not updated: got %v", got2.CaloriesPer100g) } } func TestIngredientRepository_GetBySpoonacularID_Found(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 3001 cat := "dairy" unit := "g" _, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "cheese", SpoonacularID: &id, Aliases: json.RawMessage(`["cheese"]`), Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert: %v", err) } got, err := repo.GetBySpoonacularID(ctx, id) if err != nil { t.Fatalf("get: %v", err) } if got == nil { t.Fatal("expected non-nil result") } if got.CanonicalName != "cheese" { t.Errorf("want cheese, got %s", got.CanonicalName) } } func TestIngredientRepository_GetBySpoonacularID_NotFound(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() got, err := repo.GetBySpoonacularID(ctx, 99999999) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != nil { t.Error("expected nil result for missing ID") } } func TestIngredientRepository_ListMissingTranslation(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" // Insert 3 without any translation. for i, name := range []string{"carrot", "onion", "garlic"} { id := 4000 + i _, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: name, SpoonacularID: &id, Aliases: json.RawMessage(`[]`), Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert %s: %v", name, err) } } // Insert 1 and add a Russian translation — should not appear in the result. id := 4100 saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "tomato", SpoonacularID: &id, Aliases: json.RawMessage(`[]`), Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert tomato: %v", err) } if err := repo.UpsertTranslation(ctx, saved.ID, "ru", "помидор", json.RawMessage(`["помидор","томат"]`)); err != nil { t.Fatalf("upsert translation: %v", err) } missing, err := repo.ListMissingTranslation(ctx, "ru", 10, 0) if err != nil { t.Fatalf("list missing translation: %v", err) } for _, m := range missing { if m.CanonicalName == "tomato" { t.Error("translated ingredient should not appear in ListMissingTranslation") } } if len(missing) < 3 { t.Errorf("expected at least 3 untranslated, got %d", len(missing)) } } func TestIngredientRepository_UpsertTranslation(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 5001 cat := "meat" unit := "g" saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "chicken_breast", SpoonacularID: &id, Aliases: json.RawMessage(`["chicken breast"]`), Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert: %v", err) } err = repo.UpsertTranslation(ctx, saved.ID, "ru", "куриная грудка", json.RawMessage(`["куриная грудка","куриное филе"]`)) if err != nil { t.Fatalf("upsert translation: %v", err) } // Retrieve with Russian context — CanonicalName should be the Russian name. ruCtx := locale.WithLang(ctx, "ru") got, err := repo.GetByID(ruCtx, saved.ID) if err != nil { t.Fatalf("get by id: %v", err) } if got.CanonicalName != "куриная грудка" { t.Errorf("expected CanonicalName='куриная грудка', got %q", got.CanonicalName) } // Retrieve with English context (default) — CanonicalName should be the English name. enCtx := locale.WithLang(ctx, "en") gotEn, err := repo.GetByID(enCtx, saved.ID) if err != nil { t.Fatalf("get by id (en): %v", err) } if gotEn.CanonicalName != "chicken_breast" { t.Errorf("expected English CanonicalName='chicken_breast', got %q", gotEn.CanonicalName) } }