package affinity import ( "testing" "time" ) func TestTableKeepsBindingsIsolatedAndExpiresThem(t *testing.T) { t.Parallel() now := time.Date(2026, time.August, 2, 9, 0, 0, 0, time.UTC) table, err := NewTable(Options{ MaxEntries: 2, Now: func() time.Time { return now }, }) if err != nil { t.Fatalf("NewTable() error = %v", err) } first, err := NewKey("client-a", "route-a", "session-a") if err != nil { t.Fatalf("NewKey(first) error = %v", err) } second, err := NewKey("client-b", "route-a", "session-a") if err != nil { t.Fatalf("NewKey(second) error = %v", err) } if first == second { t.Fatal("same session under distinct clients must have different affinity keys") } table.Bind(first, "proxy-a", now.Add(time.Minute)) table.Bind(second, "proxy-b", now.Add(time.Minute)) if binding, ok := table.Lookup(first); !ok || binding.ProxyID != "proxy-a" { t.Fatalf("Lookup(first) = (%+v, %v), want proxy-a", binding, ok) } now = now.Add(time.Minute) if _, ok := table.Lookup(first); ok { t.Fatal("Lookup(first) retained an expired binding") } } func TestTableEvictsLeastRecentlyUsedBindingAtCapacity(t *testing.T) { t.Parallel() now := time.Date(2026, time.August, 2, 9, 0, 0, 0, time.UTC) table, err := NewTable(Options{ MaxEntries: 2, Now: func() time.Time { return now }, }) if err != nil { t.Fatalf("NewTable() error = %v", err) } first := mustKey(t, "client-a", "route-a", "session-a") second := mustKey(t, "client-a", "route-a", "session-b") third := mustKey(t, "client-a", "route-a", "session-c") expiresAt := now.Add(time.Minute) table.Bind(first, "proxy-a", expiresAt) table.Bind(second, "proxy-b", expiresAt) if _, ok := table.Lookup(first); !ok { t.Fatal("Lookup(first) must retain the binding") } table.Bind(third, "proxy-c", expiresAt) if _, ok := table.Lookup(second); ok { t.Fatal("Lookup(second) retained least recently used binding after capacity eviction") } if binding, ok := table.Lookup(first); !ok || binding.ProxyID != "proxy-a" { t.Fatalf("Lookup(first) = (%+v, %v), want proxy-a", binding, ok) } if binding, ok := table.Lookup(third); !ok || binding.ProxyID != "proxy-c" { t.Fatalf("Lookup(third) = (%+v, %v), want proxy-c", binding, ok) } } func TestNewKeyRejectsInvalidInputs(t *testing.T) { t.Parallel() for _, test := range []struct { name string clientID string routing string sessionID string }{ {name: "empty client", routing: "route-a", sessionID: "session-a"}, {name: "empty routing", clientID: "client-a", sessionID: "session-a"}, {name: "empty session", clientID: "client-a", routing: "route-a"}, {name: "oversized session", clientID: "client-a", routing: "route-a", sessionID: string(make([]byte, 129))}, {name: "space session", clientID: "client-a", routing: "route-a", sessionID: "session a"}, } { t.Run(test.name, func(t *testing.T) { t.Parallel() if _, err := NewKey(test.clientID, test.routing, test.sessionID); err == nil { t.Fatal("NewKey() error = nil, want invalid input") } }) } } func mustKey(t *testing.T, clientID, routing, sessionID string) Key { t.Helper() key, err := NewKey(clientID, routing, sessionID) if err != nil { t.Fatalf("NewKey() error = %v", err) } return key }