package diary_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/food-ai/backend/internal/domain/diary" diarymocks "github.com/food-ai/backend/internal/domain/diary/mocks" "github.com/food-ai/backend/internal/infra/middleware" "github.com/go-chi/chi/v5" ) // alwaysAuthValidator injects a fixed user ID via the Auth middleware. type alwaysAuthValidator struct{ userID string } func (v *alwaysAuthValidator) ValidateAccessToken(_ string) (*middleware.TokenClaims, error) { return &middleware.TokenClaims{UserID: v.userID}, nil } // buildRouter wraps handler methods with auth middleware on a chi router. func buildRouter(handler *diary.Handler, userID string) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Auth(&alwaysAuthValidator{userID: userID})) router.Get("/diary", handler.GetByDate) router.Post("/diary", handler.Create) router.Delete("/diary/{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 newHandler(diaryRepo diary.DiaryRepository, dishRepo diary.DishRepository, recipeRepo diary.RecipeRepository) *diary.Handler { return diary.NewHandler(diaryRepo, dishRepo, recipeRepo) } func defaultMocks() (*diarymocks.MockDiaryRepository, *diarymocks.MockDishRepository, *diarymocks.MockRecipeRepository) { return &diarymocks.MockDiaryRepository{}, &diarymocks.MockDishRepository{}, &diarymocks.MockRecipeRepository{} } func TestGetByDate_MissingQueryParam(t *testing.T) { diaryRepo, dishRepo, recipeRepo := defaultMocks() handler := newHandler(diaryRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodGet, "/diary", nil)) if recorder.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", recorder.Code) } } func TestGetByDate_Success(t *testing.T) { mockRepo := &diarymocks.MockDiaryRepository{ ListByDateFn: func(ctx context.Context, userID, date string) ([]*diary.Entry, error) { return []*diary.Entry{ {ID: "entry-1", Date: date, MealType: "breakfast", Name: "Oatmeal", Portions: 1}, }, nil }, } _, dishRepo, recipeRepo := defaultMocks() handler := newHandler(mockRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodGet, "/diary?date=2026-03-15", nil)) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } var entries []diary.Entry if decodeError := json.NewDecoder(recorder.Body).Decode(&entries); decodeError != nil { t.Fatalf("decode response: %v", decodeError) } if len(entries) != 1 { t.Errorf("expected 1 entry, got %d", len(entries)) } } func TestCreate_MissingDate(t *testing.T) { diaryRepo, dishRepo, recipeRepo := defaultMocks() handler := newHandler(diaryRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(map[string]string{"name": "Oatmeal", "meal_type": "breakfast"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/diary", body)) if recorder.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", recorder.Code) } } func TestCreate_MissingNameAndDishID(t *testing.T) { diaryRepo, dishRepo, recipeRepo := defaultMocks() handler := newHandler(diaryRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(map[string]string{"date": "2026-03-15", "meal_type": "breakfast"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/diary", body)) if recorder.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", recorder.Code) } } func TestCreate_MissingMealType(t *testing.T) { diaryRepo, dishRepo, recipeRepo := defaultMocks() handler := newHandler(diaryRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(map[string]string{"date": "2026-03-15", "name": "Oatmeal"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/diary", body)) if recorder.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", recorder.Code) } } func TestCreate_Success(t *testing.T) { mockDiaryRepo := &diarymocks.MockDiaryRepository{ CreateFn: func(ctx context.Context, userID string, req diary.CreateRequest) (*diary.Entry, error) { return &diary.Entry{ ID: "entry-1", Date: req.Date, MealType: req.MealType, Name: "Oatmeal", Portions: 1, Source: "manual", DishID: strPtr("dish-1"), CreatedAt: time.Now(), }, nil }, } mockDishRepo := &diarymocks.MockDishRepository{ FindOrCreateFn: func(ctx context.Context, name string) (string, bool, error) { return "dish-1", false, nil }, } mockRecipeRepo := &diarymocks.MockRecipeRepository{ FindOrCreateRecipeFn: func(ctx context.Context, dishID string, calories, proteinG, fatG, carbsG float64) (string, bool, error) { return "recipe-1", false, nil }, } handler := newHandler(mockDiaryRepo, mockDishRepo, mockRecipeRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(diary.CreateRequest{ Date: "2026-03-15", MealType: "breakfast", Name: "Oatmeal", Portions: 1, }) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/diary", body)) if recorder.Code != http.StatusCreated { t.Errorf("expected 201, got %d", recorder.Code) } } func TestDelete_NotFound(t *testing.T) { mockRepo := &diarymocks.MockDiaryRepository{ DeleteFn: func(ctx context.Context, id, userID string) error { return diary.ErrNotFound }, } _, dishRepo, recipeRepo := defaultMocks() handler := newHandler(mockRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodDelete, "/diary/nonexistent-id", nil)) if recorder.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", recorder.Code) } } func TestDelete_Success(t *testing.T) { mockRepo := &diarymocks.MockDiaryRepository{ DeleteFn: func(ctx context.Context, id, userID string) error { return nil }, } _, dishRepo, recipeRepo := defaultMocks() handler := newHandler(mockRepo, dishRepo, recipeRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodDelete, "/diary/entry-1", nil)) if recorder.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", recorder.Code) } } func strPtr(s string) *string { return &s }