//go:build integration package recipe import ( "context" "encoding/json" "testing" "github.com/food-ai/backend/internal/locale" "github.com/food-ai/backend/internal/testutil" ) func TestRecipeRepository_Upsert_Insert(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 10001 cuisine := "italian" diff := "easy" cookTime := 30 servings := 4 rec := &Recipe{ Source: "spoonacular", SpoonacularID: &id, Title: "Pasta Carbonara", Cuisine: &cuisine, Difficulty: &diff, CookTimeMin: &cookTime, Servings: &servings, Ingredients: json.RawMessage(`[{"name":"pasta","amount":200,"unit":"g"}]`), Steps: json.RawMessage(`[{"number":1,"description":"Boil pasta"}]`), Tags: json.RawMessage(`["italian"]`), } got, err := repo.Upsert(ctx, rec) if err != nil { t.Fatalf("upsert: %v", err) } if got.ID == "" { t.Error("expected non-empty ID") } if got.Title != "Pasta Carbonara" { t.Errorf("title: want Pasta Carbonara, got %s", got.Title) } if got.SpoonacularID == nil || *got.SpoonacularID != id { t.Errorf("spoonacular_id: want %d, got %v", id, got.SpoonacularID) } } func TestRecipeRepository_Upsert_ConflictUpdates(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 20001 cuisine := "mexican" diff := "medium" first := &Recipe{ Source: "spoonacular", SpoonacularID: &id, Title: "Tacos", Cuisine: &cuisine, Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`[]`), } got1, err := repo.Upsert(ctx, first) if err != nil { t.Fatalf("first upsert: %v", err) } second := &Recipe{ Source: "spoonacular", SpoonacularID: &id, Title: "Beef Tacos", Cuisine: &cuisine, Difficulty: &diff, Ingredients: json.RawMessage(`[{"name":"beef","amount":300,"unit":"g"}]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`[]`), } 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.Title != "Beef Tacos" { t.Errorf("title not updated: got %s", got2.Title) } } func TestRecipeRepository_GetByID_Found(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 30001 diff := "easy" rec := &Recipe{ Source: "spoonacular", SpoonacularID: &id, Title: "Greek Salad", Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`["vegetarian"]`), } saved, err := repo.Upsert(ctx, rec) if err != nil { t.Fatalf("upsert: %v", err) } got, err := repo.GetByID(ctx, saved.ID) if err != nil { t.Fatalf("get by id: %v", err) } if got == nil { t.Fatal("expected non-nil result") } if got.Title != "Greek Salad" { t.Errorf("want Greek Salad, got %s", got.Title) } } func TestRecipeRepository_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 for non-existent ID") } } func TestRecipeRepository_ListMissingTranslation_Pagination(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() diff := "easy" for i := 0; i < 5; i++ { spID := 40000 + i _, err := repo.Upsert(ctx, &Recipe{ Source: "spoonacular", SpoonacularID: &spID, Title: "Recipe " + string(rune('A'+i)), Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`[]`), }) if err != nil { t.Fatalf("upsert recipe %d: %v", i, err) } } missing, err := repo.ListMissingTranslation(ctx, "ru", 3, 0) if err != nil { t.Fatalf("list missing translation: %v", err) } if len(missing) != 3 { t.Errorf("expected 3 results with limit=3, got %d", len(missing)) } } func TestRecipeRepository_UpsertTranslation(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 50001 diff := "medium" saved, err := repo.Upsert(ctx, &Recipe{ Source: "spoonacular", SpoonacularID: &id, Title: "Chicken Tikka Masala", Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[{"number":1,"description":"Heat oil"}]`), Tags: json.RawMessage(`[]`), }) if err != nil { t.Fatalf("upsert: %v", err) } titleRu := "Курица Тикка Масала" descRu := "Классическое индийское блюдо" stepsRu := json.RawMessage(`[{"number":1,"description":"Разогрейте масло"}]`) if err := repo.UpsertTranslation(ctx, saved.ID, "ru", &titleRu, &descRu, nil, stepsRu); err != nil { t.Fatalf("upsert translation: %v", err) } // Retrieve with Russian context — title and steps should be translated. ruCtx := locale.WithLang(ctx, "ru") got, err := repo.GetByID(ruCtx, saved.ID) if err != nil { t.Fatalf("get by id: %v", err) } if got.Title != titleRu { t.Errorf("expected title=%q, got %q", titleRu, got.Title) } if got.Description == nil || *got.Description != descRu { t.Errorf("expected description=%q, got %v", descRu, got.Description) } var steps []RecipeStep if err := json.Unmarshal(got.Steps, &steps); err != nil { t.Fatalf("unmarshal steps: %v", err) } if len(steps) == 0 || steps[0].Description != "Разогрейте масло" { t.Errorf("expected Russian step description, got %v", steps) } // Retrieve with English context — should return original English content. 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.Title != "Chicken Tikka Masala" { t.Errorf("expected English title, got %q", gotEn.Title) } } func TestRecipeRepository_ListMissingTranslation_ExcludesTranslated(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() diff := "easy" // Insert untranslated recipes. for i := 0; i < 3; i++ { spID := 60000 + i _, err := repo.Upsert(ctx, &Recipe{ Source: "spoonacular", SpoonacularID: &spID, Title: "Untranslated " + string(rune('A'+i)), Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`[]`), }) if err != nil { t.Fatalf("upsert: %v", err) } } // Insert one recipe and add a Russian translation. spID := 60100 translated, err := repo.Upsert(ctx, &Recipe{ Source: "spoonacular", SpoonacularID: &spID, Title: "Translated Recipe", Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`[]`), }) if err != nil { t.Fatalf("upsert translated: %v", err) } titleRu := "Переведённый рецепт" if err := repo.UpsertTranslation(ctx, translated.ID, "ru", &titleRu, nil, nil, nil); 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 _, r := range missing { if r.Title == "Translated Recipe" { t.Error("translated recipe should not appear in ListMissingTranslation") } } if len(missing) < 3 { t.Errorf("expected at least 3 missing, got %d", len(missing)) } } func TestRecipeRepository_GIN_Tags(t *testing.T) { pool := testutil.SetupTestDB(t) repo := NewRepository(pool) ctx := context.Background() id := 70001 diff := "easy" _, err := repo.Upsert(ctx, &Recipe{ Source: "spoonacular", SpoonacularID: &id, Title: "Veggie Bowl", Difficulty: &diff, Ingredients: json.RawMessage(`[]`), Steps: json.RawMessage(`[]`), Tags: json.RawMessage(`["vegetarian","gluten-free"]`), }) if err != nil { t.Fatalf("upsert: %v", err) } // GIN index query: tags @> '["vegetarian"]' var count int row := pool.QueryRow(ctx, `SELECT count(*) FROM recipes WHERE tags @> '["vegetarian"]'::jsonb AND spoonacular_id = $1`, id) if err := row.Scan(&count); err != nil { t.Fatalf("query: %v", err) } if count != 1 { t.Errorf("expected 1 vegetarian recipe, got %d", count) } }