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 TestPublisherWritesIntent(t *testing.T) { client, _ := newRedis(t) publisher, err := NewPublisher(Config{Client: client, Stream: "notification:intents"}) require.NoError(t, err) intent, err := notificationintent.NewRuntimeImagePullFailedIntent( notificationintent.Metadata{ IdempotencyKey: "rtmanager:start:game-1:abc", OccurredAt: time.UnixMilli(1714200000000).UTC(), }, notificationintent.RuntimeImagePullFailedPayload{ GameID: "game-1", ImageRef: "galaxy/game:1.4.2", ErrorCode: "image_pull_failed", ErrorMessage: "registry timeout", AttemptedAtMs: 1714200000000, }, ) 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, "runtime.image_pull_failed", values["notification_type"]) assert.Equal(t, "runtime_manager", values["producer"]) assert.Equal(t, "admin_email", values["audience_kind"]) assert.Equal(t, "rtmanager:start:game-1:abc", values["idempotency_key"]) // recipient_user_ids_json must be absent for admin_email audience. _, hasRecipients := values["recipient_user_ids_json"] assert.False(t, hasRecipients) 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, "galaxy/game:1.4.2", payload["image_ref"]) } func TestPublisherForwardsValidationError(t *testing.T) { client, _ := newRedis(t) publisher, err := NewPublisher(Config{Client: client}) require.NoError(t, err) // Intent with a zero OccurredAt fails the shared validator. bad := notificationintent.Intent{ NotificationType: notificationintent.NotificationTypeRuntimeImagePullFailed, Producer: notificationintent.ProducerRuntimeManager, AudienceKind: notificationintent.AudienceKindAdminEmail, IdempotencyKey: "k", PayloadJSON: `{"game_id":"g","image_ref":"r","error_code":"c","error_message":"m","attempted_at_ms":1}`, } require.Error(t, publisher.Publish(context.Background(), bad)) } func TestPublisherDefaultsStreamName(t *testing.T) { client, _ := newRedis(t) publisher, err := NewPublisher(Config{Client: client, Stream: ""}) require.NoError(t, err) intent, err := notificationintent.NewRuntimeContainerStartFailedIntent( notificationintent.Metadata{ IdempotencyKey: "k", OccurredAt: time.UnixMilli(1714200000000).UTC(), }, notificationintent.RuntimeContainerStartFailedPayload{ GameID: "g", ImageRef: "r", ErrorCode: "container_start_failed", ErrorMessage: "boom", AttemptedAtMs: 1714200000000, }, ) require.NoError(t, err) require.NoError(t, publisher.Publish(context.Background(), intent)) messages := readStream(t, client, notificationintent.DefaultIntentsStream) require.Len(t, messages, 1) }