- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件 - 新增等级模型:model/level 及前端 levels 元数据 - 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置 - 积分、签到、评论等服务适配等级与徽章逻辑
160 lines
4.2 KiB
Go
160 lines
4.2 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/freefire/jiang13-bbs/model"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func newBadgeTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.User{}, &model.Badge{}, &model.UserBadge{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestBadgeGrantIdempotent(t *testing.T) {
|
|
db := newBadgeTestDB(t)
|
|
s := NewBadgeService(db)
|
|
|
|
user := model.User{Username: "alice", Password: "x"}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("seed user: %v", err)
|
|
}
|
|
badge, err := s.Create(" 骨灰玩家 ", "", "")
|
|
if err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if badge.Name != "骨灰玩家" || badge.Color != DefaultBadgeColor {
|
|
t.Fatalf("名称未规整/默认色不符: %+v", badge)
|
|
}
|
|
|
|
if err := s.Grant(badge.ID, user.ID, 9); err != nil {
|
|
t.Fatalf("Grant: %v", err)
|
|
}
|
|
// 重复授予应幂等成功且不产生新行
|
|
if err := s.Grant(badge.ID, user.ID, 9); err != nil {
|
|
t.Fatalf("Grant twice: %v", err)
|
|
}
|
|
var count int64
|
|
db.Model(&model.UserBadge{}).Where("badge_id = ?", badge.ID).Count(&count)
|
|
if count != 1 {
|
|
t.Fatalf("授予记录=%d want 1", count)
|
|
}
|
|
|
|
// 不存在的用户
|
|
if err := s.Grant(badge.ID, 999, 9); !errors.Is(err, ErrBadgeUserGone) {
|
|
t.Fatalf("不存在用户应 ErrBadgeUserGone, got %v", err)
|
|
}
|
|
// 不存在的徽章
|
|
if err := s.Grant(999, user.ID, 9); !errors.Is(err, ErrBadgeNotFound) {
|
|
t.Fatalf("不存在徽章应 ErrBadgeNotFound, got %v", err)
|
|
}
|
|
|
|
// ForUser 带 Badge 定义
|
|
rows, err := s.ForUser(user.ID)
|
|
if err != nil || len(rows) != 1 {
|
|
t.Fatalf("ForUser: %v rows=%d", err, len(rows))
|
|
}
|
|
if rows[0].Badge.Name != "骨灰玩家" {
|
|
t.Fatalf("Preload Badge 失败: %+v", rows[0])
|
|
}
|
|
|
|
// Revoke 后再撤销 → NotFound
|
|
if err := s.Revoke(badge.ID, user.ID); err != nil {
|
|
t.Fatalf("Revoke: %v", err)
|
|
}
|
|
if err := s.Revoke(badge.ID, user.ID); !errors.Is(err, ErrBadgeNotFound) {
|
|
t.Fatal("重复撤销应 ErrBadgeNotFound")
|
|
}
|
|
}
|
|
|
|
func TestBadgeDeleteCascadeAndListCount(t *testing.T) {
|
|
db := newBadgeTestDB(t)
|
|
s := NewBadgeService(db)
|
|
|
|
u1 := model.User{Username: "u1", Password: "x"}
|
|
u2 := model.User{Username: "u2", Password: "x"}
|
|
if err := db.Create(&u1).Error; err != nil {
|
|
t.Fatalf("seed u1: %v", err)
|
|
}
|
|
if err := db.Create(&u2).Error; err != nil {
|
|
t.Fatalf("seed u2: %v", err)
|
|
}
|
|
b1, _ := s.Create("元老", "medal", "gold")
|
|
b2, _ := s.Create("萌新", "", "")
|
|
|
|
if err := s.Grant(b1.ID, u1.ID, 9); err != nil {
|
|
t.Fatalf("Grant b1: %v", err)
|
|
}
|
|
if err := s.Grant(b1.ID, u2.ID, 9); err != nil {
|
|
t.Fatalf("Grant b1 u2: %v", err)
|
|
}
|
|
if err := s.Grant(b2.ID, u1.ID, 9); err != nil {
|
|
t.Fatalf("Grant b2: %v", err)
|
|
}
|
|
|
|
// List 计数
|
|
list, err := s.List()
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Fatalf("徽章数=%d want 2", len(list))
|
|
}
|
|
for _, b := range list {
|
|
want := int64(2)
|
|
if b.ID == b2.ID {
|
|
want = 1
|
|
}
|
|
if b.Holders != want {
|
|
t.Fatalf("徽章 %d 持有数=%d want %d", b.ID, b.Holders, want)
|
|
}
|
|
}
|
|
|
|
// 删除级联:徽章与其授予记录一起消失
|
|
if err := s.Delete(b1.ID); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
var count int64
|
|
db.Model(&model.UserBadge{}).Where("badge_id = ?", b1.ID).Count(&count)
|
|
if count != 0 {
|
|
t.Fatalf("删除后授予记录=%d want 0", count)
|
|
}
|
|
if err := s.Delete(b1.ID); !errors.Is(err, ErrBadgeNotFound) {
|
|
t.Fatal("重复删除应 ErrBadgeNotFound")
|
|
}
|
|
|
|
// 名称唯一冲突
|
|
if _, err := s.Create("萌新", "", ""); !errors.Is(err, ErrBadgeNameExists) {
|
|
t.Fatalf("重名应 ErrBadgeNameExists, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBadgeValidation(t *testing.T) {
|
|
s := NewBadgeService(newBadgeTestDB(t))
|
|
|
|
// 空名称 / 超长名称 / 非法颜色
|
|
if _, err := s.Create(" ", "", ""); !errors.Is(err, ErrBadgeInvalid) {
|
|
t.Fatal("空名称应被拒绝")
|
|
}
|
|
long := make([]rune, MaxBadgeNameRunes+1)
|
|
for i := range long {
|
|
long[i] = '字'
|
|
}
|
|
if _, err := s.Create(string(long), "", ""); !errors.Is(err, ErrBadgeInvalid) {
|
|
t.Fatal("超长名称应被拒绝")
|
|
}
|
|
if _, err := s.Create("x", "", "pink"); !errors.Is(err, ErrBadgeInvalid) {
|
|
t.Fatal("色板外颜色应被拒绝")
|
|
}
|
|
}
|