Bootstrap copied BFF and local accounts with public Sendico contracts (#34)
This commit is contained in:
1 parent
67dc5b5b1e
commit
7062672a87
556 files changed
+42291
-777
No files matched your search
@@ -0,0 +1,171 @@
|
||||
package verification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestReserveIssuanceEnforcesThreshold(t *testing.T) {
|
||||
now := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC)
|
||||
store := newMemoryIssuanceStore()
|
||||
db := &verificationDB{limits: store}
|
||||
request := &Request{IssuanceLimits: []IssuanceLimit{
|
||||
NewIssuanceLimit("destination", "login\x00user@example.com", 2, time.Hour, now),
|
||||
}}
|
||||
|
||||
for attempt := 1; attempt <= 2; attempt++ {
|
||||
if err := db.reserveIssuance(context.Background(), request, now); err != nil {
|
||||
t.Fatalf("attempt %d was unexpectedly rejected: %v", attempt, err)
|
||||
}
|
||||
}
|
||||
err := db.reserveIssuance(context.Background(), request, now)
|
||||
if !errors.Is(err, ErrIssuanceRateLimited) {
|
||||
t.Fatalf("expected rate-limit error, got %v", err)
|
||||
}
|
||||
details, ok := Throttle(err)
|
||||
if !ok || details.ScopeKind != "destination" || details.RetryAfter != time.Hour {
|
||||
t.Fatalf("unexpected throttle details: %+v, ok=%v", details, ok)
|
||||
}
|
||||
|
||||
nextWindow := &Request{IssuanceLimits: []IssuanceLimit{
|
||||
NewIssuanceLimit("destination", "login\x00user@example.com", 2, time.Hour, now.Add(time.Hour)),
|
||||
}}
|
||||
if err := db.reserveIssuance(context.Background(), nextWindow, now.Add(time.Hour)); err != nil {
|
||||
t.Fatalf("first request in the next window was rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWithResultDoesNotMutateTokensWhenIssuanceDenied(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
store := newMemoryIssuanceStore()
|
||||
db := newTestVerificationDB(t).WithIssuanceLimits(store)
|
||||
accountRef := bson.NewObjectID()
|
||||
limit := NewIssuanceLimit("destination", "login\x00user@example.com", 1, time.Hour, now)
|
||||
|
||||
first, err := db.CreateWithResult(context.Background(), NewOTPRequest(
|
||||
accountRef,
|
||||
model.PurposeLogin,
|
||||
"user@example.com",
|
||||
).WithIdempotencyKey("issuance-first").WithIssuanceLimits(limit))
|
||||
if err != nil || !first.Created {
|
||||
t.Fatalf("first token creation failed: result=%+v err=%v", first, err)
|
||||
}
|
||||
|
||||
_, err = db.CreateWithResult(context.Background(), NewOTPRequest(
|
||||
accountRef,
|
||||
model.PurposeLogin,
|
||||
"user@example.com",
|
||||
).WithIdempotencyKey("issuance-second").WithIssuanceLimits(limit))
|
||||
if !errors.Is(err, ErrIssuanceRateLimited) {
|
||||
t.Fatalf("expected second token creation to be rate-limited, got %v", err)
|
||||
}
|
||||
|
||||
repo, ok := db.Repository.(*memoryTokenRepository)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected verification repository type %T", db.Repository)
|
||||
}
|
||||
snapshot := repo.snapshot()
|
||||
if len(snapshot.data) != 1 {
|
||||
t.Fatalf("rate-limited request changed token count: %d", len(snapshot.data))
|
||||
}
|
||||
for _, token := range snapshot.data {
|
||||
if token.UsedAt != nil {
|
||||
t.Fatal("rate-limited request invalidated the existing token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReserveIssuanceIsSharedAcrossInstances(t *testing.T) {
|
||||
now := time.Date(2026, 8, 13, 12, 15, 0, 0, time.UTC)
|
||||
store := newMemoryIssuanceStore()
|
||||
dbs := []*verificationDB{{limits: store}, {limits: store}}
|
||||
request := &Request{IssuanceLimits: []IssuanceLimit{
|
||||
NewIssuanceLimit("ip", "login\x00192.0.2.10", 8, time.Hour, now),
|
||||
}}
|
||||
|
||||
var allowed atomic.Int32
|
||||
var denied atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
for attempt := 0; attempt < 40; attempt++ {
|
||||
wg.Add(1)
|
||||
go func(db *verificationDB) {
|
||||
defer wg.Done()
|
||||
err := db.reserveIssuance(context.Background(), request, now)
|
||||
switch {
|
||||
case err == nil:
|
||||
allowed.Add(1)
|
||||
case errors.Is(err, ErrIssuanceRateLimited):
|
||||
denied.Add(1)
|
||||
default:
|
||||
t.Errorf("unexpected issuance error: %v", err)
|
||||
}
|
||||
}(dbs[attempt%len(dbs)])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if got := allowed.Load(); got != 8 {
|
||||
t.Fatalf("expected exactly 8 allowed reservations, got %d", got)
|
||||
}
|
||||
if got := denied.Load(); got != 32 {
|
||||
t.Fatalf("expected 32 denied reservations, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReserveIssuanceFailsClosedWithoutLeakingStorageError(t *testing.T) {
|
||||
now := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC)
|
||||
cause := merrors.Define("restricted verification destination")
|
||||
db := &verificationDB{limits: &memoryIssuanceStore{err: cause}}
|
||||
request := &Request{IssuanceLimits: []IssuanceLimit{
|
||||
NewIssuanceLimit("destination", "login\x00secret@example.com", 2, time.Hour, now),
|
||||
}}
|
||||
|
||||
err := db.reserveIssuance(context.Background(), request, now)
|
||||
if !errors.Is(err, merrors.ErrServiceUnavailable) || !errors.Is(err, cause) {
|
||||
t.Fatalf("expected opaque service-unavailable error preserving cause, got %v", err)
|
||||
}
|
||||
if got := err.Error(); got != "serviceUnavailable: failed to reserve verification issuance capacity" {
|
||||
t.Fatalf("storage error leaked or changed: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type memoryIssuanceStore struct {
|
||||
mu sync.Mutex
|
||||
counts map[string]int
|
||||
err error
|
||||
}
|
||||
|
||||
func newMemoryIssuanceStore() *memoryIssuanceStore {
|
||||
return &memoryIssuanceStore{counts: make(map[string]int)}
|
||||
}
|
||||
|
||||
func (s *memoryIssuanceStore) Current(_ context.Context, scopeKind, scopeHash string, windowStart time.Time) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.err != nil {
|
||||
return 0, s.err
|
||||
}
|
||||
return s.counts[issuanceStoreKey(scopeKind, scopeHash, windowStart)], nil
|
||||
}
|
||||
|
||||
func (s *memoryIssuanceStore) Increment(_ context.Context, scopeKind, scopeHash string, windowStart, _ time.Time) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.err != nil {
|
||||
return 0, s.err
|
||||
}
|
||||
key := issuanceStoreKey(scopeKind, scopeHash, windowStart)
|
||||
s.counts[key]++
|
||||
return s.counts[key], nil
|
||||
}
|
||||
|
||||
func issuanceStoreKey(scopeKind, scopeHash string, windowStart time.Time) string {
|
||||
return scopeKind + "\x00" + scopeHash + "\x00" + windowStart.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
Reference in new issue
Block a user