package notify import ( "context" "testing" "time" "github.com/google/uuid" ) func TestStreamHubSubscribeReceivesPublish(t *testing.T) { hub := NewStreamHub() uid := uuid.New() ch, cancel := hub.Subscribe(uid) defer cancel() want := Message{ID: uuid.New(), UserID: uid, Topic: "t", Title: "hi"} hub.Publish(uid, want) select { case got := <-ch: if got.ID != want.ID { t.Fatalf("got %v, want %v", got.ID, want.ID) } case <-time.After(time.Second): t.Fatal("did not receive published message") } } func TestStreamHubUnsubscribeStopsDelivery(t *testing.T) { hub := NewStreamHub() uid := uuid.New() ch, cancel := hub.Subscribe(uid) cancel() // immediate unsubscribe closes the channel // channel should be closed, draining returns zero value + !ok. if _, ok := <-ch; ok { t.Fatal("expected channel closed after cancel") } // Publishing to an unsubscribed user must not panic. hub.Publish(uid, Message{UserID: uid}) } func TestStreamHubDropsSlowSubscriber(t *testing.T) { hub := NewStreamHub() uid := uuid.New() ch, cancel := hub.Subscribe(uid) defer cancel() // Flood beyond buffer; Publish must not block / deadlock. done := make(chan struct{}) go func() { for i := 0; i < streamSubBuffer*4; i++ { hub.Publish(uid, Message{UserID: uid, Topic: "flood"}) } close(done) }() select { case <-done: case <-time.After(2 * time.Second): t.Fatal("Publish deadlocked on slow subscriber") } // Buffer holds at most streamSubBuffer; the rest were dropped. count := 0 for { select { case <-ch: count++ default: if count > streamSubBuffer { t.Fatalf("buffered %d > cap %d (drop-on-slow violated)", count, streamSubBuffer) } return } } } func TestDispatcherPublishesAfterInsert(t *testing.T) { repo := &fakeRepo{} d := NewDispatcher(repo, newTestLogger()) hub := NewStreamHub() d.SetStreamHub(hub) uid := uuid.New() ch, cancel := hub.Subscribe(uid) defer cancel() if err := d.Dispatch(context.Background(), Message{UserID: uid, Topic: "x", Title: "y"}); err != nil { t.Fatalf("dispatch: %v", err) } select { case got := <-ch: if got.Topic != "x" { t.Fatalf("topic = %q", got.Topic) } case <-time.After(time.Second): t.Fatal("hub did not receive dispatched message") } } func TestDispatcherNoPublishOnInsertFailure(t *testing.T) { repo := &fakeRepo{failNext: true} d := NewDispatcher(repo, newTestLogger()) hub := NewStreamHub() d.SetStreamHub(hub) uid := uuid.New() ch, cancel := hub.Subscribe(uid) defer cancel() if err := d.Dispatch(context.Background(), Message{UserID: uid}); err == nil { t.Fatal("expected dispatch error on insert failure") } select { case <-ch: t.Fatal("hub received message despite insert failure") case <-time.After(100 * time.Millisecond): // expected: no publish. } }