Files

156 lines
5.1 KiB
Go

package affinity
import (
"testing"
"time"
)
func TestCacheLearnsAndRefreshesSuccessfulTarget(t *testing.T) {
now := time.Date(2026, time.August, 11, 12, 0, 0, 0, time.UTC)
cache := newCache(2, time.Hour, func() time.Time { return now })
key := Key("one")
target := Target{GroupID: 1, CredentialID: 11, IdentityGeneration: 101}
miss := cache.Lookup(key)
if miss.Found() {
t.Fatalf("initial Lookup() = %#v, want miss", miss)
}
if !cache.RecordSuccess(key, miss, target) {
t.Fatal("RecordSuccess() = false, want insert")
}
first := cache.Lookup(key)
if !first.Found() || first.Target != target {
t.Fatalf("Lookup() = %#v, want target %#v", first, target)
}
now = now.Add(50 * time.Minute)
if !cache.RecordSuccess(key, first, target) {
t.Fatal("RecordSuccess() = false, want TTL refresh")
}
now = now.Add(20 * time.Minute)
if refreshed := cache.Lookup(key); !refreshed.Found() || refreshed.Target != target {
t.Fatalf("Lookup() after refreshed TTL = %#v, want hit", refreshed)
}
}
func TestCacheLookupDoesNotRefreshTTL(t *testing.T) {
now := time.Date(2026, time.August, 11, 12, 0, 0, 0, time.UTC)
cache := newCache(2, time.Hour, func() time.Time { return now })
key := Key("one")
target := Target{GroupID: 1, CredentialID: 11, IdentityGeneration: 101}
cache.RecordSuccess(key, Observation{}, target)
now = now.Add(50 * time.Minute)
if !cache.Lookup(key).Found() {
t.Fatal("Lookup() before expiry = miss, want hit")
}
now = now.Add(11 * time.Minute)
if got := cache.Lookup(key); got.Found() {
t.Fatalf("Lookup() after original expiry = %#v, want miss", got)
}
}
func TestCacheFirstSuccessWinsAndFallbackUsesCompareAndSwap(t *testing.T) {
cache := newCache(4, time.Hour, time.Now)
key := Key("conversation")
first := Target{GroupID: 1, CredentialID: 11, IdentityGeneration: 101}
second := Target{GroupID: 1, CredentialID: 12, IdentityGeneration: 102}
missOne := cache.Lookup(key)
missTwo := cache.Lookup(key)
if !cache.RecordSuccess(key, missOne, first) {
t.Fatal("first miss success did not create mapping")
}
if cache.RecordSuccess(key, missTwo, second) {
t.Fatal("second concurrent miss overwrote first-success mapping")
}
observedFirst := cache.Lookup(key)
if !cache.RecordSuccess(key, observedFirst, second) {
t.Fatal("fallback success did not switch observed mapping")
}
if cache.RecordSuccess(key, observedFirst, first) {
t.Fatal("stale success overwrote newer fallback mapping")
}
got := cache.Lookup(key)
if !got.Found() || got.Target != second {
t.Fatalf("Lookup() = %#v, want fallback target %#v", got, second)
}
}
func TestCacheEvictsLeastRecentlyUsedEntry(t *testing.T) {
cache := newCache(2, time.Hour, time.Now)
target := Target{GroupID: 1, CredentialID: 11, IdentityGeneration: 101}
for _, key := range []Key{"one", "two"} {
cache.RecordSuccess(key, Observation{}, target)
}
cache.Lookup("one")
cache.RecordSuccess("three", Observation{}, target)
if cache.Lookup("two").Found() {
t.Fatal("least recently used entry remained cached")
}
if !cache.Lookup("one").Found() || !cache.Lookup("three").Found() || cache.entryCount() != 2 {
t.Fatalf("cache state invalid after eviction; entries=%d", cache.entryCount())
}
}
func TestCacheRejectsInvalidInputs(t *testing.T) {
cache := newCache(2, time.Hour, time.Now)
validTarget := Target{GroupID: 1, CredentialID: 11, IdentityGeneration: 101}
for _, test := range []struct {
key Key
target Target
}{
{target: validTarget},
{key: "key"},
{key: "key", target: Target{GroupID: 1, CredentialID: 11}},
} {
if cache.RecordSuccess(test.key, Observation{}, test.target) {
t.Fatalf("RecordSuccess(%q, %#v) = true, want false", test.key, test.target)
}
}
if cache.Lookup("").Found() || cache.entryCount() != 0 {
t.Fatalf("invalid inputs changed cache; entries=%d", cache.entryCount())
}
}
func TestCacheConfigureClearsEntriesAndRejectsOlderRevision(t *testing.T) {
now := time.Date(2026, time.August, 12, 12, 0, 0, 0, time.UTC)
cache := newCache(2, time.Hour, func() time.Time { return now })
key := Key("one")
target := Target{GroupID: 1, CredentialID: 11, IdentityGeneration: 101}
if !cache.Configure(1, 2, time.Hour) {
t.Fatal("Configure(1) = false")
}
observed := cache.Lookup(key)
if !cache.RecordSuccess(key, observed, target) {
t.Fatal("RecordSuccess() = false")
}
stale := cache.Lookup(key)
if !cache.Configure(2, 1, 30*time.Minute) {
t.Fatal("Configure(2) = false")
}
if cache.Lookup(key).Found() || cache.entryCount() != 0 {
t.Fatal("new configuration did not clear old entries")
}
if cache.Configure(1, 2, time.Hour) {
t.Fatal("older configuration revision was accepted")
}
if cache.RecordSuccess(key, stale, target) {
t.Fatal("request from an older configuration revision rewrote the cache")
}
for _, nextKey := range []Key{"two", "three"} {
if !cache.RecordSuccess(nextKey, cache.Lookup(nextKey), target) {
t.Fatalf("RecordSuccess(%q) = false", nextKey)
}
}
if cache.Lookup("two").Found() || !cache.Lookup("three").Found() || cache.entryCount() != 1 {
t.Fatal("configured capacity was not enforced")
}
now = now.Add(31 * time.Minute)
if cache.Lookup("three").Found() {
t.Fatal("configured TTL was not enforced")
}
}