Smail/backend/services/verification/service_test.go
smail 6de0400cb4
Some checks are pending
Security Checks / security (push) Waiting to run
验证码使用成功后立即释放发送冷却并保留累计限额
2026-09-18 23:11:46 +08:00

117 lines
3.8 KiB
Go

package verification
import (
"errors"
"fmt"
"testing"
"time"
)
func TestServiceIssuesVerifiesAndAppliesMultiDimensionCooldown(t *testing.T) {
store, err := NewStore(8)
if err != nil {
t.Fatal(err)
}
var code string
service, err := NewService(store, func(_ Challenge, issued string) error {
code = issued
return nil
}, nil)
if err != nil {
t.Fatal(err)
}
fixed := time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC)
service.now = func() time.Time { return fixed }
challenge, err := service.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "user@example.com"}, "127.0.0.1")
if err != nil {
t.Fatal(err)
}
if err := service.Verify(challenge.ID, "smail", "user@example.com", code, PurposeLogin, "127.0.0.1"); err != nil {
t.Fatal(err)
}
if _, err := service.Issue(IssueRequest{Purpose: PurposeLogin, Account: "smail", Email: "user@example.com"}, "127.0.0.1"); err != nil {
t.Fatalf("second issue error = %v", err)
}
}
func TestServiceRequiresDelivery(t *testing.T) {
store, err := NewStore(4)
if err != nil {
t.Fatal(err)
}
if _, err := NewService(store, nil, nil); !errors.Is(err, ErrServiceDown) {
t.Fatalf("error = %v", err)
}
}
func TestServiceAppliesIndependentAccountEmailAndIPLimits(t *testing.T) {
store, err := NewStore(64)
if err != nil {
t.Fatal(err)
}
service, err := NewService(store, func(_ Challenge, _ string) error { return nil }, nil)
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC)
service.now = func() time.Time { return now }
for i := 0; i < maxPerIPHour; i++ {
if _, err := service.Issue(IssueRequest{Purpose: PurposeLogin, Account: "user", Email: "one@example.com", TTL: time.Second}, "192.0.2.1"); err != nil {
if i == 0 || !errors.Is(err, ErrRateLimited) {
t.Fatalf("issue %d error = %v", i, err)
}
}
now = now.Add(issueInterval)
}
if _, err := service.Issue(IssueRequest{Purpose: PurposeLogin, Account: "user", Email: "two@example.com"}, "192.0.2.2"); !errors.Is(err, ErrRateLimited) {
t.Fatalf("account limit was bypassed by changing email and IP: %v", err)
}
}
func TestServiceAppliesIPLimitAcrossAccounts(t *testing.T) {
store, err := NewStore(128)
if err != nil {
t.Fatal(err)
}
service, err := NewService(store, func(_ Challenge, _ string) error { return nil }, nil)
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC)
service.now = func() time.Time { return now }
for i := 0; i < maxPerIPHour; i++ {
if _, err := service.Issue(IssueRequest{Purpose: PurposeLogin, Account: fmt.Sprintf("user-%d", i), Email: fmt.Sprintf("user-%d@example.com", i), TTL: time.Second}, "192.0.2.10"); err != nil {
t.Fatal(err)
}
now = now.Add(issueInterval)
}
if _, err := service.Issue(IssueRequest{Purpose: PurposeLogin, Account: "another", Email: "another@example.com"}, "192.0.2.10"); !errors.Is(err, ErrRateLimited) {
t.Fatalf("IP limit was bypassed by changing account and email: %v", err)
}
}
func TestServiceBindsVerificationToResource(t *testing.T) {
store, err := NewStore(8)
if err != nil {
t.Fatal(err)
}
var code string
service, err := NewService(store, func(_ Challenge, issued string) error {
code = issued
return nil
}, nil)
if err != nil {
t.Fatal(err)
}
challenge, err := service.Issue(IssueRequest{Purpose: PurposeExternalEmailBind, Account: "smail", Email: "user@example.com", ResourceID: 10}, "192.0.2.30")
if err != nil {
t.Fatal(err)
}
if err := service.VerifyBound(challenge.ID, "smail", "user@example.com", code, PurposeExternalEmailBind, 11, "192.0.2.30"); !errors.Is(err, ErrInvalidChallenge) {
t.Fatalf("cross-resource verification error = %v", err)
}
if err := service.VerifyBound(challenge.ID, "smail", "user@example.com", code, PurposeExternalEmailBind, 10, "192.0.2.30"); err != nil {
t.Fatalf("bound verification failed: %v", err)
}
}