Some checks are pending
Security Checks / security (push) Waiting to run
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
111 lines
3.2 KiB
Go
111 lines
3.2 KiB
Go
package totp
|
|
|
|
import (
|
|
"encoding/base32"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Smail/models"
|
|
_ "modernc.org/sqlite"
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
func newTestService(t *testing.T) (*Service, *xorm.Engine) {
|
|
t.Helper()
|
|
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "totp.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = engine.Close() })
|
|
if err := engine.Sync2(&models.TOTPAuthenticator{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root := t.TempDir()
|
|
service, err := NewService(engine, root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return service, engine
|
|
}
|
|
|
|
func TestEnrollmentVerifyReplayAndDisable(t *testing.T) {
|
|
service, _ := newTestService(t)
|
|
now := time.Date(2026, time.August, 27, 12, 0, 0, 0, time.UTC)
|
|
service.now = func() time.Time { return now }
|
|
|
|
enrollment, err := service.BeginEnrollment(1, "root")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(enrollment.Secret)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code := generateCode(secret, uint64(now.Unix()/PeriodSeconds))
|
|
if err := service.ConfirmEnrollment(1, enrollment.ID, code); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if enabled, err := service.IsEnabled(1); err != nil || !enabled {
|
|
t.Fatalf("enabled=%v err=%v", enabled, err)
|
|
}
|
|
if err := service.Verify(1, code); !errors.Is(err, ErrReplay) {
|
|
t.Fatalf("expected replay error, got %v", err)
|
|
}
|
|
|
|
now = now.Add(PeriodSeconds * time.Second)
|
|
nextCode := generateCode(secret, uint64(now.Unix()/PeriodSeconds))
|
|
if err := service.Verify(1, nextCode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = now.Add(PeriodSeconds * time.Second)
|
|
disableCode := generateCode(secret, uint64(now.Unix()/PeriodSeconds))
|
|
if err := service.Disable(1, disableCode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if enabled, err := service.IsEnabled(1); err != nil || enabled {
|
|
t.Fatalf("enabled=%v err=%v", enabled, err)
|
|
}
|
|
}
|
|
|
|
func TestEnrollmentInvalidCodeLimit(t *testing.T) {
|
|
service, _ := newTestService(t)
|
|
enrollment, err := service.BeginEnrollment(2, "alice")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for attempt := 0; attempt < 5; attempt++ {
|
|
if err := service.ConfirmEnrollment(2, enrollment.ID, "000000"); !errors.Is(err, ErrInvalidCode) {
|
|
t.Fatalf("attempt %d: %v", attempt+1, err)
|
|
}
|
|
}
|
|
if err := service.ConfirmEnrollment(2, enrollment.ID, "000000"); !errors.Is(err, ErrEnrollmentExpired) {
|
|
t.Fatalf("expected expired enrollment, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateAtAcceptsAdjacentWindow(t *testing.T) {
|
|
secret := []byte("01234567890123456789012345678901")
|
|
now := time.Unix(1_800_000_000, 0).UTC()
|
|
previousStep := now.Unix()/PeriodSeconds - 1
|
|
code := generateCode(secret, uint64(previousStep))
|
|
step, ok := validateAt(secret, code, now, 1)
|
|
if !ok || step != previousStep {
|
|
t.Fatalf("step=%d ok=%v", step, ok)
|
|
}
|
|
if _, ok := validateAt(secret, code, now.Add(PeriodSeconds*time.Second), 1); ok {
|
|
t.Fatal("expired adjacent code was accepted")
|
|
}
|
|
}
|
|
|
|
func TestProvisioningURIUsesSHA256(t *testing.T) {
|
|
uri := provisioningURI("root@example.com", "ABCDEF")
|
|
for _, value := range []string{"otpauth://totp/", "issuer=Smail", "algorithm=SHA256", "digits=6", "period=30", "secret=ABCDEF"} {
|
|
if !strings.Contains(uri, value) {
|
|
t.Fatalf("URI %q does not contain %q", uri, value)
|
|
}
|
|
}
|
|
}
|