feat: edge gateway service

This commit is contained in:
Ilia Denisov
2026-04-02 19:18:42 +02:00
committed by GitHub
parent 8cde99936c
commit 436c97a38b
95 changed files with 20504 additions and 57 deletions
+270
View File
@@ -0,0 +1,270 @@
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):
}
}