Files
galaxy-game/backend/internal/auth/cache_test.go
T
2026-05-06 10:14:55 +03:00

142 lines
3.2 KiB
Go

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())
}
}