package push import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHubDeliversSessionTargetedEvent(t *testing.T) { t.Parallel() hub := NewHub(4) target, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-1", }) require.NoError(t, err) otherSession, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-2", }) require.NoError(t, err) unrelatedUser, err := hub.Register(StreamBinding{ UserID: "user-999", DeviceSessionID: "device-session-3", }) require.NoError(t, err) hub.Publish(Event{ UserID: "user-123", DeviceSessionID: "device-session-1", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) assertEvent(t, target.Events(), Event{ UserID: "user-123", DeviceSessionID: "device-session-1", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) assertNoEvent(t, otherSession.Events()) assertNoEvent(t, unrelatedUser.Events()) } func TestHubDeliversUserTargetedEventToAllUserSessions(t *testing.T) { t.Parallel() hub := NewHub(4) first, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-1", }) require.NoError(t, err) second, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-2", }) require.NoError(t, err) unrelated, err := hub.Register(StreamBinding{ UserID: "user-999", DeviceSessionID: "device-session-3", }) require.NoError(t, err) hub.Publish(Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), RequestID: "request-1", TraceID: "trace-1", }) want := Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), RequestID: "request-1", TraceID: "trace-1", } assertEvent(t, first.Events(), want) assertEvent(t, second.Events(), want) assertNoEvent(t, unrelated.Events()) } func TestSubscriptionCloseUnregistersStream(t *testing.T) { t.Parallel() hub := NewHub(4) subscription, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-1", }) require.NoError(t, err) subscription.Close() select { case <-subscription.Done(): case <-time.After(time.Second): require.FailNow(t, "subscription did not close") } hub.Publish(Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) assertNoEvent(t, subscription.Events()) assert.NoError(t, subscription.Err()) } func TestHubOverflowClosesOnlySlowSubscription(t *testing.T) { t.Parallel() hub := NewHub(1) slow, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-1", }) require.NoError(t, err) fast, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-2", }) require.NoError(t, err) hub.Publish(Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) assertEvent(t, fast.Events(), Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) hub.Publish(Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-2", PayloadBytes: []byte("payload-2"), }) select { case <-slow.Done(): case <-time.After(time.Second): require.FailNow(t, "slow subscription did not close after overflow") } assert.ErrorIs(t, slow.Err(), ErrSubscriptionOverflow) assertEvent(t, fast.Events(), Event{ UserID: "user-123", EventType: "fleet.updated", EventID: "event-2", PayloadBytes: []byte("payload-2"), }) } func TestHubRevokeDeviceSessionClosesOnlyMatchingSubscriptions(t *testing.T) { t.Parallel() hub := NewHub(4) targetOne, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-1", }) require.NoError(t, err) targetTwo, err := hub.Register(StreamBinding{ UserID: "user-456", DeviceSessionID: "device-session-1", }) require.NoError(t, err) otherSession, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-2", }) require.NoError(t, err) hub.RevokeDeviceSession("device-session-1") select { case <-targetOne.Done(): case <-time.After(time.Second): require.FailNow(t, "first matching subscription did not close after revoke") } select { case <-targetTwo.Done(): case <-time.After(time.Second): require.FailNow(t, "second matching subscription did not close after revoke") } assert.ErrorIs(t, targetOne.Err(), ErrSubscriptionRevoked) assert.ErrorIs(t, targetTwo.Err(), ErrSubscriptionRevoked) select { case <-otherSession.Done(): require.FailNow(t, "unrelated session subscription closed after revoke") case <-time.After(50 * time.Millisecond): } hub.Publish(Event{ UserID: "user-123", DeviceSessionID: "device-session-2", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) assertEvent(t, otherSession.Events(), Event{ UserID: "user-123", DeviceSessionID: "device-session-2", EventType: "fleet.updated", EventID: "event-1", PayloadBytes: []byte("payload-1"), }) } func TestHubRevokeDeviceSessionIgnoresUnknownOrEmptySession(t *testing.T) { t.Parallel() hub := NewHub(4) subscription, err := hub.Register(StreamBinding{ UserID: "user-123", DeviceSessionID: "device-session-1", }) require.NoError(t, err) hub.RevokeDeviceSession("") hub.RevokeDeviceSession("missing-session") select { case <-subscription.Done(): require.FailNow(t, "subscription closed for empty or unknown session revoke") case <-time.After(50 * time.Millisecond): } } func assertEvent(t *testing.T, eventCh <-chan Event, want Event) { t.Helper() select { case got := <-eventCh: assert.Equal(t, want, got) case <-time.After(time.Second): require.FailNow(t, "event was not delivered") } } func assertNoEvent(t *testing.T, eventCh <-chan Event) { t.Helper() select { case got := <-eventCh: require.FailNowf(t, "unexpected event delivered", "%+v", got) case <-time.After(50 * time.Millisecond): } }