feat(sse): per-connection filtering with user and workstation context
- Extend sseClient with userID, workstationID, and item filter set - Update Subscribe() to accept userID and workstationID params - Add WatchItem/UnwatchItem/IsWatchingItem methods on sseClient - Add PublishToItem, PublishToWorkstation, PublishToUser targeted delivery - Targeted events get IDs but skip history ring buffer (real-time only) - Update HandleEvents to pass auth user ID and workstation_id query param - Touch workstation last_seen on SSE connect - Existing Publish() broadcast unchanged; all current callers unaffected - Add 5 new tests for targeted delivery and item watch lifecycle Closes #162
This commit is contained in:
@@ -10,7 +10,7 @@ import (
|
||||
func TestBrokerSubscribeUnsubscribe(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
|
||||
c := b.Subscribe()
|
||||
c := b.Subscribe("", "")
|
||||
if b.ClientCount() != 1 {
|
||||
t.Fatalf("expected 1 client, got %d", b.ClientCount())
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func TestBrokerSubscribeUnsubscribe(t *testing.T) {
|
||||
|
||||
func TestBrokerPublish(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
c := b.Subscribe()
|
||||
c := b.Subscribe("", "")
|
||||
defer b.Unsubscribe(c)
|
||||
|
||||
b.Publish("item.created", `{"part_number":"F01-0001"}`)
|
||||
@@ -46,7 +46,7 @@ func TestBrokerPublish(t *testing.T) {
|
||||
|
||||
func TestBrokerPublishDropsSlow(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
c := b.Subscribe()
|
||||
c := b.Subscribe("", "")
|
||||
defer b.Unsubscribe(c)
|
||||
|
||||
// Fill the client's channel
|
||||
@@ -89,9 +89,9 @@ func TestBrokerEventsSince(t *testing.T) {
|
||||
func TestBrokerClientCount(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
|
||||
c1 := b.Subscribe()
|
||||
c2 := b.Subscribe()
|
||||
c3 := b.Subscribe()
|
||||
c1 := b.Subscribe("", "")
|
||||
c2 := b.Subscribe("", "")
|
||||
c3 := b.Subscribe("", "")
|
||||
|
||||
if b.ClientCount() != 3 {
|
||||
t.Fatalf("expected 3 clients, got %d", b.ClientCount())
|
||||
@@ -111,7 +111,7 @@ func TestBrokerClientCount(t *testing.T) {
|
||||
|
||||
func TestBrokerShutdown(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
c := b.Subscribe()
|
||||
c := b.Subscribe("", "")
|
||||
|
||||
b.Shutdown()
|
||||
|
||||
@@ -145,3 +145,128 @@ func TestBrokerMonotonicIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchUnwatchItem(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
c := b.Subscribe("user1", "ws1")
|
||||
defer b.Unsubscribe(c)
|
||||
|
||||
if c.IsWatchingItem("item-abc") {
|
||||
t.Fatal("should not be watching item-abc before WatchItem")
|
||||
}
|
||||
|
||||
c.WatchItem("item-abc")
|
||||
if !c.IsWatchingItem("item-abc") {
|
||||
t.Fatal("should be watching item-abc after WatchItem")
|
||||
}
|
||||
|
||||
c.UnwatchItem("item-abc")
|
||||
if c.IsWatchingItem("item-abc") {
|
||||
t.Fatal("should not be watching item-abc after UnwatchItem")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishToItem(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
watcher := b.Subscribe("user1", "ws1")
|
||||
defer b.Unsubscribe(watcher)
|
||||
bystander := b.Subscribe("user2", "ws2")
|
||||
defer b.Unsubscribe(bystander)
|
||||
|
||||
watcher.WatchItem("item-abc")
|
||||
b.PublishToItem("item-abc", "edit.started", `{"item_id":"item-abc"}`)
|
||||
|
||||
// Watcher should receive the event.
|
||||
select {
|
||||
case ev := <-watcher.ch:
|
||||
if ev.Type != "edit.started" {
|
||||
t.Fatalf("expected edit.started, got %s", ev.Type)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("watcher did not receive targeted event")
|
||||
}
|
||||
|
||||
// Bystander should not.
|
||||
select {
|
||||
case ev := <-bystander.ch:
|
||||
t.Fatalf("bystander should not receive targeted event, got %s", ev.Type)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishToWorkstation(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
target := b.Subscribe("user1", "ws-target")
|
||||
defer b.Unsubscribe(target)
|
||||
other := b.Subscribe("user1", "ws-other")
|
||||
defer b.Unsubscribe(other)
|
||||
|
||||
b.PublishToWorkstation("ws-target", "sync.update", `{"data":"x"}`)
|
||||
|
||||
select {
|
||||
case ev := <-target.ch:
|
||||
if ev.Type != "sync.update" {
|
||||
t.Fatalf("expected sync.update, got %s", ev.Type)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("target workstation did not receive event")
|
||||
}
|
||||
|
||||
select {
|
||||
case ev := <-other.ch:
|
||||
t.Fatalf("other workstation should not receive event, got %s", ev.Type)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishToUser(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
c1 := b.Subscribe("user1", "ws1")
|
||||
defer b.Unsubscribe(c1)
|
||||
c2 := b.Subscribe("user1", "ws2")
|
||||
defer b.Unsubscribe(c2)
|
||||
c3 := b.Subscribe("user2", "ws3")
|
||||
defer b.Unsubscribe(c3)
|
||||
|
||||
b.PublishToUser("user1", "user.notify", `{"msg":"hello"}`)
|
||||
|
||||
// Both user1 connections should receive.
|
||||
for _, c := range []*sseClient{c1, c2} {
|
||||
select {
|
||||
case ev := <-c.ch:
|
||||
if ev.Type != "user.notify" {
|
||||
t.Fatalf("expected user.notify, got %s", ev.Type)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("user1 client did not receive event")
|
||||
}
|
||||
}
|
||||
|
||||
// user2 should not.
|
||||
select {
|
||||
case ev := <-c3.ch:
|
||||
t.Fatalf("user2 should not receive event, got %s", ev.Type)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetedEventsNotInHistory(t *testing.T) {
|
||||
b := NewBroker(zerolog.Nop())
|
||||
c := b.Subscribe("user1", "ws1")
|
||||
defer b.Unsubscribe(c)
|
||||
c.WatchItem("item-abc")
|
||||
|
||||
b.Publish("broadcast", `{}`)
|
||||
b.PublishToItem("item-abc", "targeted", `{}`)
|
||||
|
||||
events := b.EventsSince(0)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event in history (broadcast only), got %d", len(events))
|
||||
}
|
||||
if events[0].Type != "broadcast" {
|
||||
t.Fatalf("expected broadcast event in history, got %s", events[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user