mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-06-02 17:40:14 +00:00
tests: add cache store tests
This commit is contained in:
@@ -62,6 +62,15 @@ binary-linux-arm64:
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Go vet
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
# Go race
|
||||
test-race:
|
||||
go test -race ./...
|
||||
|
||||
# Development
|
||||
dev:
|
||||
docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCacheStoreGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(cs *CacheStore[string])
|
||||
wantValue string
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: "returns a stored value",
|
||||
setup: func(cs *CacheStore[string]) { cs.Set("key", "value", 0) },
|
||||
wantValue: "value",
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "reports a missing key",
|
||||
setup: func(cs *CacheStore[string]) {},
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "returns the latest value after an overwrite",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("key", "first", 0)
|
||||
cs.Set("key", "second", 0)
|
||||
},
|
||||
wantValue: "second",
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "returns a non-expired entry",
|
||||
setup: func(cs *CacheStore[string]) { cs.Set("key", "value", time.Minute) },
|
||||
wantValue: "value",
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "treats an expired entry as missing",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("key", "value", 10*time.Millisecond)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
},
|
||||
wantOk: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs := NewCacheStore[string](0)
|
||||
tt.setup(cs)
|
||||
|
||||
value, ok := cs.Get("key")
|
||||
assert.Equal(t, tt.wantOk, ok)
|
||||
if tt.wantOk {
|
||||
assert.Equal(t, tt.wantValue, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheStoreUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(cs *CacheStore[string])
|
||||
ttl time.Duration
|
||||
wantOk bool
|
||||
afterWait time.Duration
|
||||
wantPresent bool
|
||||
wantValue string
|
||||
}{
|
||||
{
|
||||
name: "updates an existing entry",
|
||||
setup: func(cs *CacheStore[string]) { cs.Set("key", "old", 0) },
|
||||
ttl: 0,
|
||||
wantOk: true,
|
||||
wantPresent: true,
|
||||
wantValue: "new",
|
||||
},
|
||||
{
|
||||
name: "does not create a missing entry",
|
||||
setup: func(cs *CacheStore[string]) {},
|
||||
ttl: 0,
|
||||
wantOk: false,
|
||||
wantPresent: false,
|
||||
},
|
||||
{
|
||||
name: "preserves the existing expiry when ttl is zero",
|
||||
setup: func(cs *CacheStore[string]) { cs.Set("key", "old", 30*time.Millisecond) },
|
||||
ttl: 0,
|
||||
wantOk: true,
|
||||
afterWait: 40 * time.Millisecond,
|
||||
wantPresent: false,
|
||||
},
|
||||
{
|
||||
name: "refreshes the expiry when ttl is provided",
|
||||
setup: func(cs *CacheStore[string]) { cs.Set("key", "old", 10*time.Millisecond) },
|
||||
ttl: time.Minute,
|
||||
wantOk: true,
|
||||
afterWait: 20 * time.Millisecond,
|
||||
wantPresent: true,
|
||||
wantValue: "new",
|
||||
},
|
||||
{
|
||||
name: "does not update an expired entry",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("key", "old", 10*time.Millisecond)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
},
|
||||
ttl: time.Minute,
|
||||
wantOk: false,
|
||||
wantPresent: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs := NewCacheStore[string](0)
|
||||
tt.setup(cs)
|
||||
|
||||
ok := cs.Update("key", "new", tt.ttl)
|
||||
assert.Equal(t, tt.wantOk, ok)
|
||||
|
||||
time.Sleep(tt.afterWait)
|
||||
|
||||
value, present := cs.Get("key")
|
||||
assert.Equal(t, tt.wantPresent, present)
|
||||
if tt.wantPresent {
|
||||
assert.Equal(t, tt.wantValue, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheStoreDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(cs *CacheStore[string])
|
||||
key string
|
||||
wantSize int
|
||||
}{
|
||||
{
|
||||
name: "removes an existing key",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("a", "1", 0)
|
||||
cs.Set("b", "2", 0)
|
||||
},
|
||||
key: "a",
|
||||
wantSize: 1,
|
||||
},
|
||||
{
|
||||
name: "is a no-op for a missing key",
|
||||
setup: func(cs *CacheStore[string]) { cs.Set("a", "1", 0) },
|
||||
key: "missing",
|
||||
wantSize: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs := NewCacheStore[string](0)
|
||||
tt.setup(cs)
|
||||
|
||||
cs.Delete(tt.key)
|
||||
|
||||
_, ok := cs.Get(tt.key)
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, tt.wantSize, cs.Size())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheStoreSweep(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(cs *CacheStore[string])
|
||||
present []string
|
||||
absent []string
|
||||
wantSize int
|
||||
}{
|
||||
{
|
||||
name: "removes expired entries and keeps the rest",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("permanent", "value", 0)
|
||||
cs.Set("expired", "value", 10*time.Millisecond)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
},
|
||||
present: []string{"permanent"},
|
||||
absent: []string{"expired"},
|
||||
wantSize: 1,
|
||||
},
|
||||
{
|
||||
name: "keeps all live entries",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("a", "value", 0)
|
||||
cs.Set("b", "value", time.Minute)
|
||||
},
|
||||
present: []string{"a", "b"},
|
||||
wantSize: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs := NewCacheStore[string](0)
|
||||
tt.setup(cs)
|
||||
|
||||
cs.Sweep()
|
||||
|
||||
for _, key := range tt.present {
|
||||
_, ok := cs.Get(key)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
for _, key := range tt.absent {
|
||||
_, ok := cs.Get(key)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
assert.Equal(t, tt.wantSize, cs.Size())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheStoreEviction(t *testing.T) {
|
||||
// Every case uses a cache with maxSize 2; the final Set in setup is the
|
||||
// insertion that overflows the cache and triggers an eviction.
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(cs *CacheStore[string])
|
||||
present []string
|
||||
absent []string
|
||||
wantSize int
|
||||
}{
|
||||
{
|
||||
name: "evicts an already expired entry first",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("expired", "value", 10*time.Millisecond)
|
||||
cs.Set("fresh", "value", time.Minute)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
cs.Set("new", "value", time.Minute)
|
||||
},
|
||||
present: []string{"fresh", "new"},
|
||||
absent: []string{"expired"},
|
||||
wantSize: 2,
|
||||
},
|
||||
{
|
||||
name: "evicts the entry expiring soonest",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("soon", "value", 50*time.Millisecond)
|
||||
cs.Set("later", "value", time.Hour)
|
||||
cs.Set("new", "value", time.Hour)
|
||||
},
|
||||
present: []string{"later", "new"},
|
||||
absent: []string{"soon"},
|
||||
wantSize: 2,
|
||||
},
|
||||
{
|
||||
name: "evicts the oldest inserted entry when none have a ttl",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("first", "value", 0)
|
||||
cs.Set("second", "value", 0)
|
||||
cs.Set("third", "value", 0)
|
||||
},
|
||||
present: []string{"second", "third"},
|
||||
absent: []string{"first"},
|
||||
wantSize: 2,
|
||||
},
|
||||
{
|
||||
name: "overwriting an existing key does not trigger eviction",
|
||||
setup: func(cs *CacheStore[string]) {
|
||||
cs.Set("a", "1", 0)
|
||||
cs.Set("b", "2", 0)
|
||||
cs.Set("a", "updated", 0)
|
||||
},
|
||||
present: []string{"a", "b"},
|
||||
wantSize: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs := NewCacheStore[string](2)
|
||||
tt.setup(cs)
|
||||
|
||||
for _, key := range tt.present {
|
||||
_, ok := cs.Get(key)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
for _, key := range tt.absent {
|
||||
_, ok := cs.Get(key)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
assert.Equal(t, tt.wantSize, cs.Size())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheStoreSizeAndClear(t *testing.T) {
|
||||
cs := NewCacheStore[string](0)
|
||||
assert.Equal(t, 0, cs.Size())
|
||||
|
||||
cs.Set("a", "1", 0)
|
||||
cs.Set("b", "2", 0)
|
||||
assert.Equal(t, 2, cs.Size())
|
||||
|
||||
cs.Clear()
|
||||
assert.Equal(t, 0, cs.Size())
|
||||
|
||||
_, ok := cs.Get("a")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestCacheStoreWithLock(t *testing.T) {
|
||||
cs := NewCacheStore[int](0)
|
||||
cs.Set("counter", 1, 0)
|
||||
|
||||
// All four actions run atomically under a single lock.
|
||||
cs.WithLock(func(actions CacheStoreActions[int]) {
|
||||
current, ok := actions.Get("counter")
|
||||
assert.True(t, ok)
|
||||
|
||||
actions.Set("counter", current+1, 0)
|
||||
actions.Set("other", 100, 0)
|
||||
actions.Delete("counter")
|
||||
|
||||
updated := actions.Update("other", 200, 0)
|
||||
assert.True(t, updated)
|
||||
})
|
||||
|
||||
_, ok := cs.Get("counter")
|
||||
assert.False(t, ok)
|
||||
|
||||
value, ok := cs.Get("other")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 200, value)
|
||||
}
|
||||
|
||||
// TestCacheStoreConcurrency exercises every locking path concurrently so the
|
||||
// race detector (go test -race) can flag unsynchronised access.
|
||||
func TestCacheStoreConcurrency(t *testing.T) {
|
||||
cs := NewCacheStore[int](64)
|
||||
|
||||
const goroutines = 16
|
||||
const iterations = 200
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
|
||||
for g := range goroutines {
|
||||
go func(g int) {
|
||||
defer wg.Done()
|
||||
for i := range iterations {
|
||||
key := strconv.Itoa((g*iterations + i) % 32)
|
||||
switch i % 6 {
|
||||
case 0:
|
||||
cs.Set(key, i, time.Minute)
|
||||
case 1:
|
||||
cs.Get(key)
|
||||
case 2:
|
||||
cs.Update(key, i, time.Minute)
|
||||
case 3:
|
||||
cs.Delete(key)
|
||||
case 4:
|
||||
cs.Size()
|
||||
case 5:
|
||||
cs.WithLock(func(actions CacheStoreActions[int]) {
|
||||
if v, ok := actions.Get(key); ok {
|
||||
actions.Set(key, v+1, time.Minute)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user