feat: backend service
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user