134 lines
4.9 KiB
Go
134 lines
4.9 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
|
)
|
|
|
|
func TestSQLUserRepositoryCRUDAndLookup(t *testing.T) {
|
|
db := openUserTestDatabase(t)
|
|
defer db.Close()
|
|
repo := NewSQLUserRepository(db.DB, "sqlite")
|
|
hash := userTestPasswordHash(t, "UserPass123")
|
|
item := User{ID: "user-1", Username: "Operator", Email: "Operator@Example.Test", PasswordHash: hash, Role: UserRoleUser, Status: UserStatusActive}
|
|
if err := repo.CreateUser(t.Context(), item); err != nil {
|
|
t.Fatalf("创建用户失败:%v", err)
|
|
}
|
|
|
|
byName, err := repo.FindByIdentifier(t.Context(), " operator ")
|
|
if err != nil || byName.ID != item.ID || byName.Username != "operator" {
|
|
t.Fatalf("按用户名查询用户失败:%v %+v", err, byName)
|
|
}
|
|
byEmail, err := repo.FindByIdentifier(t.Context(), "OPERATOR@EXAMPLE.TEST")
|
|
if err != nil || byEmail.ID != item.ID || byEmail.Email != "operator@example.test" {
|
|
t.Fatalf("按邮箱查询用户失败:%v %+v", err, byEmail)
|
|
}
|
|
|
|
encoded, err := json.Marshal(byName)
|
|
if err != nil || strings.Contains(string(encoded), hash) || strings.Contains(string(encoded), "password_hash") {
|
|
t.Fatalf("用户 JSON 不应包含密码摘要:%s", encoded)
|
|
}
|
|
|
|
updated := byName
|
|
updated.Role = UserRoleAdmin
|
|
updated.Status = UserStatusDisabled
|
|
if err := repo.UpdateUser(t.Context(), updated); err != nil {
|
|
t.Fatalf("更新用户失败:%v", err)
|
|
}
|
|
loaded, err := repo.GetUserByID(t.Context(), item.ID)
|
|
if err != nil || loaded.Role != UserRoleAdmin || loaded.Status != UserStatusDisabled {
|
|
t.Fatalf("读取更新后的用户失败:%v %+v", err, loaded)
|
|
}
|
|
if bcrypt.CompareHashAndPassword([]byte(loaded.PasswordHash), []byte("UserPass123")) != nil {
|
|
t.Fatal("更新用户不应改变原密码")
|
|
}
|
|
|
|
items, err := repo.ListUsers(t.Context())
|
|
if err != nil || len(items) != 1 {
|
|
t.Fatalf("列出用户失败:%v %+v", err, items)
|
|
}
|
|
if err := repo.DeleteUser(t.Context(), item.ID); err != nil {
|
|
t.Fatalf("删除用户失败:%v", err)
|
|
}
|
|
if _, err := repo.GetUserByID(t.Context(), item.ID); !errors.Is(err, context.Canceled) && !IsNotFound(err) {
|
|
t.Fatalf("删除用户后查询结果错误:%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSQLUserRepositoryEnforcesIdentityAndSuperAdminRules(t *testing.T) {
|
|
db := openUserTestDatabase(t)
|
|
defer db.Close()
|
|
repo := NewSQLUserRepository(db.DB, "sqlite")
|
|
first := User{ID: "user-1", Username: "operator", Email: "operator@example.test", PasswordHash: userTestPasswordHash(t, "UserPass123"), Role: UserRoleUser, Status: UserStatusActive}
|
|
if err := repo.CreateUser(t.Context(), first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
duplicateName := first
|
|
duplicateName.ID = "user-2"
|
|
duplicateName.Email = "second@example.test"
|
|
if err := repo.CreateUser(t.Context(), duplicateName); !IsConflict(err) {
|
|
t.Fatalf("重复用户名未被拒绝:%v", err)
|
|
}
|
|
duplicateEmail := first
|
|
duplicateEmail.ID = "user-3"
|
|
duplicateEmail.Username = "second"
|
|
if err := repo.CreateUser(t.Context(), duplicateEmail); !IsConflict(err) {
|
|
t.Fatalf("重复邮箱未被拒绝:%v", err)
|
|
}
|
|
|
|
if err := repo.EnsureSuperAdmin(t.Context(), User{ID: "primary-super-admin", Username: "admin", Email: "admin@example.test", PasswordHash: userTestPasswordHash(t, "AdminPass123")}); err != nil {
|
|
t.Fatalf("保存超级管理员失败:%v", err)
|
|
}
|
|
if err := repo.DeleteUser(t.Context(), "primary-super-admin"); !errors.Is(err, ErrSuperAdminProtected) {
|
|
t.Fatalf("主超级管理员不应被删除:%v", err)
|
|
}
|
|
secondAdmin := User{ID: "super-2", Username: "owner", PasswordHash: userTestPasswordHash(t, "OwnerPass123"), Role: UserRoleSuperAdmin, Status: UserStatusActive}
|
|
if err := repo.CreateUser(t.Context(), secondAdmin); err != nil {
|
|
t.Fatalf("创建第二个超级管理员失败:%v", err)
|
|
}
|
|
if err := repo.DeleteUser(t.Context(), secondAdmin.ID); err != nil {
|
|
t.Fatalf("删除第二个超级管理员失败:%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSQLUserRepositoryNilDatabase(t *testing.T) {
|
|
var repo *SQLUserRepository
|
|
if err := repo.CreateUser(context.Background(), User{}); err == nil {
|
|
t.Fatal("空数据库连接应返回错误")
|
|
}
|
|
}
|
|
|
|
func openUserTestDatabase(t *testing.T) *database.Database {
|
|
t.Helper()
|
|
directory := t.TempDir()
|
|
cfg := config.Default(directory).Database
|
|
cfg.DSN = filepath.Join(directory, "data", "users.db")
|
|
db, err := database.Open(t.Context(), cfg)
|
|
if err != nil {
|
|
t.Fatalf("打开用户测试数据库失败:%v", err)
|
|
}
|
|
if err := database.Migrate(t.Context(), db.DB, "sqlite"); err != nil {
|
|
_ = db.Close()
|
|
t.Fatalf("迁移用户测试数据库失败:%v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func userTestPasswordHash(t *testing.T, password string) string {
|
|
t.Helper()
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("生成测试密码摘要失败:%v", err)
|
|
}
|
|
return string(hash)
|
|
}
|