package ingredient_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/food-ai/backend/internal/domain/ingredient" "github.com/food-ai/backend/internal/infra/middleware" "github.com/go-chi/chi/v5" ) // mockIngredientSearcher is an inline mock for ingredient.IngredientSearcher. type mockIngredientSearcher struct { searchFn func(ctx context.Context, query string, limit int) ([]*ingredient.IngredientMapping, error) } func (m *mockIngredientSearcher) Search(ctx context.Context, query string, limit int) ([]*ingredient.IngredientMapping, error) { return m.searchFn(ctx, query, limit) } type alwaysAuthValidator struct{ userID string } func (v *alwaysAuthValidator) ValidateAccessToken(_ string) (*middleware.TokenClaims, error) { return &middleware.TokenClaims{UserID: v.userID}, nil } func buildRouter(handler *ingredient.Handler) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Auth(&alwaysAuthValidator{userID: "user-1"})) router.Get("/ingredients/search", handler.Search) return router } func authorizedRequest(target string) *http.Request { request := httptest.NewRequest(http.MethodGet, target, bytes.NewReader(nil)) request.Header.Set("Authorization", "Bearer test-token") return request } func TestSearch_EmptyQuery_ReturnsEmptyArray(t *testing.T) { // When q is empty, the handler returns [] without calling the repository. handler := ingredient.NewHandler(&mockIngredientSearcher{}) router := buildRouter(handler) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest("/ingredients/search")) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } if body := recorder.Body.String(); body != "[]" { t.Errorf("expected body [], got %q", body) } } func TestSearch_LimitTooLarge_UsesDefault(t *testing.T) { // When limit > 50, the handler ignores it and uses default 10. calledLimit := 0 mockRepo := &mockIngredientSearcher{ searchFn: func(ctx context.Context, query string, limit int) ([]*ingredient.IngredientMapping, error) { calledLimit = limit return []*ingredient.IngredientMapping{}, nil }, } handler := ingredient.NewHandler(mockRepo) router := buildRouter(handler) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest("/ingredients/search?q=apple&limit=100")) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } if calledLimit != 10 { t.Errorf("expected repo called with limit=10 (default), got %d", calledLimit) } } func TestSearch_DefaultLimit(t *testing.T) { // When no limit is supplied, the handler uses default 10. calledLimit := 0 mockRepo := &mockIngredientSearcher{ searchFn: func(ctx context.Context, query string, limit int) ([]*ingredient.IngredientMapping, error) { calledLimit = limit return []*ingredient.IngredientMapping{}, nil }, } handler := ingredient.NewHandler(mockRepo) router := buildRouter(handler) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest("/ingredients/search?q=apple")) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } if calledLimit != 10 { t.Errorf("expected default limit 10, got %d", calledLimit) } } func TestSearch_ValidLimit(t *testing.T) { // limit=25 is within range and should be forwarded as-is. calledLimit := 0 mockRepo := &mockIngredientSearcher{ searchFn: func(ctx context.Context, query string, limit int) ([]*ingredient.IngredientMapping, error) { calledLimit = limit return []*ingredient.IngredientMapping{}, nil }, } handler := ingredient.NewHandler(mockRepo) router := buildRouter(handler) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest("/ingredients/search?q=apple&limit=25")) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } if calledLimit != 25 { t.Errorf("expected limit 25, got %d", calledLimit) } } func TestSearch_Success(t *testing.T) { mockRepo := &mockIngredientSearcher{ searchFn: func(ctx context.Context, query string, limit int) ([]*ingredient.IngredientMapping, error) { return []*ingredient.IngredientMapping{ {ID: "ing-1", CanonicalName: "apple"}, }, nil }, } handler := ingredient.NewHandler(mockRepo) router := buildRouter(handler) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest("/ingredients/search?q=apple")) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } var mappings []ingredient.IngredientMapping if decodeError := json.NewDecoder(recorder.Body).Decode(&mappings); decodeError != nil { t.Fatalf("decode response: %v", decodeError) } if len(mappings) != 1 { t.Errorf("expected 1 result, got %d", len(mappings)) } if mappings[0].CanonicalName != "apple" { t.Errorf("expected canonical_name=apple, got %q", mappings[0].CanonicalName) } }