588 lines
18 KiB
Go
588 lines
18 KiB
Go
// Copyright 2023 E99p1ant. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/tamakyi/TamaBox/internal/conf"
|
|
)
|
|
|
|
// requireUserEqual 比较期望与实际的 User。
|
|
// bcrypt 哈希每次加盐不同,无法逐字节比较:先按明文校验密码,再把哈希字段对齐后比较其余字段。
|
|
// Create 会按 conf.Limits.ChatMaxMessages 显式写入该字段,故期望值同步取该配置默认。
|
|
func requireUserEqual(t *testing.T, want, got *User) {
|
|
t.Helper()
|
|
require.True(t, got.Authenticate(want.Password))
|
|
want.Password = got.Password
|
|
want.ChatMaxMessages = conf.Limits.ChatMaxMessages
|
|
want.HarassmentSetting = HarassmentSettingNone // Create 显式写入 none
|
|
require.Equal(t, want, got)
|
|
}
|
|
|
|
func TestUsers(t *testing.T) {
|
|
// 注意:本测试各子测试创建用户均使用固定邮箱/域名([email protected] / e99),
|
|
// 且每个子测试结束会 truncate 整张 users 表。若开启 t.Parallel(),并发子测试会在
|
|
// 同一张表上同时插入相同唯一键导致"邮箱已注册"误报;故顺序执行,使每个子测试独占全表。
|
|
db, cleanup := newTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
usersStore := NewUsersStore(db)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
test func(t *testing.T, ctx context.Context, db *users)
|
|
}{
|
|
{"Create", testUsersCreate},
|
|
{"GetByID", testUsersGetByID},
|
|
{"GetByEmail", testUsersGetByEmail},
|
|
{"GetByDomain", testUsersGetByDomain},
|
|
{"Update", testUsersUpdate},
|
|
{"UpdateHarassmentType", testUsersUpdateHarassmentType},
|
|
{"UpdateBlockWords", testUsersUpdateBlockWords},
|
|
{"Authenticate", testUsersAuthenticate},
|
|
{"ChangePassword", testUsersChangePassword},
|
|
{"UpdatePassword", testUsersUpdatePassword},
|
|
{"Deactivate", testUsersDeactivate},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
if err := cleanup("users"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
tc.test(t, ctx, usersStore.(*users))
|
|
})
|
|
}
|
|
}
|
|
|
|
func testUsersCreate(t *testing.T, ctx context.Context, db *users) {
|
|
t.Run("normal", func(t *testing.T) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
})
|
|
|
|
t.Run("repeat email", func(t *testing.T) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99p1ant",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Equal(t, ErrDuplicateEmail, err)
|
|
})
|
|
|
|
t.Run("repeat domain", func(t *testing.T) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Equal(t, ErrDuplicateDomain, err)
|
|
})
|
|
}
|
|
|
|
func testUsersGetByID(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
got, err := db.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
|
|
got.CreatedAt = time.Time{}
|
|
got.UpdatedAt = time.Time{}
|
|
|
|
want := &User{
|
|
Model: gorm.Model{
|
|
ID: 1,
|
|
},
|
|
UID: got.UID,
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
Notify: NotifyTypeEmail,
|
|
}
|
|
requireUserEqual(t, want, got)
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
_, err := db.GetByID(ctx, 404)
|
|
require.Equal(t, ErrUserNotExists, err)
|
|
})
|
|
}
|
|
|
|
func testUsersGetByEmail(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
got, err := db.GetByEmail(ctx, "[email protected]")
|
|
require.Nil(t, err)
|
|
|
|
got.CreatedAt = time.Time{}
|
|
got.UpdatedAt = time.Time{}
|
|
|
|
want := &User{
|
|
Model: gorm.Model{
|
|
ID: 1,
|
|
},
|
|
UID: got.UID,
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
Notify: NotifyTypeEmail,
|
|
}
|
|
requireUserEqual(t, want, got)
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
_, err := db.GetByEmail(ctx, "404")
|
|
require.Equal(t, ErrUserNotExists, err)
|
|
})
|
|
}
|
|
|
|
func testUsersGetByDomain(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
got, err := db.GetByDomain(ctx, "e99")
|
|
require.Nil(t, err)
|
|
|
|
got.CreatedAt = time.Time{}
|
|
got.UpdatedAt = time.Time{}
|
|
|
|
want := &User{
|
|
Model: gorm.Model{
|
|
ID: 1,
|
|
},
|
|
UID: got.UID,
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
Notify: NotifyTypeEmail,
|
|
}
|
|
requireUserEqual(t, want, got)
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
_, err := db.GetByDomain(ctx, "404")
|
|
require.Equal(t, ErrUserNotExists, err)
|
|
})
|
|
}
|
|
|
|
func testUsersUpdate(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
err := db.Update(ctx, 1, UpdateUserOptions{
|
|
Name: "e99",
|
|
Avatar: "new_avatar.png",
|
|
Background: "new_background.png",
|
|
Intro: "Be cool, but also be warm!!",
|
|
Notify: NotifyTypeNone,
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
|
|
got.CreatedAt = time.Time{}
|
|
got.UpdatedAt = time.Time{}
|
|
|
|
want := &User{
|
|
Model: gorm.Model{
|
|
ID: 1,
|
|
},
|
|
UID: got.UID,
|
|
Name: "e99",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "new_avatar.png",
|
|
Domain: "e99",
|
|
Background: "new_background.png",
|
|
Intro: "Be cool, but also be warm!!",
|
|
Notify: NotifyTypeNone,
|
|
}
|
|
requireUserEqual(t, want, got)
|
|
})
|
|
}
|
|
|
|
func testUsersUpdateHarassmentType(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
err := db.UpdateHarassmentType(ctx, 1, HarassmentSettingTypeRegisterOnly)
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, HarassmentSettingTypeRegisterOnly, got.HarassmentSetting)
|
|
// 只应更新类型,屏蔽词列不受影响
|
|
require.Equal(t, "", got.BlockWords)
|
|
})
|
|
|
|
t.Run("unexpected harassment setting", func(t *testing.T) {
|
|
err := db.UpdateHarassmentType(ctx, 1, HarassmentSettingType("not found"))
|
|
require.NotNil(t, err)
|
|
})
|
|
}
|
|
|
|
func testUsersUpdateBlockWords(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
// 先显式设定防骚扰类型,验证 UpdateBlockWords 不会误改该列(类型列默认为空串,
|
|
// 与 HarassmentSettingNone 等价但值不同,故显式设定后再断言更可靠)。
|
|
require.Nil(t, db.UpdateHarassmentType(ctx, 1, HarassmentSettingTypeRegisterOnly))
|
|
|
|
err := db.UpdateBlockWords(ctx, 1, "广告,抽奖")
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "广告,抽奖", got.BlockWords)
|
|
// 只应更新屏蔽词,类型列不受影响
|
|
require.Equal(t, HarassmentSettingTypeRegisterOnly, got.HarassmentSetting)
|
|
})
|
|
|
|
t.Run("empty clears words", func(t *testing.T) {
|
|
err := db.UpdateBlockWords(ctx, 1, "")
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "", got.BlockWords)
|
|
})
|
|
}
|
|
|
|
func testUsersAuthenticate(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.Authenticate(ctx, "[email protected]", "super_secret")
|
|
require.Nil(t, err)
|
|
|
|
got.CreatedAt = time.Time{}
|
|
got.UpdatedAt = time.Time{}
|
|
|
|
want := &User{
|
|
Model: gorm.Model{
|
|
ID: 1,
|
|
},
|
|
UID: got.UID,
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
Notify: NotifyTypeEmail,
|
|
}
|
|
requireUserEqual(t, want, got)
|
|
}
|
|
|
|
func testUsersChangePassword(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
err := db.ChangePassword(ctx, 1, "super_secret", "new_password")
|
|
require.Nil(t, err)
|
|
})
|
|
|
|
t.Run("wrong password", func(t *testing.T) {
|
|
err := db.ChangePassword(ctx, 1, "wrong_password", "new_password")
|
|
require.Equal(t, ErrBadCredential, err)
|
|
})
|
|
}
|
|
|
|
func testUsersUpdatePassword(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "e99",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
err := db.UpdatePassword(ctx, 1, "new_password")
|
|
require.Nil(t, err)
|
|
})
|
|
}
|
|
|
|
func testUsersDeactivate(t *testing.T, ctx context.Context, db *users) {
|
|
err := db.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "deactivate",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
err := db.Deactivate(ctx, 1)
|
|
require.Nil(t, err)
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
err := db.Deactivate(ctx, 404)
|
|
require.NotNil(t, err)
|
|
})
|
|
}
|
|
|
|
// TestHarassmentSettingNormalize 覆盖防骚扰设置取值归一:
|
|
// 新建用户即写入规范值 none;历史空值经启动期归一(normalizeHarassmentSetting)后也为 none。
|
|
func TestHarassmentSettingNormalize(t *testing.T) {
|
|
db, _ := newTestDB(t)
|
|
ctx := context.Background()
|
|
usersStore := NewUsersStore(db)
|
|
|
|
require.Nil(t, usersStore.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "normalize",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
}))
|
|
|
|
got, err := usersStore.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, HarassmentSettingNone, got.HarassmentSetting, "新建用户应为规范值 none")
|
|
|
|
// 模拟历史行:直接把列置为空串,再跑一次启动期归一。
|
|
require.Nil(t, db.Exec("UPDATE users SET harassment_setting = '' WHERE id = 1").Error)
|
|
require.Nil(t, normalizeHarassmentSetting(db))
|
|
|
|
got, err = usersStore.GetByID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, HarassmentSettingNone, got.HarassmentSetting, "历史空值应被归一为 none")
|
|
}
|
|
|
|
// TestDeactivatePurgesUser 覆盖自助停用的删除语义。
|
|
//
|
|
// 停用 = 清空内容 + 保留用户行作「身份占位」(软删除),因此:
|
|
// - 该账号的提问等内容必须被物理删除;
|
|
// - 用户行必须仍在(Unscoped 可见,deleted_at 有效),邮箱/域名继续被占用 → 防止他人抢占;
|
|
// - 用同一邮箱/域名重新注册必须得到「账号已停用」的明确错误,而不是底层的唯一键 500;
|
|
// - 站长可通过 Reactivate 把账号恢复成空账号。
|
|
func TestDeactivatePurgesUser(t *testing.T) {
|
|
db, cleanup := newTestDB(t)
|
|
defer func() { _ = cleanup("users", "questions", "feature_controls") }()
|
|
ctx := context.Background()
|
|
usersStore := NewUsersStore(db)
|
|
questionsStore := NewQuestionsStore(db)
|
|
|
|
opts := CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "purge",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
}
|
|
require.Nil(t, usersStore.Create(ctx, opts))
|
|
|
|
user, err := usersStore.GetByEmail(ctx, opts.Email)
|
|
require.Nil(t, err)
|
|
|
|
_, err = questionsStore.Create(ctx, CreateQuestionOptions{UserID: user.ID, Content: "hello"})
|
|
require.Nil(t, err)
|
|
|
|
require.Nil(t, usersStore.Deactivate(ctx, user.ID))
|
|
|
|
var n int64
|
|
// 身份占位必须保留(Unscoped 可见),且处于软删除状态
|
|
var placeholder User
|
|
require.Nil(t, db.WithContext(ctx).Unscoped().Where("email = ?", opts.Email).First(&placeholder).Error)
|
|
require.True(t, placeholder.DeletedAt.Valid, "停用后用户行应保留为软删除的身份占位")
|
|
require.Equal(t, user.ID, placeholder.ID)
|
|
|
|
require.Nil(t, db.WithContext(ctx).Unscoped().Model(&Question{}).Where("user_id = ?", user.ID).Count(&n).Error)
|
|
require.EqualValues(t, 0, n, "停用后该用户的提问应被物理删除")
|
|
|
|
// 普通查询已看不见该占位行
|
|
_, err = usersStore.GetByEmail(ctx, opts.Email)
|
|
require.Equal(t, ErrUserNotExists, err)
|
|
|
|
// 同一邮箱 / 域名重新注册:给出明确的「已停用」错误,而不是底层唯一键冲突
|
|
err = usersStore.Create(ctx, opts)
|
|
require.True(t, errors.Is(err, ErrDeactivatedEmail), "同邮箱重注册应返回 ErrDeactivatedEmail,实际: %v", err)
|
|
require.False(t, errors.Is(err, ErrDuplicateEmail), "不应退化成普通重复邮箱错误")
|
|
|
|
err = usersStore.Create(ctx, CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: opts.Domain,
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
})
|
|
require.True(t, errors.Is(err, ErrDeactivatedDomain), "同域名重注册应返回 ErrDeactivatedDomain,实际: %v", err)
|
|
|
|
// 站长恢复:占位行清掉 deleted_at,账号重新可见
|
|
require.Nil(t, usersStore.Reactivate(ctx, user.ID))
|
|
|
|
restored, err := usersStore.GetByEmail(ctx, opts.Email)
|
|
require.Nil(t, err)
|
|
require.Equal(t, user.ID, restored.ID)
|
|
require.False(t, restored.DeletedAt.Valid)
|
|
|
|
// 恢复后该邮箱已被活跃账号占用 → 普通重复错误
|
|
require.True(t, errors.Is(usersStore.Create(ctx, opts), ErrDuplicateEmail))
|
|
|
|
// 未被停用的账号无法重复恢复
|
|
require.Equal(t, ErrUserNotExists, usersStore.Reactivate(ctx, user.ID))
|
|
}
|
|
|
|
// TestBanUserAndCleanupRemovesRow 封禁与停用的唯一差别:封禁连用户行一起物理删除,
|
|
// 该邮箱/域名被彻底释放。
|
|
func TestBanUserAndCleanupRemovesRow(t *testing.T) {
|
|
db, cleanup := newTestDB(t)
|
|
defer func() { _ = cleanup("users", "questions", "feature_controls") }()
|
|
ctx := context.Background()
|
|
usersStore := NewUsersStore(db)
|
|
questionsStore := NewQuestionsStore(db)
|
|
|
|
opts := CreateUserOptions{
|
|
Name: "E99p1ant",
|
|
Password: "super_secret",
|
|
Email: "[email protected]",
|
|
Avatar: "avater.png",
|
|
Domain: "ban",
|
|
Background: "background.png",
|
|
Intro: "Be cool, but also be warm.",
|
|
}
|
|
require.Nil(t, usersStore.Create(ctx, opts))
|
|
|
|
user, err := usersStore.GetByEmail(ctx, opts.Email)
|
|
require.Nil(t, err)
|
|
_, err = questionsStore.Create(ctx, CreateQuestionOptions{UserID: user.ID, Content: "hello"})
|
|
require.Nil(t, err)
|
|
|
|
require.Nil(t, usersStore.BanUserAndCleanup(ctx, user.ID))
|
|
|
|
var n int64
|
|
require.Nil(t, db.WithContext(ctx).Unscoped().Model(&User{}).Where("email = ?", opts.Email).Count(&n).Error)
|
|
require.EqualValues(t, 0, n, "封禁后用户行应被物理删除")
|
|
require.Nil(t, db.WithContext(ctx).Unscoped().Model(&Question{}).Where("user_id = ?", user.ID).Count(&n).Error)
|
|
require.EqualValues(t, 0, n, "封禁后该用户的提问应被物理删除")
|
|
|
|
require.Nil(t, usersStore.Create(ctx, opts), "封禁后应能用同一邮箱重新注册")
|
|
}
|