package savedrecipe_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/food-ai/backend/internal/domain/savedrecipe" savedrecipemocks "github.com/food-ai/backend/internal/domain/savedrecipe/mocks" "github.com/food-ai/backend/internal/infra/middleware" "github.com/go-chi/chi/v5" ) type alwaysAuthValidator struct{ userID string } func (v *alwaysAuthValidator) ValidateAccessToken(_ string) (*middleware.TokenClaims, error) { return &middleware.TokenClaims{UserID: v.userID}, nil } func buildRouter(handler *savedrecipe.Handler, userID string) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Auth(&alwaysAuthValidator{userID: userID})) router.Post("/saved-recipes", handler.Save) router.Get("/saved-recipes", handler.List) router.Get("/saved-recipes/{id}", handler.GetByID) router.Delete("/saved-recipes/{id}", handler.Delete) return router } func authorizedRequest(method, target string, body []byte) *http.Request { request := httptest.NewRequest(method, target, bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer test-token") request.Header.Set("Content-Type", "application/json") return request } func makeUserSavedRecipe(id string) *savedrecipe.UserSavedRecipe { return &savedrecipe.UserSavedRecipe{ ID: id, UserID: "user-1", RecipeID: "recipe-1", SavedAt: time.Now(), DishName: "Pasta Carbonara", Ingredients: []savedrecipe.RecipeIngredient{}, Steps: []savedrecipe.RecipeStep{}, Tags: []string{}, } } func TestSave_MissingTitleAndRecipeID(t *testing.T) { handler := savedrecipe.NewHandler(&savedrecipemocks.MockSavedRecipeRepository{}) router := buildRouter(handler, "user-1") body, _ := json.Marshal(map[string]string{"cuisine": "italian"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/saved-recipes", body)) if recorder.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", recorder.Code) } } func TestSave_WithTitle(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ SaveFn: func(ctx context.Context, userID string, req savedrecipe.SaveRequest) (*savedrecipe.UserSavedRecipe, error) { return makeUserSavedRecipe("saved-1"), nil }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(savedrecipe.SaveRequest{Title: "My Recipe"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/saved-recipes", body)) if recorder.Code != http.StatusCreated { t.Errorf("expected 201, got %d", recorder.Code) } } func TestSave_WithRecipeID(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ SaveFn: func(ctx context.Context, userID string, req savedrecipe.SaveRequest) (*savedrecipe.UserSavedRecipe, error) { return makeUserSavedRecipe("saved-2"), nil }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(savedrecipe.SaveRequest{RecipeID: "recipe-42"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/saved-recipes", body)) if recorder.Code != http.StatusCreated { t.Errorf("expected 201, got %d", recorder.Code) } } func TestList_Success(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ ListFn: func(ctx context.Context, userID string) ([]*savedrecipe.UserSavedRecipe, error) { return []*savedrecipe.UserSavedRecipe{makeUserSavedRecipe("saved-1")}, nil }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodGet, "/saved-recipes", nil)) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } } func TestGetByID_NotFound(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ GetByIDFn: func(ctx context.Context, userID, id string) (*savedrecipe.UserSavedRecipe, error) { return nil, nil }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodGet, "/saved-recipes/nonexistent", nil)) if recorder.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", recorder.Code) } } func TestGetByID_Success(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ GetByIDFn: func(ctx context.Context, userID, id string) (*savedrecipe.UserSavedRecipe, error) { return makeUserSavedRecipe(id), nil }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodGet, "/saved-recipes/saved-1", nil)) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } } func TestDelete_NotFound(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ DeleteFn: func(ctx context.Context, userID, id string) error { return savedrecipe.ErrNotFound }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodDelete, "/saved-recipes/nonexistent", nil)) if recorder.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", recorder.Code) } } func TestDelete_Success(t *testing.T) { mockRepo := &savedrecipemocks.MockSavedRecipeRepository{ DeleteFn: func(ctx context.Context, userID, id string) error { return nil }, } handler := savedrecipe.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodDelete, "/saved-recipes/saved-1", nil)) if recorder.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", recorder.Code) } }