package tailhub import ( "sync" "testing" "time" ) func TestHub_PublishSubscribe(t *testing.T) { h := NewHub() s := h.Subscribe(Filter{}) defer s.Unsubscribe() ev := &Event{AlertID: "a1", CompanyID: "acme-001", SourceID: "prom-prod"} h.Publish(ev) select { case got := <-s.C: if got.AlertID != "a1" { t.Fatalf("got %q, want a1", got.AlertID) } case <-time.After(time.Second): t.Fatal("timeout waiting for event") } } func TestHub_CompanyFilter(t *testing.T) { h := NewHub() all := h.Subscribe(Filter{}) acme := h.Subscribe(Filter{CompanyID: "acme-001"}) defer all.Unsubscribe() defer acme.Unsubscribe() h.Publish(&Event{AlertID: "a1", CompanyID: "acme-001"}) h.Publish(&Event{AlertID: "g1", CompanyID: "globex-002"}) // all sees both got := map[string]bool{} for i := 0; i < 2; i++ { select { case ev := <-all.C: got[ev.AlertID] = true case <-time.After(time.Second): t.Fatal("timeout") } } if !got["a1"] || !got["g1"] { t.Fatalf("all subscriber missing events, got %v", got) } // acme sees only a1 select { case ev := <-acme.C: if ev.AlertID != "a1" { t.Fatalf("acme got %q, want a1", ev.AlertID) } case <-time.After(time.Second): t.Fatal("timeout") } select { case ev := <-acme.C: t.Fatalf("acme got unexpected %q (filter broken)", ev.AlertID) case <-time.After(50 * time.Millisecond): // expected: no more events } } func TestHub_DropsOnSlowConsumer(t *testing.T) { h := NewHub() s := h.Subscribe(Filter{}) defer s.Unsubscribe() // Publish more than the buffer can hold. Buffer is 64. for i := 0; i < 100; i++ { h.Publish(&Event{AlertID: "x"}) } // Drain — anything in the buffer is fine. We just need to // see Drops > 0 on the subscription. for i := 0; i < 64; i++ { select { case <-s.C: case <-time.After(time.Second): t.Fatalf("timeout at %d", i) } } // Allow the hub's lock-free drop path to register time.Sleep(10 * time.Millisecond) if s.Drops.Load() == 0 { t.Fatal("expected Drops > 0") } if stats := h.Stats(); stats.DroppedTotal == 0 { t.Fatal("expected hub.DroppedTotal > 0") } } func TestHub_UnsubscribeClosesChannel(t *testing.T) { h := NewHub() s := h.Subscribe(Filter{}) s.Unsubscribe() // second unsubscribe is a no-op s.Unsubscribe() // Channel should be closed select { case _, ok := <-s.C: if ok { t.Fatal("channel still open after Unsubscribe") } case <-time.After(time.Second): t.Fatal("channel not closed after Unsubscribe") } } func TestHub_Concurrent(t *testing.T) { // Race detector: many publishers and one subscriber. h := NewHub() s := h.Subscribe(Filter{}) defer s.Unsubscribe() const n = 1000 var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < n/10; j++ { h.Publish(&Event{AlertID: "x"}) } }() } // Drain concurrently drained := 0 done := make(chan struct{}) go func() { for { select { case <-s.C: drained++ case <-done: return } } }() wg.Wait() time.Sleep(50 * time.Millisecond) close(done) // We can't assert exact drained (some dropped), but it must // be > 0 and ≤ n. if drained == 0 { t.Fatal("drained=0 (consumer dead?)") } if stats := h.Stats(); stats.PublishedTotal != uint64(n) { t.Fatalf("PublishedTotal=%d, want %d", stats.PublishedTotal, n) } }