290874720f
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 46s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
The suspension gate runs CurrentSuspension on every authenticated request. Add a write-through in-memory cache on account.Store keyed by account id, invalidated on Suspend/LiftSuspension, with the cached entry re-evaluated against the wall clock so a temporary block lapses without an explicit invalidation. Single-instance, matching the deployment (one shared Store). Keeps the gate off the database on the hot path while a block still takes effect on the next request.
53 lines
1.5 KiB
Go
53 lines
1.5 KiB
Go
package account
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestSuspensionActiveAt covers the wall-clock re-evaluation the cache relies on: a permanent
|
|
// block is always active, a future-dated one is active, a past-dated one is not.
|
|
func TestSuspensionActiveAt(t *testing.T) {
|
|
now := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
|
|
future := now.Add(time.Hour)
|
|
past := now.Add(-time.Hour)
|
|
|
|
if !suspensionActiveAt(Suspension{BlockedUntil: nil}, now) {
|
|
t.Error("a permanent block must be active")
|
|
}
|
|
if !suspensionActiveAt(Suspension{BlockedUntil: &future}, now) {
|
|
t.Error("a future-dated block must be active")
|
|
}
|
|
if suspensionActiveAt(Suspension{BlockedUntil: &past}, now) {
|
|
t.Error("a past-dated block must be inactive")
|
|
}
|
|
}
|
|
|
|
// TestSuspensionCache covers the cache primitives: a miss on an empty cache, a hit after put for
|
|
// both the not-blocked and blocked markers, and a miss after invalidate.
|
|
func TestSuspensionCache(t *testing.T) {
|
|
c := newSuspensionCache()
|
|
id := uuid.New()
|
|
|
|
if _, ok := c.get(id); ok {
|
|
t.Fatal("empty cache must miss")
|
|
}
|
|
|
|
c.put(id, suspensionCacheEntry{found: false})
|
|
if e, ok := c.get(id); !ok || e.found {
|
|
t.Fatalf("cached not-blocked entry = (%+v, ok %v), want hit with found=false", e, ok)
|
|
}
|
|
|
|
c.put(id, suspensionCacheEntry{susp: Suspension{AccountID: id}, found: true})
|
|
if e, ok := c.get(id); !ok || !e.found {
|
|
t.Fatalf("cached blocked entry = (%+v, ok %v), want hit with found=true", e, ok)
|
|
}
|
|
|
|
c.invalidate(id)
|
|
if _, ok := c.get(id); ok {
|
|
t.Fatal("invalidated entry must miss")
|
|
}
|
|
}
|