Files
minstrel/internal/eventbus/eventbus_test.go
T

192 lines
4.1 KiB
Go

package eventbus
import (
"sync"
"testing"
"time"
)
func TestPublish_DeliversToSubscriber(t *testing.T) {
t.Parallel()
b := New()
ch, unsub := b.Subscribe(4)
defer unsub()
b.Publish(Event{Kind: "test.event", UserID: "u1", Data: map[string]any{"x": 1}})
select {
case got := <-ch:
if got.Kind != "test.event" || got.UserID != "u1" {
t.Fatalf("unexpected event: %+v", got)
}
if got.Data["x"] != 1 {
t.Fatalf("payload mismatch: %+v", got.Data)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for event")
}
}
func TestPublish_FanOutToMultipleSubscribers(t *testing.T) {
t.Parallel()
b := New()
ch1, unsub1 := b.Subscribe(4)
ch2, unsub2 := b.Subscribe(4)
defer unsub1()
defer unsub2()
b.Publish(Event{Kind: "broadcast"})
for i, ch := range []<-chan Event{ch1, ch2} {
select {
case got := <-ch:
if got.Kind != "broadcast" {
t.Fatalf("ch%d: kind=%q", i, got.Kind)
}
case <-time.After(time.Second):
t.Fatalf("ch%d: timed out", i)
}
}
}
func TestPublish_DropsWhenSubscriberBufferFull(t *testing.T) {
t.Parallel()
b := New()
_, unsub := b.Subscribe(2) // tiny buffer
defer unsub()
// Without draining the channel, push more events than the buffer holds.
// Drops are silent; the assertion is that Publish does not block.
done := make(chan struct{})
go func() {
for i := 0; i < 100; i++ {
b.Publish(Event{Kind: "flood"})
}
close(done)
}()
select {
case <-done:
// Good — Publish returned without blocking on the laggard.
case <-time.After(2 * time.Second):
t.Fatal("Publish blocked on a slow subscriber")
}
}
func TestUnsubscribe_RemovesFromBusAndClosesChannel(t *testing.T) {
t.Parallel()
b := New()
ch, unsub := b.Subscribe(4)
if got := b.SubscriberCount(); got != 1 {
t.Fatalf("after Subscribe: SubscriberCount=%d, want 1", got)
}
unsub()
if got := b.SubscriberCount(); got != 0 {
t.Fatalf("after unsub: SubscriberCount=%d, want 0", got)
}
// Channel is closed: receive returns zero value with ok=false.
if _, ok := <-ch; ok {
t.Fatal("expected channel to be closed after unsubscribe")
}
}
func TestUnsubscribe_Idempotent(t *testing.T) {
t.Parallel()
b := New()
_, unsub := b.Subscribe(4)
unsub()
unsub() // must not panic / double-close
if got := b.SubscriberCount(); got != 0 {
t.Fatalf("SubscriberCount=%d, want 0", got)
}
}
func TestPublish_ConcurrentWritersAndSubscribers(t *testing.T) {
t.Parallel()
b := New()
const (
subs = 8
perWriter = 50
writers = 4
bufferSize = 100
)
var subWG sync.WaitGroup
subWG.Add(subs)
stops := make([]func(), 0, subs)
for i := 0; i < subs; i++ {
ch, unsub := b.Subscribe(bufferSize)
stops = append(stops, unsub)
go func() {
defer subWG.Done()
for range ch {
// Drain until channel closes.
}
}()
}
var pubWG sync.WaitGroup
pubWG.Add(writers)
for w := 0; w < writers; w++ {
go func() {
defer pubWG.Done()
for i := 0; i < perWriter; i++ {
b.Publish(Event{Kind: "concurrent"})
}
}()
}
pubWG.Wait()
// Tear down subscribers — channels close, goroutines exit.
for _, stop := range stops {
stop()
}
subWG.Wait()
}
// Close is what lets the server shut down with SSE clients connected: each
// stream returns when its channel closes.
func TestClose_EndsEverySubscription(t *testing.T) {
t.Parallel()
b := New()
ch1, unsub1 := b.Subscribe(4)
ch2, unsub2 := b.Subscribe(4)
b.Close()
for i, ch := range []<-chan Event{ch1, ch2} {
if _, ok := <-ch; ok {
t.Fatalf("subscriber %d: channel still open after Close", i)
}
}
if got := b.SubscriberCount(); got != 0 {
t.Fatalf("SubscriberCount=%d after Close, want 0", got)
}
// The handlers' deferred unsubscribes still run; they must not
// double-close.
unsub1()
unsub2()
b.Close()
}
func TestSubscribe_AfterCloseIsAlreadyClosed(t *testing.T) {
t.Parallel()
b := New()
b.Close()
ch, unsub := b.Subscribe(4)
defer unsub()
if _, ok := <-ch; ok {
t.Fatal("a subscription made after Close should already be closed")
}
if got := b.SubscriberCount(); got != 0 {
t.Fatalf("SubscriberCount=%d, want 0", got)
}
b.Publish(Event{Kind: "after.close"}) // must not panic
}