test(expiry): 完成 #31 后端单元测试与覆盖率提升

This commit is contained in:
root
2026-03-04 18:35:32 +08:00
parent e1b5382004
commit 6c303abd58
7 changed files with 909 additions and 10 deletions
+69
View File
@@ -0,0 +1,69 @@
package expiry
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestHandler_HealthzAndBadPathBranches(t *testing.T) {
r := newTestRouter(t)
// 覆盖 healthz。
{
db := newTestDB(t)
handler := NewHandler(NewService(NewRepository(db)))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
handler.Healthz(c)
if w.Code != http.StatusOK {
t.Fatalf("healthz status=%d", w.Code)
}
}
// 覆盖错误分支。
badIDDelete := requestJSON(t, r, http.MethodDelete, "/api/expiry/items/abc", nil)
if badIDDelete.Code != http.StatusBadRequest {
t.Fatalf("expected bad id 400, got %d", badIDDelete.Code)
}
badDateCreate := requestJSON(t, r, http.MethodPost, "/api/expiry/items", map[string]interface{}{
"name": "错误日期",
"category": "food",
"production_date": "2026/01/01",
"expiry_date": "2030-01-01",
})
if badDateCreate.Code != http.StatusBadRequest {
t.Fatalf("expected bad date 400, got %d", badDateCreate.Code)
}
badStatus := requestJSON(t, r, http.MethodPost, "/api/expiry/items/1/status", map[string]interface{}{
"status": "invalid",
})
if badStatus.Code != http.StatusBadRequest && badStatus.Code != http.StatusNotFound {
t.Fatalf("expected bad status 400/404, got %d", badStatus.Code)
}
badList := requestJSON(t, r, http.MethodGet, "/api/expiry/items?status=bad", nil)
if badList.Code != http.StatusBadRequest {
t.Fatalf("expected bad filter 400, got %d", badList.Code)
}
}
func TestHandler_HelperWriters(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
writeExpiryServerError(c)
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d", w.Code)
}
w2 := httptest.NewRecorder()
c2, _ := gin.CreateTestContext(w2)
writeExpirySuccess(c2, "ok", gin.H{"x": 1})
if w2.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w2.Code)
}
}
+205
View File
@@ -0,0 +1,205 @@
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())
}
}
+10 -10
View File
@@ -151,7 +151,9 @@ func (r *Repository) GetSummary(userID uint) (map[string]int, error) {
now := dateOnly(time.Now()) now := dateOnly(time.Now())
sevenDaysLater := now.AddDate(0, 0, 7) sevenDaysLater := now.AddDate(0, 0, 7)
base := r.db.Model(&ExpiryItem{}).Where("user_id = ?", userID) base := func() *gorm.DB {
return r.db.Model(&ExpiryItem{}).Where("user_id = ?", userID)
}
count := func(tx *gorm.DB) (int64, error) { count := func(tx *gorm.DB) (int64, error) {
var v int64 var v int64
@@ -161,35 +163,35 @@ func (r *Repository) GetSummary(userID uint) (map[string]int, error) {
return v, nil return v, nil
} }
totalItems, err := count(base.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded})) totalItems, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}))
if err != nil { if err != nil {
return nil, fmt.Errorf("count total items: %w", err) return nil, fmt.Errorf("count total items: %w", err)
} }
expiringSoon, err := count(base.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). expiringSoon, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}).
Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater)) Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater))
if err != nil { if err != nil {
return nil, fmt.Errorf("count expiring items: %w", err) return nil, fmt.Errorf("count expiring items: %w", err)
} }
expired, err := count(base.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). expired, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}).
Where("expiry_date < ?", now)) Where("expiry_date < ?", now))
if err != nil { if err != nil {
return nil, fmt.Errorf("count expired items: %w", err) return nil, fmt.Errorf("count expired items: %w", err)
} }
normal, err := count(base.Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}). normal, err := count(base().Where("status NOT IN ?", []string{StatusUsed, StatusDiscarded}).
Where("expiry_date > ?", sevenDaysLater)) Where("expiry_date > ?", sevenDaysLater))
if err != nil { if err != nil {
return nil, fmt.Errorf("count normal items: %w", err) return nil, fmt.Errorf("count normal items: %w", err)
} }
used, err := count(base.Where("status = ?", StatusUsed)) used, err := count(base().Where("status = ?", StatusUsed))
if err != nil { if err != nil {
return nil, fmt.Errorf("count used items: %w", err) return nil, fmt.Errorf("count used items: %w", err)
} }
discarded, err := count(base.Where("status = ?", StatusDiscarded)) discarded, err := count(base().Where("status = ?", StatusDiscarded))
if err != nil { if err != nil {
return nil, fmt.Errorf("count discarded items: %w", err) return nil, fmt.Errorf("count discarded items: %w", err)
} }
@@ -250,9 +252,7 @@ func (r *Repository) UpdateSettings(userID uint, remindDays []int) (*ExpiryUserS
} }
settings.RemindDays = copyIntSlice(remindDays) settings.RemindDays = copyIntSlice(remindDays)
if err := r.db.Model(&ExpiryUserSettings{}). if err := r.db.Save(settings).Error; err != nil {
Where("user_id = ?", userID).
Update("remind_days", settings.RemindDays).Error; err != nil {
return nil, fmt.Errorf("update expiry settings: %w", err) return nil, fmt.Errorf("update expiry settings: %w", err)
} }
return settings, nil return settings, nil
+302
View File
@@ -0,0 +1,302 @@
package expiry
import (
"testing"
"time"
)
func TestRepository_CreateFindUpdateDelete(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
expiryDate := mustDate(t, "2030-01-10")
shelf := 30
item := &ExpiryItem{
UserID: 100,
MiniProgramID: 1,
Name: "牛奶",
Category: CategoryFood,
ExpiryDate: expiryDate,
ShelfLifeDays: &shelf,
Quantity: 2,
Status: StatusNormal,
}
if err := repo.Create(item); err != nil {
t.Fatalf("create item: %v", err)
}
if item.ID == 0 {
t.Fatalf("expected item id > 0")
}
found, err := repo.FindByID(item.ID, item.UserID)
if err != nil {
t.Fatalf("find by id: %v", err)
}
if found.Name != "牛奶" {
t.Fatalf("unexpected name: %s", found.Name)
}
found.Name = "牛奶(更新)"
if err := repo.Update(found); err != nil {
t.Fatalf("update item: %v", err)
}
updated, err := repo.FindByID(item.ID, item.UserID)
if err != nil {
t.Fatalf("find updated item: %v", err)
}
if updated.Name != "牛奶(更新)" {
t.Fatalf("unexpected updated name: %s", updated.Name)
}
if err := repo.UpdateStatus(item.ID, item.UserID, StatusUsed); err != nil {
t.Fatalf("update status: %v", err)
}
afterStatus, err := repo.FindByID(item.ID, item.UserID)
if err != nil {
t.Fatalf("find after status: %v", err)
}
if afterStatus.Status != StatusUsed {
t.Fatalf("expected status used, got %s", afterStatus.Status)
}
if err := repo.Delete(item.ID, item.UserID); err != nil {
t.Fatalf("delete item: %v", err)
}
_, err = repo.FindByID(item.ID, item.UserID)
if err != ErrExpiryItemNotFound {
t.Fatalf("expected ErrExpiryItemNotFound after delete, got %v", err)
}
}
func TestRepository_FindByUserAndSummary(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
now := dateOnly(mustDate(t, "2030-01-01"))
items := []ExpiryItem{
{
UserID: 200,
MiniProgramID: 1,
Name: "过期食品",
Category: CategoryFood,
ExpiryDate: now.AddDate(0, 0, -1),
Quantity: 1,
Status: StatusNormal,
},
{
UserID: 200,
MiniProgramID: 1,
Name: "临期药品",
Category: CategoryMedicine,
ExpiryDate: now.AddDate(0, 0, 3),
Quantity: 1,
Status: StatusNormal,
},
{
UserID: 200,
MiniProgramID: 1,
Name: "正常化妆品",
Category: CategoryCosmetic,
ExpiryDate: now.AddDate(0, 0, 30),
Quantity: 1,
Status: StatusNormal,
},
{
UserID: 200,
MiniProgramID: 1,
Name: "已使用",
Category: CategoryOther,
ExpiryDate: now.AddDate(0, 0, 30),
Quantity: 1,
Status: StatusUsed,
},
{
UserID: 200,
MiniProgramID: 1,
Name: "已丢弃",
Category: CategoryOther,
ExpiryDate: now.AddDate(0, 0, 30),
Quantity: 1,
Status: StatusDiscarded,
},
}
for idx := range items {
if err := repo.Create(&items[idx]); err != nil {
t.Fatalf("create item %d: %v", idx, err)
}
}
list, total, err := repo.FindByUser(200, map[string]interface{}{
"status": "all",
"category": "all",
"sort": "expiry_date",
}, 1, 20)
if err != nil {
t.Fatalf("find by user all: %v", err)
}
if total != 5 || len(list) != 5 {
t.Fatalf("expected 5 items total/list, got total=%d len=%d", total, len(list))
}
usedList, usedTotal, err := repo.FindByUser(200, map[string]interface{}{
"status": StatusUsed,
"category": "all",
"sort": "expiry_date",
}, 1, 20)
if err != nil {
t.Fatalf("find used list: %v", err)
}
if usedTotal != 1 || len(usedList) != 1 {
t.Fatalf("expected used list 1, got total=%d len=%d", usedTotal, len(usedList))
}
summary, err := repo.GetSummary(200)
if err != nil {
t.Fatalf("get summary: %v", err)
}
if summary["total_items"] != 3 {
t.Fatalf("expected total_items=3, got %d", summary["total_items"])
}
if summary["used"] != 1 || summary["discarded"] != 1 {
t.Fatalf("unexpected used/discarded: %+v", summary)
}
}
func TestRepository_SettingsCRUD(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
settings, err := repo.GetSettings(300)
if err != nil {
t.Fatalf("get empty settings: %v", err)
}
if settings != nil {
t.Fatalf("expected nil settings for new user")
}
saved, err := repo.UpdateSettings(300, []int{7, 3, 1})
if err != nil {
t.Fatalf("create settings: %v", err)
}
if len(saved.RemindDays) != 3 {
t.Fatalf("expected 3 remind days, got %d", len(saved.RemindDays))
}
updated, err := repo.UpdateSettings(300, []int{10, 5, 2})
if err != nil {
t.Fatalf("update settings: %v", err)
}
if len(updated.RemindDays) != 3 || updated.RemindDays[0] != 10 {
t.Fatalf("unexpected remind days: %+v", updated.RemindDays)
}
loaded, err := repo.GetSettings(300)
if err != nil {
t.Fatalf("reload settings: %v", err)
}
if len(loaded.RemindDays) != 3 || loaded.RemindDays[0] != 10 {
t.Fatalf("unexpected loaded remind days: %+v", loaded.RemindDays)
}
}
func TestRepository_ErrorAndFilterBranches(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
if err := repo.Delete(999, 1); err != ErrExpiryItemNotFound {
t.Fatalf("expected delete not found, got %v", err)
}
if err := repo.UpdateStatus(999, 1, StatusUsed); err != ErrExpiryItemNotFound {
t.Fatalf("expected update status not found, got %v", err)
}
_, err := repo.FindByID(999, 1)
if err != ErrExpiryItemNotFound {
t.Fatalf("expected find not found, got %v", err)
}
now := dateOnly(time.Now())
sevenDaysLater := now.AddDate(0, 0, 7)
items := []ExpiryItem{
{
UserID: 400,
MiniProgramID: 1,
Name: "过期",
Category: CategoryFood,
ExpiryDate: now.AddDate(0, 0, -1),
Quantity: 1,
},
{
UserID: 400,
MiniProgramID: 1,
Name: "临期",
Category: CategoryFood,
ExpiryDate: now.AddDate(0, 0, 1),
Quantity: 1,
},
{
UserID: 400,
MiniProgramID: 1,
Name: "正常",
Category: CategoryMedicine,
ExpiryDate: sevenDaysLater.AddDate(0, 0, 3),
Quantity: 1,
},
}
for i := range items {
if err := repo.Create(&items[i]); err != nil {
t.Fatalf("seed item %d: %v", i, err)
}
}
expiredList, _, err := repo.FindByUser(400, map[string]interface{}{
"status": StatusExpired,
"sort": "created_at",
}, 1, 10)
if err != nil {
t.Fatalf("find expired: %v", err)
}
if len(expiredList) != 1 {
t.Fatalf("expected 1 expired, got %d", len(expiredList))
}
expiringList, _, err := repo.FindByUser(400, map[string]interface{}{
"status": StatusExpiring,
}, 1, 10)
if err != nil {
t.Fatalf("find expiring: %v", err)
}
if len(expiringList) != 1 {
t.Fatalf("expected 1 expiring, got %d", len(expiringList))
}
normalList, _, err := repo.FindByUser(400, map[string]interface{}{
"status": StatusNormal,
"category": CategoryMedicine,
}, 1, 10)
if err != nil {
t.Fatalf("find normal: %v", err)
}
if len(normalList) != 1 {
t.Fatalf("expected 1 normal, got %d", len(normalList))
}
summary, err := repo.GetSummary(400)
if err != nil {
t.Fatalf("get summary: %v", err)
}
if summary["expired"] != 1 || summary["expiring_soon"] != 1 || summary["normal"] != 1 {
t.Fatalf("unexpected summary: %+v", summary)
}
}
+130
View File
@@ -0,0 +1,130 @@
package expiry
import (
"testing"
)
func TestService_HelperBranches(t *testing.T) {
t.Run("normalize filters", func(t *testing.T) {
got := normalizeFilters(ItemFilters{})
if got.Page != 1 || got.PageSize != 20 || got.Status != "all" || got.Category != "all" || got.Sort != "expiry_date" {
t.Fatalf("unexpected normalized filters: %+v", got)
}
capped := normalizeFilters(ItemFilters{Page: 1, PageSize: 1000, Status: "all", Category: "all", Sort: "expiry_date"})
if capped.PageSize != 100 {
t.Fatalf("expected page size capped at 100, got %d", capped.PageSize)
}
})
t.Run("validate filters", func(t *testing.T) {
if err := validateFilters(ItemFilters{Status: "all", Category: "all", Sort: "expiry_date"}); err != nil {
t.Fatalf("unexpected valid filters error: %v", err)
}
if err := validateFilters(ItemFilters{Status: "x", Category: "all", Sort: "expiry_date"}); err != ErrExpiryFilterStatusInvalid {
t.Fatalf("expected status invalid, got %v", err)
}
if err := validateFilters(ItemFilters{Status: "all", Category: "x", Sort: "expiry_date"}); err != ErrExpiryFilterCategoryInvalid {
t.Fatalf("expected category invalid, got %v", err)
}
if err := validateFilters(ItemFilters{Status: "all", Category: "all", Sort: "x"}); err != ErrExpiryFilterSortInvalid {
t.Fatalf("expected sort invalid, got %v", err)
}
})
t.Run("validate base fields", func(t *testing.T) {
if err := validateBaseFields("名称", CategoryFood, 1, "位置", "备注"); err != nil {
t.Fatalf("expected valid fields, got %v", err)
}
if err := validateBaseFields("", CategoryFood, 1, "", ""); err != ErrExpiryNameInvalid {
t.Fatalf("expected name invalid, got %v", err)
}
if err := validateBaseFields("名称", "bad", 1, "", ""); err != ErrExpiryCategoryInvalid {
t.Fatalf("expected category invalid, got %v", err)
}
if err := validateBaseFields("名称", CategoryFood, -1, "", ""); err != ErrExpiryQuantityInvalid {
t.Fatalf("expected quantity invalid, got %v", err)
}
})
t.Run("apply expiry date", func(t *testing.T) {
prod := mustDate(t, "2031-01-01")
shelf := 5
expiry := mustDate(t, "2031-01-20")
item := &ExpiryItem{ProductionDate: &prod}
if err := applyExpiryDate(item, &expiry, &shelf); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if item.ExpiryDate.Format("2006-01-02") != "2031-01-06" {
t.Fatalf("expected computed expiry, got %s", item.ExpiryDate.Format("2006-01-02"))
}
item2 := &ExpiryItem{}
if err := applyExpiryDate(item2, &expiry, nil); err != nil {
t.Fatalf("expected no error on expiry only, got %v", err)
}
item3 := &ExpiryItem{}
if err := applyExpiryDate(item3, nil, nil); err != ErrExpiryDateRequired {
t.Fatalf("expected ErrExpiryDateRequired, got %v", err)
}
})
t.Run("category and remind days", func(t *testing.T) {
if !isValidCategory(CategoryCosmetic) || isValidCategory("invalid") {
t.Fatalf("isValidCategory result unexpected")
}
remindDays, err := validateRemindDays([]int{10, 10, 3, 1})
if err != nil {
t.Fatalf("validate remind days error: %v", err)
}
if len(remindDays) != 3 {
t.Fatalf("expected deduped len=3, got %d", len(remindDays))
}
if _, err := validateRemindDays([]int{}); err != ErrExpiryRemindDaysInvalid {
t.Fatalf("expected remind days invalid for empty, got %v", err)
}
})
}
func TestService_UpdateItemPreserveStatus(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
service := NewService(repo)
expiryDate := mustDate(t, "2031-02-01")
created, err := service.CreateItem(88, CreateItemRequest{
MiniProgramID: 1,
Name: "牙膏",
Category: CategoryOther,
ExpiryDate: &expiryDate,
Quantity: 1,
})
if err != nil {
t.Fatalf("create item: %v", err)
}
if err := service.UpdateItemStatus(created.ID, 88, StatusDiscarded); err != nil {
t.Fatalf("update status discarded: %v", err)
}
newExpiry := mustDate(t, "2031-03-01")
updated, err := service.UpdateItem(created.ID, 88, UpdateItemRequest{
MiniProgramID: 1,
Name: "牙膏(更新)",
Category: CategoryOther,
ExpiryDate: &newExpiry,
Quantity: 1,
})
if err != nil {
t.Fatalf("update discarded item: %v", err)
}
if updated.Status != StatusDiscarded {
t.Fatalf("expected discarded status preserved, got %s", updated.Status)
}
}
+160
View File
@@ -0,0 +1,160 @@
package expiry
import "testing"
func TestService_CreateItemAndGetItems(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
service := NewService(repo)
productionDate := mustDate(t, "2030-01-01")
shelfLife := 10
created, err := service.CreateItem(1, CreateItemRequest{
MiniProgramID: 1,
Name: "测试牛奶",
Category: CategoryFood,
ProductionDate: &productionDate,
ShelfLifeDays: &shelfLife,
Quantity: 2,
Location: "冰箱",
Remark: "服务层测试",
})
if err != nil {
t.Fatalf("create item: %v", err)
}
if created.ExpiryDate.Format("2006-01-02") != "2030-01-11" {
t.Fatalf("unexpected expiry date: %s", created.ExpiryDate.Format("2006-01-02"))
}
list, err := service.GetItems(1, ItemFilters{
Status: "all",
Category: "all",
Sort: "expiry_date",
Page: 1,
PageSize: 20,
})
if err != nil {
t.Fatalf("get items: %v", err)
}
if list.Total != 1 || len(list.Items) != 1 {
t.Fatalf("expected 1 item, got total=%d len=%d", list.Total, len(list.Items))
}
if list.Items[0].DaysLeft <= 0 {
t.Fatalf("expected days_left > 0, got %d", list.Items[0].DaysLeft)
}
}
func TestService_UpdateDeleteAndStatus(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
service := NewService(repo)
expiryDate := mustDate(t, "2030-02-01")
created, err := service.CreateItem(2, CreateItemRequest{
MiniProgramID: 1,
Name: "测试药品",
Category: CategoryMedicine,
ExpiryDate: &expiryDate,
Quantity: 1,
})
if err != nil {
t.Fatalf("create item: %v", err)
}
newExpiry := mustDate(t, "2030-03-01")
updated, err := service.UpdateItem(created.ID, 2, UpdateItemRequest{
MiniProgramID: 1,
Name: "测试药品(更新)",
Category: CategoryMedicine,
ExpiryDate: &newExpiry,
Quantity: 1,
})
if err != nil {
t.Fatalf("update item: %v", err)
}
if updated.Name != "测试药品(更新)" {
t.Fatalf("unexpected updated name: %s", updated.Name)
}
if err := service.UpdateItemStatus(created.ID, 2, StatusUsed); err != nil {
t.Fatalf("update item status: %v", err)
}
got, err := service.GetItem(created.ID, 2)
if err != nil {
t.Fatalf("get item: %v", err)
}
if got.Status != StatusUsed {
t.Fatalf("expected used status, got %s", got.Status)
}
if err := service.DeleteItem(created.ID, 2); err != nil {
t.Fatalf("delete item: %v", err)
}
_, err = service.GetItem(created.ID, 2)
if err != ErrExpiryItemNotFound {
t.Fatalf("expected not found, got %v", err)
}
}
func TestService_Validation(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
service := NewService(repo)
expiryDate := mustDate(t, "2030-02-01")
_, err := service.CreateItem(3, CreateItemRequest{
MiniProgramID: 1,
Name: "",
Category: CategoryFood,
ExpiryDate: &expiryDate,
Quantity: 1,
})
if err != ErrExpiryNameInvalid {
t.Fatalf("expected ErrExpiryNameInvalid, got %v", err)
}
_, err = service.GetItems(3, ItemFilters{Status: "invalid", Category: "all", Sort: "expiry_date", Page: 1, PageSize: 20})
if err != ErrExpiryFilterStatusInvalid {
t.Fatalf("expected ErrExpiryFilterStatusInvalid, got %v", err)
}
err = service.UpdateItemStatus(1, 3, "unknown")
if err != ErrExpiryStatusInvalid {
t.Fatalf("expected ErrExpiryStatusInvalid, got %v", err)
}
}
func TestService_Settings(t *testing.T) {
db := newTestDB(t)
repo := NewRepository(db)
service := NewService(repo)
defaults, err := service.GetSettings(10)
if err != nil {
t.Fatalf("get default settings: %v", err)
}
if len(defaults.RemindDays) != 3 {
t.Fatalf("expected default remind_days len=3, got %d", len(defaults.RemindDays))
}
_, err = service.UpdateSettings(10, []int{0, 31})
if err != ErrExpiryRemindDaysInvalid {
t.Fatalf("expected ErrExpiryRemindDaysInvalid, got %v", err)
}
updated, err := service.UpdateSettings(10, []int{10, 10, 3, 1})
if err != nil {
t.Fatalf("update settings: %v", err)
}
if len(updated.RemindDays) != 3 {
t.Fatalf("expected deduped remind_days len=3, got %d", len(updated.RemindDays))
}
}
+33
View File
@@ -0,0 +1,33 @@
package expiry
import (
"testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func newTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&ExpiryItem{}, &ExpiryUserSettings{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
return db
}
func mustDate(t *testing.T, value string) time.Time {
t.Helper()
parsed, err := time.ParseInLocation("2006-01-02", value, time.Local)
if err != nil {
t.Fatalf("parse date %s: %v", value, err)
}
return parsed
}