test(expiry): 完成 #31 后端单元测试与覆盖率提升
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user