//go:build integration package ingredient import ( "context" "encoding/json" "testing" "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) } // Update with same spoonacular_id 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_ListUntranslated(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() cat := "produce" unit := "g" // Insert 3 without 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 with translation (shouldn't appear in untranslated list) id := 4100 ruName := "помидор" withTranslation := &IngredientMapping{ CanonicalName: "tomato", CanonicalNameRu: &ruName, SpoonacularID: &id, Aliases: json.RawMessage(`[]`), Category: &cat, DefaultUnit: &unit, } saved, err := repo.Upsert(ctx, withTranslation) if err != nil { t.Fatalf("upsert with translation: %v", err) } // The upsert doesn't set canonical_name_ru because the UPDATE clause doesn't include it // We need to manually set it after if err := repo.UpdateTranslation(ctx, saved.ID, "помидор", []string{"помидор", "томат"}); err != nil { t.Fatalf("update translation: %v", err) } untranslated, err := repo.ListUntranslated(ctx, 10, 0) if err != nil { t.Fatalf("list untranslated: %v", err) } // Should return the 3 without translation (carrot, onion, garlic) // The translated tomato should not appear for _, m := range untranslated { if m.CanonicalName == "tomato" { t.Error("translated ingredient should not appear in ListUntranslated") } } if len(untranslated) < 3 { t.Errorf("expected at least 3 untranslated, got %d", len(untranslated)) } } func TestIngredientRepository_UpdateTranslation(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.UpdateTranslation(ctx, saved.ID, "куриная грудка", []string{"куриная грудка", "куриное филе"}) if err != nil { t.Fatalf("update translation: %v", err) } got, err := repo.GetByID(ctx, saved.ID) if err != nil { t.Fatalf("get by id: %v", err) } if got.CanonicalNameRu == nil || *got.CanonicalNameRu != "куриная грудка" { t.Errorf("expected canonical_name_ru='куриная грудка', got %v", got.CanonicalNameRu) } var aliases []string if err := json.Unmarshal(got.Aliases, &aliases); err != nil { t.Fatalf("unmarshal aliases: %v", err) } hasRu := false for _, a := range aliases { if a == "куриное филе" { hasRu = true break } } if !hasRu { t.Errorf("Russian alias not found in aliases: %v", aliases) } }