修改页面
CI / Server (Go) (push) Has been cancelled
CI / Web (Next.js) (push) Has been cancelled
CI / Mobile (React Native) (push) Has been cancelled
CI / Admin (React) (push) Has been cancelled
CI / Docker Build (push) Has been cancelled

This commit is contained in:
zeronline
2026-06-25 16:33:32 +08:00
parent c5cccd346b
commit 545329ac40
63 changed files with 2560 additions and 161 deletions
@@ -0,0 +1,122 @@
package services
import (
"testing"
"time"
"github.com/inchstep/server/internal/models"
"github.com/stretchr/testify/suite"
)
type CheckInServiceTestSuite struct {
ServiceTestSuite
}
func TestCheckInServiceSuite(t *testing.T) {
suite.Run(t, new(CheckInServiceTestSuite))
}
func (s *CheckInServiceTestSuite) setupUser() string {
user, err := s.svc.RegisterByPhone("13800138100")
s.Require().NoError(err)
return user.ID
}
func (s *CheckInServiceTestSuite) TestCreateItem() {
userID := s.setupUser()
item := &models.CheckInItem{
Name: "每日阅读",
Category: models.CategoryStudy,
CycleType: models.CycleDaily,
Privacy: models.PrivacyFriendsOnly,
DailyGoal: strPtr("读30页"),
}
err := s.svcCk.CreateItem(userID, item)
s.Require().NoError(err)
s.NotEmpty(item.ID)
s.Equal(models.StatusActive, item.Status)
}
func (s *CheckInServiceTestSuite) TestGetUserItems() {
userID := s.setupUser()
item1 := &models.CheckInItem{Name: "跑步", Category: models.CategoryHealth, CycleType: models.CycleDaily, Privacy: models.PrivacyFriendsOnly}
item2 := &models.CheckInItem{Name: "冥想", Category: models.CategoryHabit, CycleType: models.CycleDaily, Privacy: models.PrivacyFriendsOnly}
s.Require().NoError(s.svcCk.CreateItem(userID, item1))
s.Require().NoError(s.svcCk.CreateItem(userID, item2))
items, err := s.svcCk.GetUserItems(userID)
s.Require().NoError(err)
s.Len(items, 2)
}
func (s *CheckInServiceTestSuite) TestGetItemByID() {
userID := s.setupUser()
item := &models.CheckInItem{Name: "写作", Category: models.CategoryHobby, CycleType: models.CycleDaily, Privacy: models.PrivacyPublic}
s.Require().NoError(s.svcCk.CreateItem(userID, item))
found, err := s.svcCk.GetItemByID(item.ID, userID)
s.Require().NoError(err)
s.Equal("写作", found.Name)
}
func (s *CheckInServiceTestSuite) TestGetItemByID_WrongUser() {
userID1 := s.setupUser()
userID2, _ := s.svc.RegisterByPhone("13800138101")
s.Require().NotNil(userID2)
item := &models.CheckInItem{Name: "日记", Category: models.CategoryHabit, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
s.Require().NoError(s.svcCk.CreateItem(userID1, item))
_, err := s.svcCk.GetItemByID(item.ID, userID2.ID)
s.Error(err) // should fail - wrong user
}
func (s *CheckInServiceTestSuite) TestPauseAndResumeItem() {
userID := s.setupUser()
item := &models.CheckInItem{Name: "瑜伽", Category: models.CategoryHealth, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
s.Require().NoError(s.svcCk.CreateItem(userID, item))
err := s.svcCk.PauseItem(item.ID, userID)
s.Require().NoError(err)
paused, err := s.svcCk.GetItemByID(item.ID, userID)
s.Require().NoError(err)
s.Equal(models.StatusPaused, paused.Status)
err = s.svcCk.ResumeItem(item.ID, userID)
s.Require().NoError(err)
resumed, err := s.svcCk.GetItemByID(item.ID, userID)
s.Require().NoError(err)
s.Equal(models.StatusActive, resumed.Status)
}
func (s *CheckInServiceTestSuite) TestCreateRecord() {
userID := s.setupUser()
item := &models.CheckInItem{Name: "背单词", Category: models.CategoryStudy, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
s.Require().NoError(s.svcCk.CreateItem(userID, item))
record, err := s.svcCk.CreateRecord(userID, item.ID, time.Now(), strPtr("背了50个"), nil)
s.Require().NoError(err)
s.NotEmpty(record.ID)
s.Equal(models.RecordNormal, record.Status)
}
func (s *CheckInServiceTestSuite) TestCreateRecord_DuplicateDate() {
userID := s.setupUser()
item := &models.CheckInItem{Name: "喝水", Category: models.CategoryHealth, CycleType: models.CycleDaily, Privacy: models.PrivacyPrivate}
s.Require().NoError(s.svcCk.CreateItem(userID, item))
_, err := s.svcCk.CreateRecord(userID, item.ID, time.Now(), nil, nil)
s.Require().NoError(err)
_, err = s.svcCk.CreateRecord(userID, item.ID, time.Now(), nil, nil)
s.Error(err)
s.Contains(err.Error(), "已打卡")
}
@@ -0,0 +1,36 @@
package services
import (
"github.com/glebarez/sqlite"
"github.com/inchstep/server/internal/models"
"github.com/stretchr/testify/suite"
"gorm.io/gorm"
)
func strPtr(s string) *string { return &s }
// ServiceTestSuite 提供内存 SQLite 数据库用于服务层测试
type ServiceTestSuite struct {
suite.Suite
db *gorm.DB
svc *UserService
svcCk *CheckInService
svcPts *PointsService
}
func (s *ServiceTestSuite) SetupSuite() {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
s.Require().NoError(err)
s.Require().NoError(db.AutoMigrate(&models.User{}, &models.CheckInItem{}, &models.CheckInRecord{}, &models.Achievement{}, &models.Admin{}))
s.db = db
s.svc = NewUserService(db)
s.svcCk = NewCheckInService(db)
s.svcPts = NewPointsService(db)
}
func (s *ServiceTestSuite) TearDownTest() {
s.db.Exec("DELETE FROM users")
s.db.Exec("DELETE FROM check_in_items")
s.db.Exec("DELETE FROM check_in_records")
s.db.Exec("DELETE FROM achievements")
}
+40
View File
@@ -85,6 +85,46 @@ func (s *UserService) DeleteAccount(id string) error {
}).Error
}
// LeaderboardEntry 排行榜条目
type LeaderboardEntry struct {
UserID string `json:"userId"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar,omitempty"`
Value int `json:"value"`
}
// GetLeaderboardByPoints 按积分排行
func (s *UserService) GetLeaderboardByPoints(limit int) ([]LeaderboardEntry, error) {
var users []models.User
if err := s.db.Where("points > 0").Order("points desc").Limit(limit).Find(&users).Error; err != nil {
return nil, err
}
entries := make([]LeaderboardEntry, 0, len(users))
for _, u := range users {
nick := ""
if u.Nickname != nil {
nick = *u.Nickname
}
entries = append(entries, LeaderboardEntry{
UserID: u.ID,
Nickname: nick,
Value: u.Points,
})
}
return entries, nil
}
// SearchUsers 搜索用户(按昵称或手机号)
func (s *UserService) SearchUsers(query string, limit int) ([]models.User, error) {
var users []models.User
q := "%" + query + "%"
if err := s.db.Where("nickname LIKE ? OR phone LIKE ?", q, q).
Limit(limit).Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
return string(bytes), err
@@ -0,0 +1,102 @@
package services
import (
"testing"
"github.com/stretchr/testify/suite"
)
type UserServiceTestSuite struct {
ServiceTestSuite
}
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceTestSuite))
}
func (s *UserServiceTestSuite) TestRegisterByPhone() {
user, err := s.svc.RegisterByPhone("13800138000")
s.Require().NoError(err)
s.NotEmpty(user.ID)
s.Equal("13800138000", *user.Phone)
s.Equal(0, user.Points)
}
func (s *UserServiceTestSuite) TestRegisterByPhone_Duplicate() {
_, err := s.svc.RegisterByPhone("13800138001")
s.Require().NoError(err)
_, err = s.svc.RegisterByPhone("13800138001")
s.Error(err)
s.Contains(err.Error(), "已注册")
}
func (s *UserServiceTestSuite) TestGetByPhone() {
created, err := s.svc.RegisterByPhone("13800138002")
s.Require().NoError(err)
found, err := s.svc.GetByPhone("13800138002")
s.Require().NoError(err)
s.Equal(created.ID, found.ID)
}
func (s *UserServiceTestSuite) TestGetByPhone_NotFound() {
_, err := s.svc.GetByPhone("13800999999")
s.Error(err)
}
func (s *UserServiceTestSuite) TestGetByID() {
created, err := s.svc.RegisterByPhone("13800138003")
s.Require().NoError(err)
found, err := s.svc.GetByID(created.ID)
s.Require().NoError(err)
s.Equal(*created.Phone, *found.Phone)
}
func (s *UserServiceTestSuite) TestUpdateProfile() {
user, err := s.svc.RegisterByPhone("13800138004")
s.Require().NoError(err)
updated, err := s.svc.UpdateProfile(user.ID, map[string]interface{}{
"nickname": "TestUser",
"bio": "Hello",
})
s.Require().NoError(err)
s.Equal("TestUser", *updated.Nickname)
s.Equal("Hello", *updated.Bio)
}
func (s *UserServiceTestSuite) TestUpdateProfile_RejectInvalidFields() {
user, err := s.svc.RegisterByPhone("13800138005")
s.Require().NoError(err)
updated, err := s.svc.UpdateProfile(user.ID, map[string]interface{}{
"points": 9999, // points is not in the allowed list
})
s.Require().NoError(err)
s.Equal(0, updated.Points) // should not have changed
}
func (s *UserServiceTestSuite) TestDeleteAccount() {
user, err := s.svc.RegisterByPhone("13800138006")
s.Require().NoError(err)
err = s.svc.DeleteAccount(user.ID)
s.Require().NoError(err)
// After delete, the user still exists but fields are cleared
deleted, err := s.svc.GetByID(user.ID)
s.Require().NoError(err)
s.Nil(deleted.Phone)
s.Nil(deleted.Nickname)
}
func (s *UserServiceTestSuite) TestHashAndCheckPassword() {
hash, err := HashPassword("my-secret-pwd")
s.Require().NoError(err)
s.NotEmpty(hash)
s.True(CheckPassword(hash, "my-secret-pwd"))
s.False(CheckPassword(hash, "wrong-password"))
}