package stream import ( "testing" "time" amqp "github.com/rabbitmq/amqp091-go" ) func TestDispatchHub_PublishRoutesToLocalSubscriber(t *testing.T) { t.Parallel() hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"}) sub, cancel := hub.Subscribe("client-a") defer cancel() event := DesktopDispatchEvent{ Type: "task_available", TaskID: "task-1", TargetClientID: "client-a", Kind: "publish", Status: "queued", } hub.Publish(event) select { case got := <-sub.Outbound(): if got.TaskID != "task-1" { t.Fatalf("unexpected task id: %s", got.TaskID) } case <-time.After(200 * time.Millisecond): t.Fatal("expected to receive event within 200ms") } } func TestDispatchHub_DropsEventsForUnknownClient(t *testing.T) { t.Parallel() hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"}) // Subscribe for a different client, publish for another one. _, cancel := hub.Subscribe("client-b") defer cancel() hub.Publish(DesktopDispatchEvent{ Type: "task_available", TargetClientID: "client-a", }) ids := hub.ActiveClientIDs() if len(ids) != 1 || ids[0] != "client-b" { t.Fatalf("expected only client-b subscribed, got %v", ids) } } func TestDispatchHub_CancelRemovesSubscriber(t *testing.T) { t.Parallel() hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"}) sub, cancel := hub.Subscribe("client-a") cancel() // Subsequent publish must not panic even though the subscriber is gone; // channel is closed, reads must yield zero value with !ok. hub.Publish(DesktopDispatchEvent{TargetClientID: "client-a"}) select { case _, ok := <-sub.Outbound(): if ok { t.Fatal("expected outbound channel to be closed after cancel") } case <-time.After(50 * time.Millisecond): t.Fatal("expected closed channel to yield immediately") } if ids := hub.ActiveClientIDs(); len(ids) != 0 { t.Fatalf("expected no subscribers after cancel, got %v", ids) } } func TestDispatchHub_MultipleSubscribersSameClient(t *testing.T) { t.Parallel() hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"}) subA, cancelA := hub.Subscribe("client-a") defer cancelA() subB, cancelB := hub.Subscribe("client-a") defer cancelB() hub.Publish(DesktopDispatchEvent{ Type: "task_available", TaskID: "task-1", TargetClientID: "client-a", }) for i, ch := range []<-chan DesktopDispatchEvent{subA.Outbound(), subB.Outbound()} { select { case got := <-ch: if got.TaskID != "task-1" { t.Fatalf("subscriber %d got unexpected task id: %s", i, got.TaskID) } case <-time.After(200 * time.Millisecond): t.Fatalf("subscriber %d did not receive event", i) } } } func TestDispatchHub_HandleDeliveryFallsBackToRoutingKey(t *testing.T) { t.Parallel() hub := NewDesktopDispatchHub(nil, nil, []string{"publish.*"}) sub, cancel := hub.Subscribe("client-c") defer cancel() // Body intentionally omits target_client_id; the hub should recover it // from the routing key ".". delivery := amqp.Delivery{ RoutingKey: "publish.client-c", Body: []byte(`{"type":"task_available","task_id":"task-9","kind":"publish","status":"queued"}`), } hub.handleDelivery(delivery) select { case got := <-sub.Outbound(): if got.TargetClientID != "client-c" { t.Fatalf("expected fallback to set target client id, got %q", got.TargetClientID) } if got.TaskID != "task-9" { t.Fatalf("unexpected task id: %s", got.TaskID) } case <-time.After(200 * time.Millisecond): t.Fatal("expected fallback-routed event") } }