Add namespace event filtering support
All checks were successful
CI / build (pull_request) Successful in 21s
All checks were successful
CI / build (pull_request) Successful in 21s
Add SubscriptionFilter type and SubscribeWithFilter method to enable filtering events by type and actor pattern within namespace subscriptions. - SubscriptionFilter supports event type filtering (e.g., only "OrderPlaced") - SubscriptionFilter supports actor ID prefix patterns (e.g., "order-*") - Filters are combined with AND logic - NATSEventBus uses NATS subjects for server-side filtering when possible - Comprehensive test coverage for filtering functionality Closes #21 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
948
eventbus_test.go
Normal file
948
eventbus_test.go
Normal file
@@ -0,0 +1,948 @@
|
||||
package aether
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// === SubscriptionFilter Tests ===
|
||||
|
||||
func TestSubscriptionFilter_Matches_EmptyFilter(t *testing.T) {
|
||||
filter := SubscriptionFilter{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil event",
|
||||
event: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "any event type matches",
|
||||
event: &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "any actor matches",
|
||||
event: &Event{
|
||||
ID: "evt-2",
|
||||
EventType: "UserCreated",
|
||||
ActorID: "user-abc",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionFilter_Matches_EventTypeFilter(t *testing.T) {
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced", "OrderShipped"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matching first event type",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matching second event type",
|
||||
event: &Event{
|
||||
EventType: "OrderShipped",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-matching event type",
|
||||
event: &Event{
|
||||
EventType: "OrderCancelled",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty event type",
|
||||
event: &Event{
|
||||
EventType: "",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionFilter_Matches_SingleEventType(t *testing.T) {
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matching event type",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-matching event type",
|
||||
event: &Event{
|
||||
EventType: "OrderShipped",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionFilter_Matches_ActorPrefixPattern(t *testing.T) {
|
||||
filter := SubscriptionFilter{
|
||||
ActorPattern: "order-*",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matching prefix",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matching prefix with long suffix",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-abc-def-ghi",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exactly prefix (no suffix)",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-matching prefix",
|
||||
event: &Event{
|
||||
EventType: "UserCreated",
|
||||
ActorID: "user-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "prefix without hyphen",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionFilter_Matches_ActorExactPattern(t *testing.T) {
|
||||
filter := SubscriptionFilter{
|
||||
ActorPattern: "order-123",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "longer actor ID",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-1234",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "shorter actor ID",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-12",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different actor ID",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-456",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionFilter_Matches_CombinedFilters(t *testing.T) {
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced", "OrderShipped"},
|
||||
ActorPattern: "order-*",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches both filters",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches event type but not actor",
|
||||
event: &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "user-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "matches actor but not event type",
|
||||
event: &Event{
|
||||
EventType: "OrderCancelled",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "matches neither",
|
||||
event: &Event{
|
||||
EventType: "UserCreated",
|
||||
ActorID: "user-123",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionFilter_Matches_WildcardOnly(t *testing.T) {
|
||||
// Just "*" should match everything (prefix is empty)
|
||||
filter := SubscriptionFilter{
|
||||
ActorPattern: "*",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches any actor",
|
||||
event: &Event{
|
||||
EventType: "Test",
|
||||
ActorID: "anything-at-all",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches empty actor ID",
|
||||
event: &Event{
|
||||
EventType: "Test",
|
||||
ActorID: "",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filter.Matches(tt.event); got != tt.want {
|
||||
t.Errorf("Matches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// === EventBus Tests ===
|
||||
|
||||
func TestNewEventBus(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
|
||||
if bus == nil {
|
||||
t.Fatal("NewEventBus returned nil")
|
||||
}
|
||||
if bus.subscribers == nil {
|
||||
t.Error("subscribers map is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Subscribe(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
ch := bus.Subscribe("test-namespace")
|
||||
|
||||
if ch == nil {
|
||||
t.Fatal("Subscribe returned nil channel")
|
||||
}
|
||||
|
||||
count := bus.SubscriberCount("test-namespace")
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 subscriber, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_SubscribeWithFilter(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced"},
|
||||
}
|
||||
ch := bus.SubscribeWithFilter("test-namespace", filter)
|
||||
|
||||
if ch == nil {
|
||||
t.Fatal("SubscribeWithFilter returned nil channel")
|
||||
}
|
||||
|
||||
count := bus.SubscriberCount("test-namespace")
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 subscriber, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Publish_NoFilter(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
ch := bus.Subscribe("test-namespace")
|
||||
|
||||
event := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
}
|
||||
|
||||
bus.Publish("test-namespace", event)
|
||||
|
||||
select {
|
||||
case received := <-ch:
|
||||
if received.ID != event.ID {
|
||||
t.Errorf("received event ID %q, want %q", received.ID, event.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout waiting for event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Publish_WithEventTypeFilter(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced"},
|
||||
}
|
||||
ch := bus.SubscribeWithFilter("test-namespace", filter)
|
||||
|
||||
// This event should be delivered
|
||||
matchingEvent := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
}
|
||||
|
||||
// This event should NOT be delivered
|
||||
nonMatchingEvent := &Event{
|
||||
ID: "evt-2",
|
||||
EventType: "OrderShipped",
|
||||
ActorID: "order-123",
|
||||
}
|
||||
|
||||
bus.Publish("test-namespace", matchingEvent)
|
||||
bus.Publish("test-namespace", nonMatchingEvent)
|
||||
|
||||
// Should receive matching event
|
||||
select {
|
||||
case received := <-ch:
|
||||
if received.ID != matchingEvent.ID {
|
||||
t.Errorf("received event ID %q, want %q", received.ID, matchingEvent.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout waiting for matching event")
|
||||
}
|
||||
|
||||
// Should NOT receive non-matching event
|
||||
select {
|
||||
case received := <-ch:
|
||||
t.Errorf("received unexpected event: %+v", received)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected - no event should be received
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Publish_WithActorPatternFilter(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
filter := SubscriptionFilter{
|
||||
ActorPattern: "order-*",
|
||||
}
|
||||
ch := bus.SubscribeWithFilter("test-namespace", filter)
|
||||
|
||||
// This event should be delivered
|
||||
matchingEvent := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "Test",
|
||||
ActorID: "order-123",
|
||||
}
|
||||
|
||||
// This event should NOT be delivered
|
||||
nonMatchingEvent := &Event{
|
||||
ID: "evt-2",
|
||||
EventType: "Test",
|
||||
ActorID: "user-456",
|
||||
}
|
||||
|
||||
bus.Publish("test-namespace", matchingEvent)
|
||||
bus.Publish("test-namespace", nonMatchingEvent)
|
||||
|
||||
// Should receive matching event
|
||||
select {
|
||||
case received := <-ch:
|
||||
if received.ID != matchingEvent.ID {
|
||||
t.Errorf("received event ID %q, want %q", received.ID, matchingEvent.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout waiting for matching event")
|
||||
}
|
||||
|
||||
// Should NOT receive non-matching event
|
||||
select {
|
||||
case received := <-ch:
|
||||
t.Errorf("received unexpected event: %+v", received)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected - no event should be received
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Publish_WithCombinedFilters(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced"},
|
||||
ActorPattern: "order-*",
|
||||
}
|
||||
ch := bus.SubscribeWithFilter("test-namespace", filter)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event *Event
|
||||
shouldMatch bool
|
||||
}{
|
||||
{
|
||||
name: "matches both filters",
|
||||
event: &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "matches event type only",
|
||||
event: &Event{
|
||||
ID: "evt-2",
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "user-123",
|
||||
},
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
name: "matches actor only",
|
||||
event: &Event{
|
||||
ID: "evt-3",
|
||||
EventType: "OrderShipped",
|
||||
ActorID: "order-123",
|
||||
},
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bus.Publish("test-namespace", tt.event)
|
||||
|
||||
select {
|
||||
case received := <-ch:
|
||||
if !tt.shouldMatch {
|
||||
t.Errorf("received unexpected event: %+v", received)
|
||||
} else if received.ID != tt.event.ID {
|
||||
t.Errorf("received event ID %q, want %q", received.ID, tt.event.ID)
|
||||
}
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
if tt.shouldMatch {
|
||||
t.Error("timeout waiting for matching event")
|
||||
}
|
||||
// Expected for non-matching events
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_MultipleSubscribers_DifferentFilters(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
// Subscriber for all events
|
||||
chAll := bus.Subscribe("test-namespace")
|
||||
|
||||
// Subscriber for OrderPlaced only
|
||||
chOrders := bus.SubscribeWithFilter("test-namespace", SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced"},
|
||||
})
|
||||
|
||||
// Subscriber for users only
|
||||
chUsers := bus.SubscribeWithFilter("test-namespace", SubscriptionFilter{
|
||||
ActorPattern: "user-*",
|
||||
})
|
||||
|
||||
orderEvent := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
}
|
||||
|
||||
userEvent := &Event{
|
||||
ID: "evt-2",
|
||||
EventType: "UserCreated",
|
||||
ActorID: "user-456",
|
||||
}
|
||||
|
||||
bus.Publish("test-namespace", orderEvent)
|
||||
bus.Publish("test-namespace", userEvent)
|
||||
|
||||
// chAll should receive both events
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-chAll:
|
||||
// Expected
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("chAll: timeout waiting for event")
|
||||
}
|
||||
}
|
||||
|
||||
// chOrders should receive only the order event
|
||||
select {
|
||||
case received := <-chOrders:
|
||||
if received.ID != orderEvent.ID {
|
||||
t.Errorf("chOrders: received event ID %q, want %q", received.ID, orderEvent.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("chOrders: timeout waiting for order event")
|
||||
}
|
||||
|
||||
// chOrders should NOT receive the user event
|
||||
select {
|
||||
case received := <-chOrders:
|
||||
t.Errorf("chOrders: received unexpected event: %+v", received)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected
|
||||
}
|
||||
|
||||
// chUsers should receive only the user event
|
||||
select {
|
||||
case received := <-chUsers:
|
||||
if received.ID != userEvent.ID {
|
||||
t.Errorf("chUsers: received event ID %q, want %q", received.ID, userEvent.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("chUsers: timeout waiting for user event")
|
||||
}
|
||||
|
||||
// chUsers should NOT receive the order event
|
||||
select {
|
||||
case received := <-chUsers:
|
||||
t.Errorf("chUsers: received unexpected event: %+v", received)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Unsubscribe(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
ch := bus.Subscribe("test-namespace")
|
||||
|
||||
if bus.SubscriberCount("test-namespace") != 1 {
|
||||
t.Errorf("expected 1 subscriber before unsubscribe")
|
||||
}
|
||||
|
||||
bus.Unsubscribe("test-namespace", ch)
|
||||
|
||||
if bus.SubscriberCount("test-namespace") != 0 {
|
||||
t.Errorf("expected 0 subscribers after unsubscribe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Unsubscribe_MultipleSubscribers(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
ch1 := bus.Subscribe("test-namespace")
|
||||
ch2 := bus.Subscribe("test-namespace")
|
||||
|
||||
if bus.SubscriberCount("test-namespace") != 2 {
|
||||
t.Errorf("expected 2 subscribers, got %d", bus.SubscriberCount("test-namespace"))
|
||||
}
|
||||
|
||||
bus.Unsubscribe("test-namespace", ch1)
|
||||
|
||||
if bus.SubscriberCount("test-namespace") != 1 {
|
||||
t.Errorf("expected 1 subscriber after first unsubscribe, got %d", bus.SubscriberCount("test-namespace"))
|
||||
}
|
||||
|
||||
bus.Unsubscribe("test-namespace", ch2)
|
||||
|
||||
if bus.SubscriberCount("test-namespace") != 0 {
|
||||
t.Errorf("expected 0 subscribers after second unsubscribe, got %d", bus.SubscriberCount("test-namespace"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Publish_DifferentNamespaces(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
ch1 := bus.Subscribe("namespace-1")
|
||||
ch2 := bus.Subscribe("namespace-2")
|
||||
|
||||
event1 := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "Test",
|
||||
ActorID: "actor-1",
|
||||
}
|
||||
|
||||
event2 := &Event{
|
||||
ID: "evt-2",
|
||||
EventType: "Test",
|
||||
ActorID: "actor-2",
|
||||
}
|
||||
|
||||
bus.Publish("namespace-1", event1)
|
||||
bus.Publish("namespace-2", event2)
|
||||
|
||||
// ch1 should receive event1
|
||||
select {
|
||||
case received := <-ch1:
|
||||
if received.ID != event1.ID {
|
||||
t.Errorf("ch1: received event ID %q, want %q", received.ID, event1.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("ch1: timeout waiting for event")
|
||||
}
|
||||
|
||||
// ch1 should NOT receive event2
|
||||
select {
|
||||
case received := <-ch1:
|
||||
t.Errorf("ch1: received unexpected event: %+v", received)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected
|
||||
}
|
||||
|
||||
// ch2 should receive event2
|
||||
select {
|
||||
case received := <-ch2:
|
||||
if received.ID != event2.ID {
|
||||
t.Errorf("ch2: received event ID %q, want %q", received.ID, event2.ID)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("ch2: timeout waiting for event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Stop(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
|
||||
ch1 := bus.Subscribe("namespace-1")
|
||||
ch2 := bus.Subscribe("namespace-2")
|
||||
|
||||
bus.Stop()
|
||||
|
||||
// Channels should be closed
|
||||
select {
|
||||
case _, ok := <-ch1:
|
||||
if ok {
|
||||
t.Error("ch1 should be closed after Stop")
|
||||
}
|
||||
default:
|
||||
// Channel is closed and empty, which is expected
|
||||
}
|
||||
|
||||
select {
|
||||
case _, ok := <-ch2:
|
||||
if ok {
|
||||
t.Error("ch2 should be closed after Stop")
|
||||
}
|
||||
default:
|
||||
// Channel is closed and empty, which is expected
|
||||
}
|
||||
|
||||
// Subscriber count should be 0
|
||||
if bus.SubscriberCount("namespace-1") != 0 {
|
||||
t.Error("expected 0 subscribers after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_SubscriberCount(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
// No subscribers initially
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 0 {
|
||||
t.Errorf("expected 0 subscribers initially, got %d", count)
|
||||
}
|
||||
|
||||
// Add subscribers
|
||||
ch1 := bus.Subscribe("test-namespace")
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 1 {
|
||||
t.Errorf("expected 1 subscriber, got %d", count)
|
||||
}
|
||||
|
||||
ch2 := bus.Subscribe("test-namespace")
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 2 {
|
||||
t.Errorf("expected 2 subscribers, got %d", count)
|
||||
}
|
||||
|
||||
// Different namespace
|
||||
bus.Subscribe("other-namespace")
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 2 {
|
||||
t.Errorf("expected 2 subscribers for test-namespace, got %d", count)
|
||||
}
|
||||
if count := bus.SubscriberCount("other-namespace"); count != 1 {
|
||||
t.Errorf("expected 1 subscriber for other-namespace, got %d", count)
|
||||
}
|
||||
|
||||
// Unsubscribe
|
||||
bus.Unsubscribe("test-namespace", ch1)
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 1 {
|
||||
t.Errorf("expected 1 subscriber after unsubscribe, got %d", count)
|
||||
}
|
||||
|
||||
bus.Unsubscribe("test-namespace", ch2)
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 0 {
|
||||
t.Errorf("expected 0 subscribers after unsubscribe, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_ConcurrentPublishAndSubscribe(t *testing.T) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 100
|
||||
eventsPerGoroutine := 10
|
||||
|
||||
// Start subscribers in goroutines
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
ch := bus.Subscribe("test-namespace")
|
||||
|
||||
// Read a few events then unsubscribe
|
||||
for j := 0; j < eventsPerGoroutine; j++ {
|
||||
select {
|
||||
case <-ch:
|
||||
// Received event
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
// Timeout, continue
|
||||
}
|
||||
}
|
||||
|
||||
bus.Unsubscribe("test-namespace", ch)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Publish events concurrently
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < eventsPerGoroutine; j++ {
|
||||
event := &Event{
|
||||
ID: "evt",
|
||||
EventType: "Test",
|
||||
ActorID: "actor",
|
||||
}
|
||||
bus.Publish("test-namespace", event)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// No subscribers should remain
|
||||
if count := bus.SubscriberCount("test-namespace"); count != 0 {
|
||||
t.Errorf("expected 0 subscribers after test, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBus_Interface(t *testing.T) {
|
||||
// Verify EventBus implements EventBroadcaster interface
|
||||
var _ EventBroadcaster = (*EventBus)(nil)
|
||||
}
|
||||
|
||||
// === Benchmark Tests ===
|
||||
|
||||
func BenchmarkSubscriptionFilter_Matches(b *testing.B) {
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"OrderPlaced", "OrderShipped", "OrderDelivered"},
|
||||
ActorPattern: "order-*",
|
||||
}
|
||||
|
||||
event := &Event{
|
||||
EventType: "OrderPlaced",
|
||||
ActorID: "order-123",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filter.Matches(event)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEventBus_Publish(b *testing.B) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
ch := bus.Subscribe("test-namespace")
|
||||
|
||||
event := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "Test",
|
||||
ActorID: "actor-1",
|
||||
}
|
||||
|
||||
// Drain the channel in a goroutine
|
||||
go func() {
|
||||
for range ch {
|
||||
}
|
||||
}()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bus.Publish("test-namespace", event)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEventBus_PublishWithFilter(b *testing.B) {
|
||||
bus := NewEventBus()
|
||||
defer bus.Stop()
|
||||
|
||||
filter := SubscriptionFilter{
|
||||
EventTypes: []string{"Test"},
|
||||
ActorPattern: "actor-*",
|
||||
}
|
||||
ch := bus.SubscribeWithFilter("test-namespace", filter)
|
||||
|
||||
event := &Event{
|
||||
ID: "evt-1",
|
||||
EventType: "Test",
|
||||
ActorID: "actor-1",
|
||||
}
|
||||
|
||||
// Drain the channel in a goroutine
|
||||
go func() {
|
||||
for range ch {
|
||||
}
|
||||
}()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bus.Publish("test-namespace", event)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user