Smail/backend/services/credentials/service_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

147 lines
4.2 KiB
Go

package credentials
import (
"errors"
"path/filepath"
"strings"
"sync"
"testing"
"git.pcux.cn/xuans/Smail/models"
_ "modernc.org/sqlite"
"xorm.io/xorm"
)
func newFixture(t *testing.T) *Service {
t.Helper()
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "credentials.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = engine.Close() })
if err := engine.Sync2(&models.User{}, &models.SecurityQuestion{}, &models.RecoveryCode{}, &models.WebAuthnCredential{}, &models.ProtocolCredential{}); err != nil {
t.Fatal(err)
}
if _, err := engine.Insert(&models.User{Account: "alice", Name: "Alice", Password: "unused", Role: "user"}); err != nil {
t.Fatal(err)
}
service, err := NewService(engine)
if err != nil {
t.Fatal(err)
}
return service
}
func TestSecurityQuestionIsCaseInsensitiveAndNeverReturnsHash(t *testing.T) {
service := newFixture(t)
if err := service.SetSecurityQuestion(1, "第一只宠物叫什么?", "BlueBird123"); err != nil {
t.Fatal(err)
}
question, ok, err := service.GetSecurityQuestion(1)
if err != nil || !ok || question == "" {
t.Fatalf("question = %q, ok=%v, err=%v", question, ok, err)
}
if verified, err := service.VerifySecurityQuestion(1, " bluebird123 "); err != nil || !verified {
t.Fatalf("verify = %v, %v", verified, err)
}
if verified, err := service.VerifySecurityQuestion(1, "wronganswer"); err != nil || verified {
t.Fatalf("wrong answer = %v, %v", verified, err)
}
var stored models.SecurityQuestion
if _, err := service.engine.ID(1).Get(&stored); err != nil {
t.Fatal(err)
}
if stored.AnswerHash == "BlueBird123" || stored.AnswerHash == "bluebird123" {
t.Fatal("answer was stored in plaintext")
}
}
func TestRecoveryCodesAreShownOnceAndConsumedAtomically(t *testing.T) {
service := newFixture(t)
codes, err := service.GenerateRecoveryCodes(1, 5)
if err != nil || len(codes) != 5 {
t.Fatalf("codes = %v, err=%v", codes, err)
}
var stored []models.RecoveryCode
if err := service.engine.Find(&stored); err != nil {
t.Fatal(err)
}
for _, item := range stored {
if item.CodeHash == codes[0] {
t.Fatal("recovery code was stored in plaintext")
}
}
if err := service.ConsumeRecoveryCode(1, codes[0]); err != nil {
t.Fatal(err)
}
if err := service.ConsumeRecoveryCode(1, codes[0]); !errors.Is(err, ErrCodeNotFound) {
t.Fatalf("replay error = %v", err)
}
}
func TestRecoveryCodeConcurrentConsumptionHasOneWinner(t *testing.T) {
service := newFixture(t)
codes, err := service.GenerateRecoveryCodes(1, 5)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
results := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results <- service.ConsumeRecoveryCode(1, codes[0])
}()
}
wg.Wait()
close(results)
winners := 0
for err := range results {
if err == nil {
winners++
} else if !errors.Is(err, ErrCodeNotFound) && !errors.Is(err, ErrCodeConsumed) {
t.Fatalf("unexpected error: %v", err)
}
}
if winners != 1 {
t.Fatalf("winners = %d", winners)
}
}
func TestProtocolCredentialLifecycle(t *testing.T) {
service := newFixture(t)
if err := service.SetProtocolEnabled(1, ProtocolSMTP, true); err != nil {
t.Fatal(err)
}
statuses, err := service.ListProtocolCredentials(1)
if err != nil {
t.Fatal(err)
}
if len(statuses) != 3 || !statuses[2].Enabled || statuses[2].Configured {
t.Fatalf("unexpected initial protocol status: %+v", statuses)
}
if _, err := service.GenerateProtocolCode(1, ProtocolIMAP); !errors.Is(err, ErrProtocolDisabled) {
t.Fatalf("generating a disabled protocol code error = %v", err)
}
code, err := service.GenerateProtocolCode(1, ProtocolSMTP)
if err != nil {
t.Fatal(err)
}
if code == "" || strings.Contains(code, " ") {
t.Fatalf("invalid generated protocol code %q", code)
}
if !service.VerifyProtocolCode(1, ProtocolSMTP, code) {
t.Fatal("generated protocol code was rejected")
}
if service.VerifyProtocolCode(1, ProtocolSMTP, "wrong-code") {
t.Fatal("wrong protocol code was accepted")
}
if err := service.RevokeProtocolCode(1, ProtocolSMTP); err != nil {
t.Fatal(err)
}
if service.VerifyProtocolCode(1, ProtocolSMTP, code) {
t.Fatal("revoked protocol code was accepted")
}
}