feat: gamemaster

This commit is contained in:
Ilia Denisov
2026-05-03 07:59:03 +02:00
committed by GitHub
parent a7cee15115
commit 3e2622757e
229 changed files with 41521 additions and 1098 deletions
@@ -0,0 +1,167 @@
package notificationpublisher
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"galaxy/notificationintent"
)
func newRedis(t *testing.T) (*redis.Client, *miniredis.Miniredis) {
t.Helper()
server := miniredis.RunT(t)
client := redis.NewClient(&redis.Options{Addr: server.Addr()})
t.Cleanup(func() { _ = client.Close() })
return client, server
}
func readStream(t *testing.T, client *redis.Client, stream string) []redis.XMessage {
t.Helper()
messages, err := client.XRange(context.Background(), stream, "-", "+").Result()
require.NoError(t, err)
return messages
}
func TestNewPublisherValidation(t *testing.T) {
t.Run("nil client", func(t *testing.T) {
_, err := NewPublisher(Config{})
require.Error(t, err)
assert.Contains(t, err.Error(), "nil redis client")
})
}
func TestPublishGameTurnReady(t *testing.T) {
client, _ := newRedis(t)
publisher, err := NewPublisher(Config{Client: client, Stream: "notification:intents"})
require.NoError(t, err)
intent, err := notificationintent.NewGameTurnReadyIntent(
notificationintent.Metadata{
IdempotencyKey: "gamemaster:turn:game-1:42",
OccurredAt: time.UnixMilli(1714200000000).UTC(),
},
[]string{"u-2", "u-1"},
notificationintent.GameTurnReadyPayload{
GameID: "game-1",
GameName: "Galaxy",
TurnNumber: 42,
},
)
require.NoError(t, err)
require.NoError(t, publisher.Publish(context.Background(), intent))
messages := readStream(t, client, "notification:intents")
require.Len(t, messages, 1)
values := messages[0].Values
assert.Equal(t, "game.turn.ready", values["notification_type"])
assert.Equal(t, "game_master", values["producer"])
assert.Equal(t, "user", values["audience_kind"])
assert.Equal(t, "gamemaster:turn:game-1:42", values["idempotency_key"])
recipients, ok := values["recipient_user_ids_json"].(string)
require.True(t, ok)
var ids []string
require.NoError(t, json.Unmarshal([]byte(recipients), &ids))
assert.ElementsMatch(t, []string{"u-1", "u-2"}, ids)
payloadRaw, ok := values["payload_json"].(string)
require.True(t, ok)
var payload map[string]any
require.NoError(t, json.Unmarshal([]byte(payloadRaw), &payload))
assert.Equal(t, "game-1", payload["game_id"])
assert.Equal(t, float64(42), payload["turn_number"])
}
func TestPublishGameFinished(t *testing.T) {
client, _ := newRedis(t)
publisher, err := NewPublisher(Config{Client: client, Stream: "notification:intents"})
require.NoError(t, err)
intent, err := notificationintent.NewGameFinishedIntent(
notificationintent.Metadata{
IdempotencyKey: "gamemaster:finished:g-1",
OccurredAt: time.UnixMilli(1714200000000).UTC(),
},
[]string{"u-1"},
notificationintent.GameFinishedPayload{
GameID: "g-1",
GameName: "Galaxy",
FinalTurnNumber: 100,
},
)
require.NoError(t, err)
require.NoError(t, publisher.Publish(context.Background(), intent))
messages := readStream(t, client, "notification:intents")
require.Len(t, messages, 1)
assert.Equal(t, "game.finished", messages[0].Values["notification_type"])
assert.Equal(t, "user", messages[0].Values["audience_kind"])
}
func TestPublishGameGenerationFailed(t *testing.T) {
client, _ := newRedis(t)
publisher, err := NewPublisher(Config{Client: client, Stream: "notification:intents"})
require.NoError(t, err)
intent, err := notificationintent.NewGameGenerationFailedIntent(
notificationintent.Metadata{
IdempotencyKey: "gamemaster:gen-failed:g-1:42",
OccurredAt: time.UnixMilli(1714200000000).UTC(),
},
notificationintent.GameGenerationFailedPayload{
GameID: "g-1",
GameName: "Galaxy",
FailureReason: "engine timeout",
},
)
require.NoError(t, err)
require.NoError(t, publisher.Publish(context.Background(), intent))
messages := readStream(t, client, "notification:intents")
require.Len(t, messages, 1)
values := messages[0].Values
assert.Equal(t, "game.generation_failed", values["notification_type"])
assert.Equal(t, "admin_email", values["audience_kind"])
_, hasRecipients := values["recipient_user_ids_json"]
assert.False(t, hasRecipients, "admin_email audience must not carry recipient ids")
}
func TestPublishForwardsValidationError(t *testing.T) {
client, _ := newRedis(t)
publisher, err := NewPublisher(Config{Client: client})
require.NoError(t, err)
bad := notificationintent.Intent{
NotificationType: notificationintent.NotificationTypeGameTurnReady,
Producer: notificationintent.ProducerGameMaster,
AudienceKind: notificationintent.AudienceKindUser,
IdempotencyKey: "k",
PayloadJSON: `{"game_id":"g","game_name":"x","turn_number":1}`,
}
require.Error(t, publisher.Publish(context.Background(), bad))
}
func TestPublishDefaultStream(t *testing.T) {
client, _ := newRedis(t)
publisher, err := NewPublisher(Config{Client: client, Stream: ""})
require.NoError(t, err)
intent, err := notificationintent.NewGameTurnReadyIntent(
notificationintent.Metadata{IdempotencyKey: "k", OccurredAt: time.UnixMilli(1).UTC()},
[]string{"u-1"},
notificationintent.GameTurnReadyPayload{GameID: "g", GameName: "n", TurnNumber: 1},
)
require.NoError(t, err)
require.NoError(t, publisher.Publish(context.Background(), intent))
messages := readStream(t, client, notificationintent.DefaultIntentsStream)
require.Len(t, messages, 1)
}