feat: notification service

This commit is contained in:
Ilia Denisov
2026-04-22 08:49:45 +02:00
committed by GitHub
parent 5b7593e6f6
commit 32dc29359a
135 changed files with 21828 additions and 130 deletions
+20
View File
@@ -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
+5 -3
View File
@@ -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())
}