package auth import ( "context" "sync" "sync/atomic" "testing" "github.com/google/uuid" ) func TestCacheGetAddRemove(t *testing.T) { c := NewCache() if c.Ready() { t.Fatalf("fresh cache should not be Ready before Warm") } if c.Size() != 0 { t.Fatalf("fresh cache size = %d, want 0", c.Size()) } id := uuid.New() uid := uuid.New() s := Session{DeviceSessionID: id, UserID: uid, Status: SessionStatusActive} c.Add(s) if c.Size() != 1 { t.Fatalf("size after Add = %d, want 1", c.Size()) } got, ok := c.Get(id) if !ok || got.DeviceSessionID != id { t.Fatalf("Get after Add: ok=%v session=%+v", ok, got) } c.Remove(id) if c.Size() != 0 { t.Fatalf("size after Remove = %d, want 0", c.Size()) } if _, ok := c.Get(id); ok { t.Fatalf("Get after Remove returned a hit") } // Remove on already-evicted entry is a no-op. c.Remove(id) } func TestCacheRemoveByUser(t *testing.T) { c := NewCache() uid := uuid.New() other := uuid.New() c.Add(Session{DeviceSessionID: uuid.New(), UserID: uid, Status: SessionStatusActive}) c.Add(Session{DeviceSessionID: uuid.New(), UserID: uid, Status: SessionStatusActive}) c.Add(Session{DeviceSessionID: uuid.New(), UserID: other, Status: SessionStatusActive}) removed := c.RemoveByUser(uid) if len(removed) != 2 { t.Fatalf("RemoveByUser removed %d, want 2", len(removed)) } if c.Size() != 1 { t.Fatalf("size after RemoveByUser = %d, want 1", c.Size()) } if got := c.RemoveByUser(uid); got != nil { t.Fatalf("RemoveByUser on empty user returned %v, want nil", got) } } func TestCacheWarmFlipsReady(t *testing.T) { // Constructing a Cache and calling Warm against a Store without a real // database is awkward — the e2e test exercises Warm against Postgres. // Here we manually populate to confirm Ready toggles. c := NewCache() if c.Ready() { t.Fatalf("Ready before Warm") } // Simulate a successful Warm by setting ready and inserting via Add. c.ready.Store(true) if !c.Ready() { t.Fatalf("Ready did not flip after store") } } func TestCacheConcurrentGetAddRemove(t *testing.T) { c := NewCache() const writers = 4 const readers = 4 const opsPerWorker = 1000 uid := uuid.New() ids := make([]uuid.UUID, opsPerWorker) for i := range ids { ids[i] = uuid.New() } ctx, cancel := context.WithCancel(context.Background()) defer cancel() var stop atomic.Bool var wg sync.WaitGroup for range writers { wg.Add(1) go func() { defer wg.Done() for i := range opsPerWorker { if stop.Load() { return } c.Add(Session{DeviceSessionID: ids[i], UserID: uid, Status: SessionStatusActive}) c.Remove(ids[i]) } }() } for range readers { wg.Add(1) go func() { defer wg.Done() for i := range opsPerWorker { if stop.Load() { return } _, _ = c.Get(ids[i%len(ids)]) } }() } done := make(chan struct{}) go func() { wg.Wait(); close(done) }() select { case <-done: case <-ctx.Done(): stop.Store(true) <-done t.Fatalf("cache concurrency test timed out") } // After all goroutines finish, the cache must be empty (every Add // is paired with a Remove). if c.Size() != 0 { t.Fatalf("cache size after concurrent run = %d, want 0", c.Size()) } }