Smail/backend/services/adminreset/reset_test.go
smail f60d3b0880
Some checks are pending
Security Checks / security (push) Waiting to run
整理前后端与测试构建目录并完善配置和安全引导
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
2026-09-05 19:19:52 +08:00

163 lines
7.0 KiB
Go

package adminreset
import (
"context"
"path/filepath"
"testing"
"time"
"git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/services/auth"
"git.pcux.cn/xuans/Smail/utils/password"
_ "modernc.org/sqlite"
"xorm.io/xorm"
)
func TestResetSuperAdminOnlyRevokesTargetSessionsAndCredentials(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "reset.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
for _, table := range []any{&models.User{}, &models.Sessions{}, &models.SecurityQuestion{}, &models.RecoveryCode{}, &models.WebAuthnCredential{}, &models.ProtocolCredential{}} {
if err := engine.Sync2(table); err != nil {
t.Fatal(err)
}
}
superAdmin := models.User{Account: "root", Name: "超级管理员", Role: "super_admin", IsAdmin: 1, SessionVersion: 3}
if err := auth.SetPassword(&superAdmin, "OldPass123"); err != nil {
t.Fatal(err)
}
ordinaryUser := models.User{Account: "alice", Name: "普通用户", Role: "user", SessionVersion: 7}
if err := auth.SetPassword(&ordinaryUser, "UserPass123"); err != nil {
t.Fatal(err)
}
if _, err := engine.Insert(&superAdmin, &ordinaryUser); err != nil {
t.Fatal(err)
}
if _, err := engine.Insert(
&models.Sessions{Token: "super-token", Data: "opaque-super", Expiry: 4102444800},
&models.Sessions{Token: "user-token", Data: "opaque-user", Expiry: 4102444800},
&models.SecurityQuestion{UserID: superAdmin.ID, Question: "问题", AnswerHash: "hash", Version: 1},
&models.SecurityQuestion{UserID: ordinaryUser.ID, Question: "问题", AnswerHash: "hash", Version: 1},
&models.RecoveryCode{UserID: superAdmin.ID, CodeHash: "hash", CodeVersion: 1},
&models.RecoveryCode{UserID: ordinaryUser.ID, CodeHash: "hash", CodeVersion: 1},
&models.ProtocolCredential{UserID: superAdmin.ID, Protocol: "smtp", Enabled: true, CodeHash: "hash", CodeVersion: 1},
&models.ProtocolCredential{UserID: ordinaryUser.ID, Protocol: "smtp", Enabled: true, CodeHash: "hash", CodeVersion: 1},
&models.WebAuthnCredential{UserID: superAdmin.ID, CredentialID: "super-key", CredentialData: "encrypted", DisplayName: "管理密钥", CreatedAt: time.Now()},
&models.WebAuthnCredential{UserID: ordinaryUser.ID, CredentialID: "user-key", CredentialData: "encrypted", DisplayName: "用户密钥", CreatedAt: time.Now()},
); err != nil {
t.Fatal(err)
}
invalidatedUserID := 0
invalidatedAccount := ""
if err := ResetSuperAdmin(engine, "NewPass123", func(userID int, account string) {
invalidatedUserID = userID
invalidatedAccount = account
}); err != nil {
t.Fatal(err)
}
if invalidatedUserID != superAdmin.ID || invalidatedAccount != superAdmin.Account {
t.Fatalf("runtime invalidation target = %d/%s", invalidatedUserID, invalidatedAccount)
}
var updated models.User
found, err := engine.ID(superAdmin.ID).Get(&updated)
if err != nil || !found {
t.Fatalf("load updated user: %v", err)
}
if !password.Verify(updated.Password, "NewPass123") || updated.SessionVersion != 4 {
t.Fatalf("password/session version not updated: %+v", updated)
}
count, err := engine.Count(&models.Sessions{})
if err != nil {
t.Fatal(err)
}
if count != 2 {
t.Fatalf("other sessions were deleted: %d", count)
}
var unchanged models.User
if found, err := engine.ID(ordinaryUser.ID).Get(&unchanged); err != nil || !found || unchanged.SessionVersion != 7 {
t.Fatalf("ordinary user session version changed: %+v, %v", unchanged, err)
}
if count, err := engine.Where("user_id = ?", superAdmin.ID).Count(&models.SecurityQuestion{}); err != nil || count != 0 {
t.Fatalf("super administrator security question count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ? AND used_at IS NULL", superAdmin.ID).Count(&models.RecoveryCode{}); err != nil || count != 0 {
t.Fatalf("super administrator active recovery code count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ? AND revoked_at IS NULL", superAdmin.ID).Count(&models.WebAuthnCredential{}); err != nil || count != 0 {
t.Fatalf("super administrator active WebAuthn count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ? AND revoked_at IS NULL", superAdmin.ID).Count(&models.ProtocolCredential{}); err != nil || count != 0 {
t.Fatalf("super administrator active protocol credential count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ?", ordinaryUser.ID).Count(&models.SecurityQuestion{}); err != nil || count != 1 {
t.Fatalf("ordinary user security question count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ? AND used_at IS NULL", ordinaryUser.ID).Count(&models.RecoveryCode{}); err != nil || count != 1 {
t.Fatalf("ordinary user active recovery code count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ? AND revoked_at IS NULL", ordinaryUser.ID).Count(&models.WebAuthnCredential{}); err != nil || count != 1 {
t.Fatalf("ordinary user active WebAuthn count = %d, err = %v", count, err)
}
if count, err := engine.Where("user_id = ? AND revoked_at IS NULL", ordinaryUser.ID).Count(&models.ProtocolCredential{}); err != nil || count != 1 {
t.Fatalf("ordinary user active protocol credential count = %d, err = %v", count, err)
}
}
func TestResetSuperAdminRollsBackWhenCredentialRevocationFails(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "rollback.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
if err := engine.Sync2(&models.User{}); err != nil {
t.Fatal(err)
}
user := models.User{Account: "root", Name: "超级管理员", Role: "super_admin", IsAdmin: 1, SessionVersion: 9}
if err := auth.SetPassword(&user, "OldPass123"); err != nil {
t.Fatal(err)
}
oldHash := user.Password
if _, err := engine.Insert(&user); err != nil {
t.Fatal(err)
}
invalidated := false
if err := ResetSuperAdmin(engine, "NewPass123", func(int, string) { invalidated = true }); err == nil {
t.Fatal("missing credential tables did not fail reset")
}
if invalidated {
t.Fatal("runtime state was invalidated before transaction commit")
}
var unchanged models.User
if found, err := engine.ID(user.ID).Get(&unchanged); err != nil || !found {
t.Fatalf("load rolled back user: %v", err)
}
if unchanged.Password != oldHash || unchanged.SessionVersion != 9 {
t.Fatalf("password transaction was not rolled back: %+v", unchanged)
}
}
func TestResetSuperAdminRejectsMissingTarget(t *testing.T) {
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "reset.db"))
if err != nil {
t.Fatal(err)
}
defer engine.Close()
if err := engine.Sync2(&models.User{}, &models.Sessions{}, &models.SecurityQuestion{}, &models.RecoveryCode{}, &models.WebAuthnCredential{}); err != nil {
t.Fatal(err)
}
if err := ResetSuperAdmin(engine, "NewPass123"); err != ErrSuperAdminNotFound {
t.Fatalf("error = %v", err)
}
}
func TestControlServerInterfaceCompiles(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := ListenControl(filepath.Join(t.TempDir(), "control.sock"), []uint32{0}); err == nil {
// 具体平台实现由各自适配器测试覆盖。
}
_ = ctx
}