package service import ( "encoding/json" "fmt" "net/url" "os" "testing" "time" "github.com/freefire/jiang13-bbs/config" "github.com/freefire/jiang13-bbs/model" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) // staffTestDB 独立 schema 隔离的测试库(同 testOps 模式;未配置 DSN 则跳过) func staffTestDB(t *testing.T) *gorm.DB { t.Helper() dsn := os.Getenv("OPS_TEST_DATABASE_URL") if dsn == "" { t.Skip("set OPS_TEST_DATABASE_URL to an isolated PostgreSQL database") } u, e := url.Parse(dsn) if e != nil { t.Fatal(e) } db, e := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) if e != nil { t.Fatal(e) } schema := "staff_exempt_" + fmt.Sprint(time.Now().UnixNano()) if e = db.Exec("CREATE SCHEMA " + schema).Error; e != nil { t.Fatal(e) } q := u.Query() q.Set("search_path", schema) u.RawQuery = q.Encode() scoped, e := gorm.Open(postgres.Open(u.String()), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) if e != nil { t.Fatal(e) } t.Cleanup(func() { sql, _ := scoped.DB() _ = sql.Close() db.Exec("DROP SCHEMA " + schema + " CASCADE") sql, _ = db.DB() _ = sql.Close() }) if e = scoped.AutoMigrate(&model.User{}, &model.Board{}, &model.Post{}, &model.Comment{}, &model.SiteSetting{}, &model.PostLotteryEntry{}, &model.ModuleConfig{}, &model.SettingsAudit{}); e != nil { t.Fatal(e) } return scoped } func mustUser(t *testing.T, db *gorm.DB, username string, role model.Role, createdAt time.Time) model.User { t.Helper() u := model.User{Username: username, Password: "x", Role: role, CreatedAt: createdAt, UpdatedAt: createdAt} if err := db.Create(&u).Error; err != nil { t.Fatal(err) } return u } func TestCheckNewUserCooldownStaffExempt(t *testing.T) { db := staffTestDB(t) now := time.Now().UTC() plain := mustUser(t, db, "plain", model.RoleUser, now) owner := mustUser(t, db, "owner", model.RoleOwner, now) badmin := mustUser(t, db, "badmin", model.RoleBoardAdmin, now) ps := NewPostService(db).WithSetting(NewSettingService(db)) if err := ps.checkNewUserCooldown(plain.ID); err == nil { t.Fatal("new plain user should be blocked by cooldown") } for _, u := range []model.User{owner, badmin} { if err := ps.checkNewUserCooldown(u.ID); err != nil { t.Fatalf("staff role %s should bypass cooldown: %v", u.Role, err) } } } func TestFilterStaffExempt(t *testing.T) { db := staffTestDB(t) b, _ := json.Marshal(FilterConfig{Enabled: true, Rules: []FilterRule{{ ID: "r1", Word: "bad", Scopes: []string{"body"}, Action: "block", Enabled: true, }}}) if err := db.Create(&model.ModuleConfig{Name: "filter", Version: 1, Data: string(b)}).Error; err != nil { t.Fatal(err) } plain := mustUser(t, db, "plain", model.RoleUser, time.Now().UTC()) owner := mustUser(t, db, "owner", model.RoleOwner, time.Now().UTC()) o := NewOperations(db, &config.Config{DevMode: true}) if err := o.Filter("body", "a bad word", plain.ID); err == nil { t.Fatal("plain user should be blocked by filter") } if err := o.Filter("body", "a bad word", owner.ID); err != nil { t.Fatalf("owner should bypass filter: %v", err) } if err := o.Filter("body", "a bad word", 0); err == nil { t.Fatal("anonymous should be blocked by filter") } } func TestLotteryEligibleStaffBypassesCooldown(t *testing.T) { db := staffTestDB(t) now := time.Now().UTC() author := mustUser(t, db, "author", model.RoleUser, now) plain := mustUser(t, db, "plain", model.RoleUser, now) owner := mustUser(t, db, "owner", model.RoleOwner, now) board := model.Board{Name: "test"} if err := db.Create(&board).Error; err != nil { t.Fatal(err) } post := model.Post{BoardID: board.ID, UserID: author.ID, Title: "lottery", Content: "draw", PostType: model.PostTypeLottery, Status: model.ContentStatusPublished} if err := db.Create(&post).Error; err != nil { t.Fatal(err) } for _, uid := range []uint{plain.ID, owner.ID} { c := model.Comment{PostID: post.ID, UserID: uid, Content: "join", Status: model.ContentStatusPublished} if err := db.Create(&c).Error; err != nil { t.Fatal(err) } } ps := NewPostService(db).WithSetting(NewSettingService(db)) ids, err := ps.lotteryEligibleIDs(post.ID, author.ID) if err != nil { t.Fatal(err) } got := map[uint]bool{} for _, id := range ids { got[id] = true } if got[plain.ID] { t.Fatal("new plain user should be excluded from lottery pool") } if !got[owner.ID] { t.Fatal("staff new account should stay in lottery pool") } }