feat: notification service
This commit is contained in:
@@ -477,6 +477,26 @@ payload only: `user_id`, optional `device_session_id`, `event_type`,
|
||||
gateway derives `timestamp_ms`, recomputes `payload_hash`, signs the event,
|
||||
and only then forwards it to the matching `SubscribeEvents` streams.
|
||||
|
||||
Notification-owned user-facing payloads are expected to use
|
||||
`pkg/schema/fbs/notification.fbs`. The initial notification event vocabulary
|
||||
in v1 is exactly:
|
||||
|
||||
- `game.turn.ready`
|
||||
- `game.finished`
|
||||
- `lobby.application.submitted`
|
||||
- `lobby.membership.approved`
|
||||
- `lobby.membership.rejected`
|
||||
- `lobby.invite.created`
|
||||
- `lobby.invite.redeemed`
|
||||
|
||||
`lobby.application.submitted` is published toward `Gateway` only for the
|
||||
private-game owner flow. The public-game variant is email-only.
|
||||
The real `Notification Service -> Gateway` integration suite verifies this
|
||||
user-targeted fan-out path and asserts that notification-owned push events do
|
||||
not include `device_session_id`, so Gateway delivers them to every active
|
||||
stream for the target user. Auth-code email does not use this push path and
|
||||
continues to bypass `Notification Service`.
|
||||
|
||||
## Verification and Routing Pipeline
|
||||
|
||||
The gateway applies the same strict verification order for authenticated gRPC
|
||||
|
||||
@@ -173,9 +173,9 @@ User-wide event:
|
||||
```bash
|
||||
redis-cli XADD gateway:client-events '*' \
|
||||
user_id user-123 \
|
||||
event_type fleet.updated \
|
||||
event_id event-123 \
|
||||
payload_bytes payload-v1
|
||||
event_type game.turn.ready \
|
||||
event_id notification-route-123 \
|
||||
payload_bytes flatbuffers-game-turn-ready
|
||||
```
|
||||
|
||||
Session-targeted event with correlation:
|
||||
@@ -194,6 +194,8 @@ redis-cli XADD gateway:client-events '*' \
|
||||
Notes:
|
||||
|
||||
- `payload_bytes` in Redis Stream entries must be binary-safe payload data;
|
||||
- notification-owned payload bytes should follow
|
||||
`pkg/schema/fbs/notification.fbs`;
|
||||
- the gateway derives `timestamp_ms`, recomputes `payload_hash`, and signs the
|
||||
outgoing event at delivery time;
|
||||
- each gateway replica consumes streams with plain `XREAD`, so publishers must
|
||||
|
||||
@@ -15,8 +15,10 @@ import (
|
||||
"galaxy/gateway/internal/push"
|
||||
"galaxy/gateway/internal/session"
|
||||
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1"
|
||||
notificationfbs "galaxy/schema/fbs/notification"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
@@ -63,31 +65,37 @@ func TestSubscribeEventsFanOutsUserTargetedEventToAllUserSessions(t *testing.T)
|
||||
require.NoError(t, err)
|
||||
assertPushBootstrapEvent(t, recvPushEvent(t, unrelated), "request-3", "trace-device-session-3")
|
||||
|
||||
payloadBytes := buildGameTurnReadyPayload(t, "game-123", 54)
|
||||
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
||||
"user_id": "user-123",
|
||||
"event_type": "fleet.updated",
|
||||
"event_type": "game.turn.ready",
|
||||
"event_id": "event-123",
|
||||
"payload_bytes": []byte("payload-123"),
|
||||
"payload_bytes": payloadBytes,
|
||||
"request_id": "request-123",
|
||||
"trace_id": "trace-123",
|
||||
})
|
||||
|
||||
assertSignedPushEvent(t, recvPushEvent(t, targetOne), push.Event{
|
||||
firstDelivered := recvPushEvent(t, targetOne)
|
||||
assertSignedPushEvent(t, firstDelivered, push.Event{
|
||||
UserID: "user-123",
|
||||
EventType: "fleet.updated",
|
||||
EventType: "game.turn.ready",
|
||||
EventID: "event-123",
|
||||
PayloadBytes: []byte("payload-123"),
|
||||
PayloadBytes: payloadBytes,
|
||||
RequestID: "request-123",
|
||||
TraceID: "trace-123",
|
||||
})
|
||||
assertSignedPushEvent(t, recvPushEvent(t, targetTwo), push.Event{
|
||||
assertDecodedGameTurnReadyPayload(t, firstDelivered.GetPayloadBytes(), "game-123", 54)
|
||||
|
||||
secondDelivered := recvPushEvent(t, targetTwo)
|
||||
assertSignedPushEvent(t, secondDelivered, push.Event{
|
||||
UserID: "user-123",
|
||||
EventType: "fleet.updated",
|
||||
EventType: "game.turn.ready",
|
||||
EventID: "event-123",
|
||||
PayloadBytes: []byte("payload-123"),
|
||||
PayloadBytes: payloadBytes,
|
||||
RequestID: "request-123",
|
||||
TraceID: "trace-123",
|
||||
})
|
||||
assertDecodedGameTurnReadyPayload(t, secondDelivered.GetPayloadBytes(), "game-123", 54)
|
||||
assertNoPushEvent(t, unrelated, cancelUnrelated)
|
||||
}
|
||||
|
||||
@@ -414,3 +422,26 @@ func pushResponseSignerPublicKey() ed25519.PublicKey {
|
||||
seed := sha256.Sum256([]byte("gateway-events-grpc-test-response"))
|
||||
return ed25519.NewKeyFromSeed(seed[:]).Public().(ed25519.PublicKey)
|
||||
}
|
||||
|
||||
func buildGameTurnReadyPayload(t *testing.T, gameID string, turnNumber int64) []byte {
|
||||
t.Helper()
|
||||
|
||||
builder := flatbuffers.NewBuilder(64)
|
||||
gameIDOffset := builder.CreateString(gameID)
|
||||
|
||||
notificationfbs.GameTurnReadyEventStart(builder)
|
||||
notificationfbs.GameTurnReadyEventAddGameId(builder, gameIDOffset)
|
||||
notificationfbs.GameTurnReadyEventAddTurnNumber(builder, turnNumber)
|
||||
offset := notificationfbs.GameTurnReadyEventEnd(builder)
|
||||
notificationfbs.FinishGameTurnReadyEventBuffer(builder, offset)
|
||||
|
||||
return builder.FinishedBytes()
|
||||
}
|
||||
|
||||
func assertDecodedGameTurnReadyPayload(t *testing.T, payload []byte, wantGameID string, wantTurnNumber int64) {
|
||||
t.Helper()
|
||||
|
||||
event := notificationfbs.GetRootAsGameTurnReadyEvent(payload, 0)
|
||||
require.Equal(t, wantGameID, string(event.GameId()))
|
||||
require.Equal(t, wantTurnNumber, event.TurnNumber())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user