205 lines
6.0 KiB
Go
205 lines
6.0 KiB
Go
package session_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/gateway/internal/session"
|
|
)
|
|
|
|
// stubLookup is the BackendLookup test fake. lookups counts hits;
|
|
// records is the canonical source of truth keyed by device_session_id.
|
|
type stubLookup struct {
|
|
mu sync.Mutex
|
|
records map[string]session.Record
|
|
hits atomic.Int64
|
|
notFound bool
|
|
}
|
|
|
|
func newStubLookup() *stubLookup {
|
|
return &stubLookup{records: make(map[string]session.Record)}
|
|
}
|
|
|
|
func (s *stubLookup) put(rec session.Record) {
|
|
s.mu.Lock()
|
|
s.records[rec.DeviceSessionID] = rec
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *stubLookup) LookupSession(_ context.Context, deviceSessionID string) (session.Record, error) {
|
|
s.hits.Add(1)
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.notFound {
|
|
return session.Record{}, session.ErrNotFound
|
|
}
|
|
rec, ok := s.records[deviceSessionID]
|
|
if !ok {
|
|
return session.Record{}, session.ErrNotFound
|
|
}
|
|
return rec, nil
|
|
}
|
|
|
|
func TestMemoryCacheLookupHitsCacheAfterFirstFetch(t *testing.T) {
|
|
stub := newStubLookup()
|
|
stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
|
|
|
|
cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{
|
|
MaxEntries: 10,
|
|
TTL: time.Hour,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryCache: %v", err)
|
|
}
|
|
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("first lookup: %v", err)
|
|
}
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("second lookup: %v", err)
|
|
}
|
|
if got := stub.hits.Load(); got != 1 {
|
|
t.Fatalf("backend hits = %d, want 1 (cache should serve the second call)", got)
|
|
}
|
|
}
|
|
|
|
func TestMemoryCacheLookupRefreshesOnTTLExpiry(t *testing.T) {
|
|
stub := newStubLookup()
|
|
stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
|
|
|
|
clock := time.Unix(1_000_000, 0)
|
|
now := func() time.Time { return clock }
|
|
|
|
cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{
|
|
MaxEntries: 10,
|
|
TTL: 100 * time.Millisecond,
|
|
Now: now,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryCache: %v", err)
|
|
}
|
|
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("first lookup: %v", err)
|
|
}
|
|
clock = clock.Add(200 * time.Millisecond)
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("post-TTL lookup: %v", err)
|
|
}
|
|
if got := stub.hits.Load(); got != 2 {
|
|
t.Fatalf("backend hits = %d, want 2 (TTL expiry should refetch)", got)
|
|
}
|
|
}
|
|
|
|
func TestMemoryCacheMarkRevokedFlipsCachedRecord(t *testing.T) {
|
|
stub := newStubLookup()
|
|
stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
|
|
|
|
cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 10, TTL: time.Hour})
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryCache: %v", err)
|
|
}
|
|
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("first lookup: %v", err)
|
|
}
|
|
cache.MarkRevoked("a")
|
|
rec, err := cache.Lookup(context.Background(), "a")
|
|
if err != nil {
|
|
t.Fatalf("post-revoke lookup: %v", err)
|
|
}
|
|
if rec.Status != session.StatusRevoked {
|
|
t.Fatalf("status = %q, want %q", rec.Status, session.StatusRevoked)
|
|
}
|
|
if got := stub.hits.Load(); got != 1 {
|
|
t.Fatalf("backend hits = %d, want 1 (MarkRevoked must not refetch)", got)
|
|
}
|
|
}
|
|
|
|
func TestMemoryCacheMarkAllRevokedForUserFlipsAllSessions(t *testing.T) {
|
|
stub := newStubLookup()
|
|
stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
|
|
stub.put(session.Record{DeviceSessionID: "b", UserID: "u1", Status: session.StatusActive})
|
|
stub.put(session.Record{DeviceSessionID: "c", UserID: "u2", Status: session.StatusActive})
|
|
|
|
cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 10, TTL: time.Hour})
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryCache: %v", err)
|
|
}
|
|
for _, id := range []string{"a", "b", "c"} {
|
|
if _, err := cache.Lookup(context.Background(), id); err != nil {
|
|
t.Fatalf("seed %s: %v", id, err)
|
|
}
|
|
}
|
|
|
|
cache.MarkAllRevokedForUser("u1")
|
|
|
|
for _, id := range []string{"a", "b"} {
|
|
rec, err := cache.Lookup(context.Background(), id)
|
|
if err != nil {
|
|
t.Fatalf("post-revoke lookup %s: %v", id, err)
|
|
}
|
|
if rec.Status != session.StatusRevoked {
|
|
t.Fatalf("session %s status = %q, want revoked", id, rec.Status)
|
|
}
|
|
}
|
|
rec, err := cache.Lookup(context.Background(), "c")
|
|
if err != nil {
|
|
t.Fatalf("post-revoke lookup c: %v", err)
|
|
}
|
|
if rec.Status != session.StatusActive {
|
|
t.Fatalf("session c status = %q, want active (other user)", rec.Status)
|
|
}
|
|
}
|
|
|
|
func TestMemoryCacheLRUEvictsLeastRecentlyUsed(t *testing.T) {
|
|
stub := newStubLookup()
|
|
stub.put(session.Record{DeviceSessionID: "a", UserID: "u1", Status: session.StatusActive})
|
|
stub.put(session.Record{DeviceSessionID: "b", UserID: "u2", Status: session.StatusActive})
|
|
stub.put(session.Record{DeviceSessionID: "c", UserID: "u3", Status: session.StatusActive})
|
|
|
|
cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 2, TTL: time.Hour})
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryCache: %v", err)
|
|
}
|
|
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("seed a: %v", err)
|
|
}
|
|
if _, err := cache.Lookup(context.Background(), "b"); err != nil {
|
|
t.Fatalf("seed b: %v", err)
|
|
}
|
|
if _, err := cache.Lookup(context.Background(), "c"); err != nil {
|
|
t.Fatalf("seed c: %v", err)
|
|
}
|
|
if got := cache.Len(); got != 2 {
|
|
t.Fatalf("Len = %d, want 2", got)
|
|
}
|
|
|
|
hitsBefore := stub.hits.Load()
|
|
if _, err := cache.Lookup(context.Background(), "a"); err != nil {
|
|
t.Fatalf("re-lookup a: %v", err)
|
|
}
|
|
if got := stub.hits.Load(); got != hitsBefore+1 {
|
|
t.Fatalf("backend hits = %d, want +1 (a was evicted)", got-hitsBefore)
|
|
}
|
|
}
|
|
|
|
func TestMemoryCachePropagatesBackendNotFound(t *testing.T) {
|
|
stub := newStubLookup()
|
|
stub.notFound = true
|
|
|
|
cache, err := session.NewMemoryCache(stub, session.MemoryCacheOptions{MaxEntries: 4, TTL: time.Hour})
|
|
if err != nil {
|
|
t.Fatalf("NewMemoryCache: %v", err)
|
|
}
|
|
_, err = cache.Lookup(context.Background(), "missing")
|
|
if !errors.Is(err, session.ErrNotFound) {
|
|
t.Fatalf("Lookup error = %v, want ErrNotFound", err)
|
|
}
|
|
}
|