//go:build integration package ingredient_test import ( "context" "testing" "github.com/food-ai/backend/internal/ingredient" "github.com/food-ai/backend/internal/infra/locale" "github.com/food-ai/backend/internal/testutil" ) func TestIngredientRepository_Upsert_Insert(t *testing.T) { pool := testutil.SetupTestDB(t) repo := ingredient.NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" cal := 52.0 mapping := &ingredient.IngredientMapping{ CanonicalName: "apple", Category: &cat, DefaultUnit: &unit, CaloriesPer100g: &cal, } got, upsertError := repo.Upsert(ctx, mapping) if upsertError != nil { t.Fatalf("upsert: %v", upsertError) } 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 := ingredient.NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" first := &ingredient.IngredientMapping{ CanonicalName: "banana", Category: &cat, DefaultUnit: &unit, } got1, firstUpsertError := repo.Upsert(ctx, first) if firstUpsertError != nil { t.Fatalf("first upsert: %v", firstUpsertError) } cal := 89.0 second := &ingredient.IngredientMapping{ CanonicalName: "banana", Category: &cat, DefaultUnit: &unit, CaloriesPer100g: &cal, } got2, secondUpsertError := repo.Upsert(ctx, second) if secondUpsertError != nil { t.Fatalf("second upsert: %v", secondUpsertError) } 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 := ingredient.NewRepository(pool) ctx := context.Background() cat := "dairy" unit := "g" saved, upsertError := repo.Upsert(ctx, &ingredient.IngredientMapping{ CanonicalName: "cheese", Category: &cat, DefaultUnit: &unit, }) if upsertError != nil { t.Fatalf("upsert: %v", upsertError) } got, getError := repo.GetByID(ctx, saved.ID) if getError != nil { t.Fatalf("get: %v", getError) } 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 := ingredient.NewRepository(pool) ctx := context.Background() got, getError := repo.GetByID(ctx, "00000000-0000-0000-0000-000000000000") if getError != nil { t.Fatalf("unexpected error: %v", getError) } if got != nil { t.Error("expected nil result for missing ID") } } func TestIngredientRepository_ListMissingTranslation(t *testing.T) { pool := testutil.SetupTestDB(t) repo := ingredient.NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" // Insert 3 without any translation. for _, name := range []string{"carrot", "onion", "garlic"} { _, upsertError := repo.Upsert(ctx, &ingredient.IngredientMapping{ CanonicalName: name, Category: &cat, DefaultUnit: &unit, }) if upsertError != nil { t.Fatalf("upsert %s: %v", name, upsertError) } } // Insert 1 and add a translation — should not appear in the result. saved, upsertError := repo.Upsert(ctx, &ingredient.IngredientMapping{ CanonicalName: "tomato", Category: &cat, DefaultUnit: &unit, }) if upsertError != nil { t.Fatalf("upsert tomato: %v", upsertError) } if translationError := repo.UpsertTranslation(ctx, saved.ID, "ru", "помидор"); translationError != nil { t.Fatalf("upsert translation: %v", translationError) } missing, listError := repo.ListMissingTranslation(ctx, "ru", 10, 0) if listError != nil { t.Fatalf("list missing translation: %v", listError) } for _, mapping := range missing { if mapping.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 := ingredient.NewRepository(pool) ctx := context.Background() cat := "meat" unit := "g" saved, upsertError := repo.Upsert(ctx, &ingredient.IngredientMapping{ CanonicalName: "chicken_breast", Category: &cat, DefaultUnit: &unit, }) if upsertError != nil { t.Fatalf("upsert: %v", upsertError) } if translationError := repo.UpsertTranslation(ctx, saved.ID, "ru", "куриная грудка"); translationError != nil { t.Fatalf("upsert translation: %v", translationError) } // Retrieve with Russian context — CanonicalName should be the Russian name. russianContext := locale.WithLang(ctx, "ru") got, getError := repo.GetByID(russianContext, saved.ID) if getError != nil { t.Fatalf("get by id: %v", getError) } if got.CanonicalName != "куриная грудка" { t.Errorf("expected CanonicalName='куриная грудка', got %q", got.CanonicalName) } // Retrieve with English context (default) — CanonicalName should be the English name. englishContext := locale.WithLang(ctx, "en") gotEnglish, getEnglishError := repo.GetByID(englishContext, saved.ID) if getEnglishError != nil { t.Fatalf("get by id (en): %v", getEnglishError) } if gotEnglish.CanonicalName != "chicken_breast" { t.Errorf("expected English CanonicalName='chicken_breast', got %q", gotEnglish.CanonicalName) } } func TestIngredientRepository_UpsertAliases(t *testing.T) { pool := testutil.SetupTestDB(t) repo := ingredient.NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" saved, upsertError := repo.Upsert(ctx, &ingredient.IngredientMapping{ CanonicalName: "apple_test_aliases", Category: &cat, DefaultUnit: &unit, }) if upsertError != nil { t.Fatalf("upsert: %v", upsertError) } aliases := []string{"apple", "apples", "red apple"} if aliasError := repo.UpsertAliases(ctx, saved.ID, "en", aliases); aliasError != nil { t.Fatalf("upsert aliases: %v", aliasError) } // Idempotent — second call should not fail. if aliasError := repo.UpsertAliases(ctx, saved.ID, "en", aliases); aliasError != nil { t.Fatalf("second upsert aliases: %v", aliasError) } // Retrieve with English context — aliases should appear. englishContext := locale.WithLang(ctx, "en") got, getError := repo.GetByID(englishContext, saved.ID) if getError != nil { t.Fatalf("get by id: %v", getError) } 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 := ingredient.NewRepository(pool) ctx := context.Background() // Upsert an ingredient with a known category. cat := "dairy" unit := "g" saved, upsertError := repo.Upsert(ctx, &ingredient.IngredientMapping{ CanonicalName: "milk_test_category", Category: &cat, DefaultUnit: &unit, }) if upsertError != nil { t.Fatalf("upsert: %v", upsertError) } // The migration already inserted Russian translations for known categories. // Retrieve with Russian context — CategoryName should be set. russianContext := locale.WithLang(ctx, "ru") got, getError := repo.GetByID(russianContext, saved.ID) if getError != nil { t.Fatalf("get by id: %v", getError) } 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 translationError := repo.UpsertCategoryTranslation(ctx, "dairy", "de", "Milchprodukte"); translationError != nil { t.Fatalf("upsert category translation: %v", translationError) } germanContext := locale.WithLang(ctx, "de") gotGerman, getGermanError := repo.GetByID(germanContext, saved.ID) if getGermanError != nil { t.Fatalf("get by id (de): %v", getGermanError) } if gotGerman.CategoryName == nil || *gotGerman.CategoryName != "Milchprodukte" { t.Errorf("expected CategoryName='Milchprodukte', got %v", gotGerman.CategoryName) } }