Files
galaxy-game/backend/push/publisher_test.go
T
2026-05-07 00:58:53 +03:00

162 lines
4.6 KiB
Go

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, JSONEvent{EventKind: "lobby.invite.received", Payload: 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, JSONEvent{EventKind: "x"},"", "", ""))
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, JSONEvent{EventKind: "k"},"", "", ""))
require.Error(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, JSONEvent{EventKind: " "},"", "", ""))
}
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, JSONEvent{EventKind: "k"},"", "", ""))
}
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, JSONEvent{EventKind: "k"},"", "", ""))
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, event Event, 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)