Smail/backend/services/auth/protocol_session_test.go
smail 8899e74a66
Some checks are pending
Security Checks / security (push) Waiting to run
修复邮件授权码撤销后旧协议连接仍可操作并补充回归验收
2026-09-21 01:57:23 +08:00

119 lines
3.8 KiB
Go

package auth
import (
"path/filepath"
"sync"
"testing"
"time"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/db"
"git.pcux.cn/xuans/Smail/internal/passwordtest"
"git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/services/credentials"
"xorm.io/xorm"
)
func TestProtocolSessionRechecksUserAndCredential(t *testing.T) {
for _, kind := range []string{"protocol", "client"} {
for _, change := range []string{"revoke", "disabled", "password", "session_version", "security_version", "role", "deleted", "database_failure"} {
t.Run(kind+"/"+change, func(t *testing.T) {
oldDB, oldConfig := db.Instance, config.Instance
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "session.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = engine.Close(); db.Instance, config.Instance = oldDB, oldConfig })
db.Instance = engine
config.Instance = &config.Config{Domain: "example.com"}
if err := engine.Sync2(&models.User{}, &models.UserEmail{}, &models.ClientCredential{}, &models.ProtocolCredential{}); err != nil {
t.Fatal(err)
}
user := models.User{Account: "session-user", Password: "test-hash", Role: "user", SessionVersion: 1, SecurityVersion: 1}
if _, err := engine.Insert(&user); err != nil {
t.Fatal(err)
}
address, now := "session-user@example.com", time.Now().UTC()
if _, err := engine.Insert(&models.UserEmail{UserID: user.ID, Address: address, AddressCanonical: &address, VerifiedAt: &now}); err != nil {
t.Fatal(err)
}
service, err := credentials.NewService(engine)
if err != nil {
t.Fatal(err)
}
var code string
var clientID int
if kind == "protocol" {
if err := service.SetProtocolEnabled(user.ID, "smtp", true); err != nil {
t.Fatal(err)
}
code, err = service.GenerateProtocolCode(user.ID, "smtp")
} else {
var item *credentials.ClientCredentialItem
code, item, err = service.CreateClientCredential(user.ID, "会话验收")
if item != nil {
clientID = item.ID
}
}
if err != nil {
t.Fatal(err)
}
_, session, err := AuthenticateProtocolSession("smtp", "192.0.2.91:1234", address, code)
if err != nil {
t.Fatal(err)
}
// 命令复核不占用密码哈希容量,也不累加登录失败计数。
release := passwordtest.Hold(t)
if err := session.Check(); err != nil {
t.Fatal(err)
}
release()
switch change {
case "revoke":
if kind == "protocol" {
err = service.RevokeProtocolCode(user.ID, "smtp")
} else {
err = service.RevokeClientCredential(user.ID, clientID)
}
case "disabled":
_, err = engine.ID(user.ID).Cols("disabled").Update(&models.User{Disabled: 1})
case "password":
_, err = engine.ID(user.ID).Cols("password").Update(&models.User{Password: "new-hash"})
case "session_version":
_, err = engine.ID(user.ID).Incr("session_version", 1).Update(&models.User{})
case "security_version":
_, err = engine.ID(user.ID).Incr("security_version", 1).Update(&models.User{})
case "role":
_, err = engine.ID(user.ID).Cols("role").Update(&models.User{Role: "admin"})
case "deleted":
_, err = engine.ID(user.ID).Delete(&models.User{})
case "database_failure":
err = engine.Close()
}
if err != nil {
t.Fatal(err)
}
var wait sync.WaitGroup
for range 4 {
wait.Add(1)
go func() {
defer wait.Done()
if session.Check() == nil {
t.Error("旧会话仍可访问数据")
}
}()
}
wait.Wait()
if change == "disabled" {
if _, err := engine.ID(user.ID).Cols("disabled").Update(&models.User{}); err != nil {
t.Fatal(err)
}
if session.Check() == nil {
t.Fatal("被拒绝的连接不应恢复授权")
}
}
})
}
}
}