package membership import ( "strconv" "testing" "time" ) // TestNonceCacheRememberPrune covers the replay/expiry behavior directly on the // cache: a fresh nonce is accepted (golden), an immediate repeat is rejected // (error), and after the TTL the same nonce is accepted again because its entry // was pruned (edge). func TestNonceCacheRememberPrune(t *testing.T) { nc := newMemNonceCache(50*time.Millisecond, 1000) base := time.Now() if !nc.rememberOrReject("a", base) { t.Fatalf("first sighting should be accepted") } if nc.rememberOrReject("a", base) { t.Fatalf("an immediate replay should be rejected") } if !nc.rememberOrReject("a", base.Add(60*time.Millisecond)) { t.Fatalf("after the TTL the nonce should be accepted again (pruned)") } } // TestNonceCacheCapBounded covers the memory bound (audit H7): with a long TTL so // nothing expires, inserting far more nonces than the cap must still keep the // cache at or under the cap (oldest evicted), and the order queue must not drift // from the map. func TestNonceCacheCapBounded(t *testing.T) { const capacity = 100 nc := newMemNonceCache(time.Hour, capacity) base := time.Now() for i := 0; i < 500; i++ { nc.rememberOrReject("n"+strconv.Itoa(i), base) } nc.mu.Lock() size := len(nc.seen) orderLen := len(nc.order) nc.mu.Unlock() if size > capacity { t.Fatalf("cache exceeded its cap: %d > %d", size, capacity) } if orderLen != size { t.Fatalf("order queue drifted from the map: order=%d seen=%d", orderLen, size) } }