package main import ( "fmt" "testing" "time" ) // TestHubBounded churns random client-chosen sids and asserts the hub never // grows past its cap (P0.3a), while a session with a live stream survives the // eviction that makes room. func TestHubBounded(t *testing.T) { h := &Hub{sessions: make(map[string]*Session), max: 8} // no tickers needed live := h.getOrCreate("live-tab") ch := live.subscribe() defer live.unsubscribe(ch) for i := 0; i < 100; i++ { h.getOrCreate(fmt.Sprintf("sid-%d", i)) if n := h.count(); n > h.max { t.Fatalf("after %d sids: %d sessions, want <= %d", i+1, n, h.max) } } if n := h.count(); n != h.max { t.Fatalf("got %d sessions, want the cap %d", n, h.max) } if s := h.getOrCreate("live-tab"); s != live { t.Fatal("session with a live stream was evicted") } } // TestHubFullOfLiveSessions: when every stored session has a subscriber, a new // sid still gets a working session but the map refuses to grow. func TestHubFullOfLiveSessions(t *testing.T) { h := &Hub{sessions: make(map[string]*Session), max: 4} for i := 0; i < h.max; i++ { s := h.getOrCreate(fmt.Sprintf("live-%d", i)) defer s.unsubscribe(s.subscribe()) } s := h.getOrCreate("overflow") if s == nil { t.Fatal("getOrCreate returned nil at capacity") } if n := h.count(); n != h.max { t.Fatalf("got %d sessions, want %d (unregistered overflow)", n, h.max) } } // TestHubFrameWakesDirty: notify only marks the session dirty; the frame // ticker (not the write path) is what wakes the subscribed stream, clearing // the flag. func TestHubFrameWakesDirty(t *testing.T) { h := newHub(time.Millisecond, time.Minute) s := h.getOrCreate("tab") ch := s.subscribe() defer s.unsubscribe(ch) s.notify() select { case <-ch: case <-time.After(2 * time.Second): t.Fatal("frame ticker never woke the dirty session's stream") } if s.dirty.Load() { t.Fatal("dirty flag not cleared by the frame flush") } } // TestHubSweepEvictsIdle: a sweep within ttl keeps everything; past ttl it // reclaims the subscriber-less session but never one with a live stream. func TestHubSweepEvictsIdle(t *testing.T) { h := &Hub{sessions: make(map[string]*Session), ttl: time.Minute, max: maxSessions} // no tickers needed now := time.Now() h.getOrCreate("idle") live := h.getOrCreate("live") ch := live.subscribe() defer live.unsubscribe(ch) h.sweep(now.Add(30 * time.Second)) if n := h.count(); n != 2 { t.Fatalf("sweep within ttl evicted: %d sessions, want 2", n) } h.sweep(now.Add(2 * time.Minute)) h.mu.Lock() _, idleKept := h.sessions["idle"] _, liveKept := h.sessions["live"] h.mu.Unlock() if idleKept { t.Fatal("idle session survived a sweep past ttl") } if !liveKept { t.Fatal("session with a live stream was swept") } } // TestWriteLimiterPerIdentity: the bucket is keyed by login, so rotating sids // (i.e. fresh sessions) does not reset one login's write budget (P0.3b), while // a different login keeps its own. func TestWriteLimiterPerIdentity(t *testing.T) { l := newWriteLimiter() now := time.Now() // Each call simulates a request under a brand-new sid — the limiter only // ever sees the authenticated login, so the burst is spent exactly once. for i := 0; i < int(writeBurst); i++ { if !l.allow("alice", now) { t.Fatalf("write %d within burst denied", i+1) } } if l.allow("alice", now) { t.Fatal("write over burst allowed despite sid rotation") } if !l.allow("bob", now) { t.Fatal("independent login denied by alice's spent budget") } // Refill: one token accrues after 1/writeRate seconds. later := now.Add(time.Duration(float64(time.Second) / writeRate)) if !l.allow("alice", later) { t.Fatal("refilled token denied") } if l.allow("alice", later) { t.Fatal("second write allowed after a single-token refill") } }