package expiry import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "wx_service/internal/middleware" "wx_service/internal/model" ) type apiResponse struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } func newTestRouter(t *testing.T) *gin.Engine { t.Helper() gin.SetMode(gin.TestMode) db := newTestDB(t) repo := NewRepository(db) service := NewService(repo) handler := NewHandler(service) r := gin.New() r.Use(func(c *gin.Context) { c.Set(middleware.ContextCurrentUserKey, &model.User{ID: 1, MiniProgramID: 1}) c.Next() }) api := r.Group("/api/expiry") { api.GET("/summary", handler.GetSummary) api.GET("/items", handler.GetItems) api.POST("/items", handler.CreateItem) api.PUT("/items/:id", handler.UpdateItem) api.DELETE("/items/:id", handler.DeleteItem) api.POST("/items/:id/status", handler.UpdateStatus) api.GET("/settings", handler.GetSettings) api.POST("/settings", handler.UpdateSettings) } return r } func requestJSON(t *testing.T, r http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder { t.Helper() var payload []byte if body != nil { var err error payload, err = json.Marshal(body) if err != nil { t.Fatalf("marshal body: %v", err) } } req := httptest.NewRequest(method, path, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func decodeResponse(t *testing.T, w *httptest.ResponseRecorder) apiResponse { t.Helper() var resp apiResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decode response: %v, body=%s", err, w.Body.String()) } return resp } func TestHandler_ItemsFlow(t *testing.T) { r := newTestRouter(t) createResp := requestJSON(t, r, http.MethodPost, "/api/expiry/items", map[string]interface{}{ "name": "酸奶", "category": "food", "expiry_date": "2030-06-01", "quantity": 2, "location": "冰箱", }) if createResp.Code != http.StatusOK { t.Fatalf("create status code = %d, body=%s", createResp.Code, createResp.Body.String()) } createBody := decodeResponse(t, createResp) if createBody.Code != 0 { t.Fatalf("create business code = %d", createBody.Code) } var created struct { ID uint `json:"id"` } if err := json.Unmarshal(createBody.Data, &created); err != nil { t.Fatalf("decode create data: %v", err) } if created.ID == 0 { t.Fatalf("expected created id > 0") } updateResp := requestJSON(t, r, http.MethodPut, "/api/expiry/items/1", map[string]interface{}{ "name": "酸奶(更新)", "category": "food", "expiry_date": "2030-06-02", "quantity": 1, }) if updateResp.Code != http.StatusOK { t.Fatalf("update status code = %d, body=%s", updateResp.Code, updateResp.Body.String()) } notFoundUpdateResp := requestJSON(t, r, http.MethodPut, "/api/expiry/items/999", map[string]interface{}{ "name": "不存在", "category": "food", "expiry_date": "2030-06-02", "quantity": 1, }) if notFoundUpdateResp.Code != http.StatusNotFound { t.Fatalf("update not found code = %d, body=%s", notFoundUpdateResp.Code, notFoundUpdateResp.Body.String()) } listResp := requestJSON(t, r, http.MethodGet, "/api/expiry/items?status=all&page=1&page_size=20", nil) if listResp.Code != http.StatusOK { t.Fatalf("list status code = %d, body=%s", listResp.Code, listResp.Body.String()) } listBody := decodeResponse(t, listResp) if listBody.Code != 0 { t.Fatalf("list business code = %d", listBody.Code) } statusResp := requestJSON(t, r, http.MethodPost, "/api/expiry/items/1/status", map[string]string{ "status": "used", }) if statusResp.Code != http.StatusOK { t.Fatalf("status update code = %d, body=%s", statusResp.Code, statusResp.Body.String()) } delResp := requestJSON(t, r, http.MethodDelete, "/api/expiry/items/1", nil) if delResp.Code != http.StatusOK { t.Fatalf("delete code = %d, body=%s", delResp.Code, delResp.Body.String()) } } func TestHandler_SettingsAndSummary(t *testing.T) { r := newTestRouter(t) settingsResp := requestJSON(t, r, http.MethodGet, "/api/expiry/settings", nil) if settingsResp.Code != http.StatusOK { t.Fatalf("get settings code=%d body=%s", settingsResp.Code, settingsResp.Body.String()) } settingsBody := decodeResponse(t, settingsResp) if settingsBody.Code != 0 { t.Fatalf("get settings business code=%d", settingsBody.Code) } updateSettingsResp := requestJSON(t, r, http.MethodPost, "/api/expiry/settings", map[string]interface{}{ "remind_days": []int{10, 5, 1}, }) if updateSettingsResp.Code != http.StatusOK { t.Fatalf("update settings code=%d body=%s", updateSettingsResp.Code, updateSettingsResp.Body.String()) } updateSettingsBody := decodeResponse(t, updateSettingsResp) if updateSettingsBody.Code != 0 { t.Fatalf("update settings business code=%d", updateSettingsBody.Code) } summaryResp := requestJSON(t, r, http.MethodGet, "/api/expiry/summary", nil) if summaryResp.Code != http.StatusOK { t.Fatalf("summary code=%d body=%s", summaryResp.Code, summaryResp.Body.String()) } summaryBody := decodeResponse(t, summaryResp) if summaryBody.Code != 0 { t.Fatalf("summary business code=%d", summaryBody.Code) } } func TestHandler_BadRequest(t *testing.T) { r := newTestRouter(t) badCreate := requestJSON(t, r, http.MethodPost, "/api/expiry/items", map[string]interface{}{ "name": "", "category": "food", }) if badCreate.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d body=%s", badCreate.Code, badCreate.Body.String()) } badSettings := requestJSON(t, r, http.MethodPost, "/api/expiry/settings", map[string]interface{}{ "remind_days": []int{0}, }) if badSettings.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d body=%s", badSettings.Code, badSettings.Body.String()) } }