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) }