package push import ( "context" "encoding/json" "testing" "time" pushv1 "galaxy/backend/proto/push/v1" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestService(t *testing.T) *Service { t.Helper() svc, err := NewService(ServiceConfig{ FreshnessWindow: time.Minute, RingCapacity: 16, PerConnBuffer: 8, }, nil, nil) require.NoError(t, err) return svc } func TestPublishClientEventStampsCursorAndPayload(t *testing.T) { t.Parallel() svc := newTestService(t) t.Cleanup(svc.Close) userID := uuid.New() devID := uuid.New() payload := map[string]any{"game_id": "g1", "n": 7.0} require.NoError(t, svc.PublishClientEvent(context.Background(), userID, &devID, "lobby.invite.received", payload, "route-1", "req-1", "trace-1")) events, stale := svc.ring.since(0, time.Now()) require.False(t, stale) require.Len(t, events, 1) ev := events[0] assert.Equal(t, formatCursor(1), ev.Cursor) ce := ev.GetClientEvent() require.NotNil(t, ce) assert.Equal(t, userID.String(), ce.UserId) assert.Equal(t, devID.String(), ce.DeviceSessionId) assert.Equal(t, "lobby.invite.received", ce.Kind) assert.Equal(t, "route-1", ce.EventId) assert.Equal(t, "req-1", ce.RequestId) assert.Equal(t, "trace-1", ce.TraceId) var got map[string]any require.NoError(t, json.Unmarshal(ce.Payload, &got)) assert.Equal(t, "g1", got["game_id"]) assert.EqualValues(t, 7.0, got["n"]) } func TestPublishClientEventOmitsDeviceSessionWhenNil(t *testing.T) { t.Parallel() svc := newTestService(t) t.Cleanup(svc.Close) userID := uuid.New() require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "x", nil, "", "", "")) events, _ := svc.ring.since(0, time.Now()) require.Len(t, events, 1) assert.Empty(t, events[0].GetClientEvent().DeviceSessionId) } func TestPublishClientEventRequiresUserAndKind(t *testing.T) { t.Parallel() svc := newTestService(t) t.Cleanup(svc.Close) require.Error(t, svc.PublishClientEvent(context.Background(), uuid.Nil, nil, "k", nil, "", "", "")) require.Error(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, " ", nil, "", "", "")) } func TestPublishSessionInvalidationStampsCursor(t *testing.T) { t.Parallel() svc := newTestService(t) t.Cleanup(svc.Close) userID := uuid.New() devID := uuid.New() svc.PublishSessionInvalidation(context.Background(), devID, userID, "auth.revoke_session") events, _ := svc.ring.since(0, time.Now()) require.Len(t, events, 1) si := events[0].GetSessionInvalidation() require.NotNil(t, si) assert.Equal(t, userID.String(), si.UserId) assert.Equal(t, devID.String(), si.DeviceSessionId) assert.Equal(t, "auth.revoke_session", si.Reason) } func TestPublishSessionInvalidationFanOutOmitsDeviceSession(t *testing.T) { t.Parallel() svc := newTestService(t) t.Cleanup(svc.Close) userID := uuid.New() svc.PublishSessionInvalidation(context.Background(), uuid.Nil, userID, "auth.revoke_all_for_user") events, _ := svc.ring.since(0, time.Now()) require.Len(t, events, 1) si := events[0].GetSessionInvalidation() assert.Empty(t, si.DeviceSessionId) assert.Equal(t, userID.String(), si.UserId) } func TestPublishCursorMonotonic(t *testing.T) { t.Parallel() svc := newTestService(t) t.Cleanup(svc.Close) userID := uuid.New() for range 5 { require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, "k", nil, "", "", "")) } events, _ := svc.ring.since(0, time.Now()) require.Len(t, events, 5) for i, ev := range events { assert.Equal(t, formatCursor(uint64(i+1)), ev.Cursor) } } func TestPublishOnClosedServiceIsNoop(t *testing.T) { t.Parallel() svc := newTestService(t) svc.Close() require.NoError(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, "k", nil, "", "", "")) events, _ := svc.ring.since(0, time.Now()) assert.Empty(t, events) } // Compile-time interface checks: Service must satisfy the publisher // contracts that internal/auth and internal/notification import. var ( _ pushClientEventPublisher = (*Service)(nil) _ pushSessionInvalidationEmitter = (*Service)(nil) ) type pushClientEventPublisher interface { PublishClientEvent(ctx context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, kind string, payload map[string]any, eventID, requestID, traceID string) error } type pushSessionInvalidationEmitter interface { PublishSessionInvalidation(ctx context.Context, deviceSessionID, userID uuid.UUID, reason string) } // Make sure the publisher satisfies pushv1.PushServer at the type level. var _ pushv1.PushServer = (*Service)(nil)