//go:build integration package ingredient import ( "context" "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() cat := "produce" unit := "g" cal := 52.0 m := &IngredientMapping{ CanonicalName: "apple", 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() cat := "produce" unit := "g" first := &IngredientMapping{ CanonicalName: "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", 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" { 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_GetByID_Found(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() cat := "dairy" unit := "g" saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "cheese", Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert: %v", err) } got, err := repo.GetByID(ctx, saved.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_GetByID_NotFound(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() got, err := repo.GetByID(ctx, "00000000-0000-0000-0000-000000000000") 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 _, name := range []string{"carrot", "onion", "garlic"} { _, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: name, Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert %s: %v", name, err) } } // Insert 1 and add a translation — should not appear in the result. saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "tomato", Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert tomato: %v", err) } if err := repo.UpsertTranslation(ctx, saved.ID, "ru", "помидор"); 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() cat := "meat" unit := "g" saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "chicken_breast", Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert: %v", err) } if err := repo.UpsertTranslation(ctx, saved.ID, "ru", "куриная грудка"); 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) } } func TestIngredientRepository_UpsertAliases(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "apple_test_aliases", Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert: %v", err) } aliases := []string{"apple", "apples", "red apple"} if err := repo.UpsertAliases(ctx, saved.ID, "en", aliases); err != nil { t.Fatalf("upsert aliases: %v", err) } // Idempotent — second call should not fail. if err := repo.UpsertAliases(ctx, saved.ID, "en", aliases); err != nil { t.Fatalf("second upsert aliases: %v", err) } // Retrieve with English context — aliases should appear. enCtx := locale.WithLang(ctx, "en") got, err := repo.GetByID(enCtx, saved.ID) if err != nil { t.Fatalf("get by id: %v", err) } if string(got.Aliases) == "[]" || string(got.Aliases) == "null" { t.Errorf("expected non-empty aliases, got %s", got.Aliases) } } func TestIngredientRepository_UpsertCategoryTranslation(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() // Upsert an ingredient with a known category. cat := "dairy" unit := "g" saved, err := repo.Upsert(ctx, &IngredientMapping{ CanonicalName: "milk_test_category", Category: &cat, DefaultUnit: &unit, }) if err != nil { t.Fatalf("upsert: %v", err) } // The migration already inserted Russian translations for known categories. // Retrieve with Russian context — CategoryName should be set. ruCtx := locale.WithLang(ctx, "ru") got, err := repo.GetByID(ruCtx, saved.ID) if err != nil { t.Fatalf("get by id: %v", err) } if got.CategoryName == nil || *got.CategoryName == "" { t.Error("expected non-empty CategoryName for 'dairy' in Russian") } // Upsert a new translation and verify it is returned. if err := repo.UpsertCategoryTranslation(ctx, "dairy", "de", "Milchprodukte"); err != nil { t.Fatalf("upsert category translation: %v", err) } deCtx := locale.WithLang(ctx, "de") gotDe, err := repo.GetByID(deCtx, saved.ID) if err != nil { t.Fatalf("get by id (de): %v", err) } if gotDe.CategoryName == nil || *gotDe.CategoryName != "Milchprodukte" { t.Errorf("expected CategoryName='Milchprodukte', got %v", gotDe.CategoryName) } }