142 lines
3.2 KiB
Go
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())
|
|
}
|
|
}
|