package notificationintent import ( "encoding/json" "testing" "time" "github.com/stretchr/testify/require" ) func TestConstructorsBuildExpectedIntentValues(t *testing.T) { t.Parallel() metadata := Metadata{ IdempotencyKey: "idempotency-1", OccurredAt: time.UnixMilli(1775121700000).Add(123 * time.Nanosecond), RequestID: "request-1", TraceID: "trace-1", } tests := []struct { name string build func() (Intent, error) notificationType NotificationType producer Producer audienceKind AudienceKind recipientUserIDs []string payloadJSON string }{ { name: "geo review recommended", build: func() (Intent, error) { return NewGeoReviewRecommendedIntent(metadata, GeoReviewRecommendedPayload{ UserID: "user-1", UserEmail: "pilot@example.com", ObservedCountry: "DE", UsualConnectionCountry: "PL", ReviewReason: "country_mismatch", }) }, notificationType: NotificationTypeGeoReviewRecommended, producer: ProducerGeoProfile, audienceKind: AudienceKindAdminEmail, payloadJSON: `{"user_id":"user-1","user_email":"pilot@example.com","observed_country":"DE","usual_connection_country":"PL","review_reason":"country_mismatch"}`, }, { name: "game turn ready", build: func() (Intent, error) { return NewGameTurnReadyIntent(metadata, []string{"user-2", "user-1"}, GameTurnReadyPayload{ GameID: "game-1", GameName: "Nebula Clash", TurnNumber: 54, }) }, notificationType: NotificationTypeGameTurnReady, producer: ProducerGameMaster, audienceKind: AudienceKindUser, recipientUserIDs: []string{"user-1", "user-2"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","turn_number":54}`, }, { name: "game finished", build: func() (Intent, error) { return NewGameFinishedIntent(metadata, []string{"user-1", "user-2"}, GameFinishedPayload{ GameID: "game-1", GameName: "Nebula Clash", FinalTurnNumber: 55, }) }, notificationType: NotificationTypeGameFinished, producer: ProducerGameMaster, audienceKind: AudienceKindUser, recipientUserIDs: []string{"user-1", "user-2"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","final_turn_number":55}`, }, { name: "game generation failed", build: func() (Intent, error) { return NewGameGenerationFailedIntent(metadata, GameGenerationFailedPayload{ GameID: "game-1", GameName: "Nebula Clash", FailureReason: "engine_timeout", }) }, notificationType: NotificationTypeGameGenerationFailed, producer: ProducerGameMaster, audienceKind: AudienceKindAdminEmail, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","failure_reason":"engine_timeout"}`, }, { name: "lobby runtime paused after start", build: func() (Intent, error) { return NewLobbyRuntimePausedAfterStartIntent(metadata, LobbyRuntimePausedAfterStartPayload{ GameID: "game-1", GameName: "Nebula Clash", }) }, notificationType: NotificationTypeLobbyRuntimePausedAfterStart, producer: ProducerGameLobby, audienceKind: AudienceKindAdminEmail, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash"}`, }, { name: "private lobby application submitted", build: func() (Intent, error) { return NewPrivateLobbyApplicationSubmittedIntent(metadata, "owner-1", LobbyApplicationSubmittedPayload{ GameID: "game-1", GameName: "Nebula Clash", ApplicantUserID: "user-2", ApplicantName: "Nova Pilot", }) }, notificationType: NotificationTypeLobbyApplicationSubmitted, producer: ProducerGameLobby, audienceKind: AudienceKindUser, recipientUserIDs: []string{"owner-1"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","applicant_user_id":"user-2","applicant_name":"Nova Pilot"}`, }, { name: "public lobby application submitted", build: func() (Intent, error) { return NewPublicLobbyApplicationSubmittedIntent(metadata, LobbyApplicationSubmittedPayload{ GameID: "game-1", GameName: "Nebula Clash", ApplicantUserID: "user-2", ApplicantName: "Nova Pilot", }) }, notificationType: NotificationTypeLobbyApplicationSubmitted, producer: ProducerGameLobby, audienceKind: AudienceKindAdminEmail, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","applicant_user_id":"user-2","applicant_name":"Nova Pilot"}`, }, { name: "lobby membership approved", build: func() (Intent, error) { return NewLobbyMembershipApprovedIntent(metadata, "applicant-1", LobbyMembershipApprovedPayload{ GameID: "game-1", GameName: "Nebula Clash", }) }, notificationType: NotificationTypeLobbyMembershipApproved, producer: ProducerGameLobby, audienceKind: AudienceKindUser, recipientUserIDs: []string{"applicant-1"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash"}`, }, { name: "lobby membership rejected", build: func() (Intent, error) { return NewLobbyMembershipRejectedIntent(metadata, "applicant-1", LobbyMembershipRejectedPayload{ GameID: "game-1", GameName: "Nebula Clash", }) }, notificationType: NotificationTypeLobbyMembershipRejected, producer: ProducerGameLobby, audienceKind: AudienceKindUser, recipientUserIDs: []string{"applicant-1"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash"}`, }, { name: "lobby invite created", build: func() (Intent, error) { return NewLobbyInviteCreatedIntent(metadata, "invited-1", LobbyInviteCreatedPayload{ GameID: "game-1", GameName: "Nebula Clash", InviterUserID: "owner-1", InviterName: "Owner Pilot", }) }, notificationType: NotificationTypeLobbyInviteCreated, producer: ProducerGameLobby, audienceKind: AudienceKindUser, recipientUserIDs: []string{"invited-1"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","inviter_user_id":"owner-1","inviter_name":"Owner Pilot"}`, }, { name: "lobby invite redeemed", build: func() (Intent, error) { return NewLobbyInviteRedeemedIntent(metadata, "owner-1", LobbyInviteRedeemedPayload{ GameID: "game-1", GameName: "Nebula Clash", InviteeUserID: "invitee-1", InviteeName: "Nova Pilot", }) }, notificationType: NotificationTypeLobbyInviteRedeemed, producer: ProducerGameLobby, audienceKind: AudienceKindUser, recipientUserIDs: []string{"owner-1"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","invitee_user_id":"invitee-1","invitee_name":"Nova Pilot"}`, }, { name: "lobby invite expired", build: func() (Intent, error) { return NewLobbyInviteExpiredIntent(metadata, "owner-1", LobbyInviteExpiredPayload{ GameID: "game-1", GameName: "Nebula Clash", InviteeUserID: "invitee-1", InviteeName: "Nova Pilot", }) }, notificationType: NotificationTypeLobbyInviteExpired, producer: ProducerGameLobby, audienceKind: AudienceKindUser, recipientUserIDs: []string{"owner-1"}, payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","invitee_user_id":"invitee-1","invitee_name":"Nova Pilot"}`, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() intent, err := tt.build() require.NoError(t, err) require.Equal(t, tt.notificationType, intent.NotificationType) require.Equal(t, tt.producer, intent.Producer) require.Equal(t, tt.audienceKind, intent.AudienceKind) require.Equal(t, tt.recipientUserIDs, intent.RecipientUserIDs) values, err := intent.Values() require.NoError(t, err) require.Equal(t, tt.notificationType.String(), values[fieldNotificationType]) require.Equal(t, tt.producer.String(), values[fieldProducer]) require.Equal(t, tt.audienceKind.String(), values[fieldAudienceKind]) require.Equal(t, metadata.IdempotencyKey, values[fieldIdempotencyKey]) require.Equal(t, "1775121700000", values[fieldOccurredAtMS]) require.Equal(t, metadata.RequestID, values[fieldRequestID]) require.Equal(t, metadata.TraceID, values[fieldTraceID]) require.JSONEq(t, tt.payloadJSON, values[fieldPayloadJSON].(string)) if len(tt.recipientUserIDs) == 0 { require.NotContains(t, values, fieldRecipientUserIDs) return } var recipientUserIDs []string require.NoError(t, json.Unmarshal([]byte(values[fieldRecipientUserIDs].(string)), &recipientUserIDs)) require.Equal(t, tt.recipientUserIDs, recipientUserIDs) }) } } func TestUserRecipientConstructorsRejectDuplicates(t *testing.T) { t.Parallel() _, err := NewGameTurnReadyIntent(defaultMetadata(), []string{"user-1", "user-1"}, GameTurnReadyPayload{ GameID: "game-1", GameName: "Nebula Clash", TurnNumber: 54, }) require.Error(t, err) require.Contains(t, err.Error(), "duplicates user id") } func TestConstructorsRejectInvalidPayloads(t *testing.T) { t.Parallel() _, err := NewGameTurnReadyIntent(defaultMetadata(), []string{"user-1"}, GameTurnReadyPayload{ GameName: "Nebula Clash", TurnNumber: 54, }) require.Error(t, err) require.Contains(t, err.Error(), "payload_json.game_id must not be empty") _, err = NewGameTurnReadyIntent(defaultMetadata(), []string{"user-1"}, GameTurnReadyPayload{ GameID: "game-1", GameName: "Nebula Clash", TurnNumber: 0, }) require.Error(t, err) require.Contains(t, err.Error(), "payload_json.turn_number must be at least 1") } func TestDecodeIntentRejectsMissingRequiredTopLevelField(t *testing.T) { t.Parallel() _, err := DecodeIntent(map[string]any{ fieldNotificationType: NotificationTypeGameTurnReady.String(), fieldProducer: ProducerGameMaster.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-1"]`, fieldIdempotencyKey: "game-1:turn-54", fieldOccurredAtMS: "1775121700000", }) require.Error(t, err) require.Contains(t, err.Error(), fieldPayloadJSON) } func defaultMetadata() Metadata { return Metadata{ IdempotencyKey: "idempotency-1", OccurredAt: time.UnixMilli(1775121700000), } }