package product_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/food-ai/backend/internal/domain/product" productmocks "github.com/food-ai/backend/internal/domain/product/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 *product.Handler, userID string) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Auth(&alwaysAuthValidator{userID: userID})) router.Get("/products", handler.List) router.Post("/products", handler.Create) router.Post("/products/batch", handler.BatchCreate) router.Put("/products/{id}", handler.Update) router.Delete("/products/{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 makeProduct(name string) *product.Product { return &product.Product{ ID: "prod-1", UserID: "user-1", Name: name, Quantity: 1, Unit: "pcs", StorageDays: 7, AddedAt: time.Now(), ExpiresAt: time.Now().AddDate(0, 0, 7), DaysLeft: 7, } } func TestList_Success(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ ListFn: func(ctx context.Context, userID string) ([]*product.Product, error) { return []*product.Product{makeProduct("Milk")}, nil }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodGet, "/products", nil)) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } } func TestCreate_MissingName(t *testing.T) { handler := product.NewHandler(&productmocks.MockProductRepository{}) router := buildRouter(handler, "user-1") body, _ := json.Marshal(map[string]any{"quantity": 1}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/products", body)) if recorder.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", recorder.Code) } } func TestCreate_Success(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ CreateFn: func(ctx context.Context, userID string, req product.CreateRequest) (*product.Product, error) { return makeProduct(req.Name), nil }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal(product.CreateRequest{Name: "Milk", Quantity: 1, Unit: "L"}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/products", body)) if recorder.Code != http.StatusCreated { t.Errorf("expected 201, got %d", recorder.Code) } } func TestBatchCreate_Success(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ BatchCreateFn: func(ctx context.Context, userID string, items []product.CreateRequest) ([]*product.Product, error) { result := make([]*product.Product, len(items)) for index, item := range items { result[index] = makeProduct(item.Name) } return result, nil }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") body, _ := json.Marshal([]product.CreateRequest{ {Name: "Milk", Quantity: 1, Unit: "L"}, {Name: "Eggs", Quantity: 12, Unit: "pcs"}, }) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPost, "/products/batch", body)) if recorder.Code != http.StatusCreated { t.Errorf("expected 201, got %d", recorder.Code) } } func TestUpdate_NotFound(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ UpdateFn: func(ctx context.Context, id, userID string, req product.UpdateRequest) (*product.Product, error) { return nil, product.ErrNotFound }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") namePtr := "NewName" body, _ := json.Marshal(product.UpdateRequest{Name: &namePtr}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPut, "/products/nonexistent", body)) if recorder.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", recorder.Code) } } func TestUpdate_Success(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ UpdateFn: func(ctx context.Context, id, userID string, req product.UpdateRequest) (*product.Product, error) { updated := makeProduct(*req.Name) return updated, nil }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") namePtr := "Oat Milk" body, _ := json.Marshal(product.UpdateRequest{Name: &namePtr}) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodPut, "/products/prod-1", body)) if recorder.Code != http.StatusOK { t.Errorf("expected 200, got %d", recorder.Code) } } func TestDelete_NotFound(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ DeleteFn: func(ctx context.Context, id, userID string) error { return product.ErrNotFound }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodDelete, "/products/nonexistent", nil)) if recorder.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", recorder.Code) } } func TestDelete_Success(t *testing.T) { mockRepo := &productmocks.MockProductRepository{ DeleteFn: func(ctx context.Context, id, userID string) error { return nil }, } handler := product.NewHandler(mockRepo) router := buildRouter(handler, "user-1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, authorizedRequest(http.MethodDelete, "/products/prod-1", nil)) if recorder.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", recorder.Code) } }