package intentstream import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestDecodeIntentNormalizesUserRecipientsAndPayload(t *testing.T) { t.Parallel() fields := map[string]any{ fieldNotificationType: NotificationTypeGameTurnReady.String(), fieldProducer: ProducerGameMaster.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-2","user-1"]`, fieldIdempotencyKey: "game-123:turn-54", fieldOccurredAtMS: "1775121700000", fieldPayloadJSON: `{"turn_number":54,"game_name":"Nebula Clash","game_id":"game-123"}`, fieldRequestID: "request-123", fieldTraceID: "trace-123", } intent, err := DecodeIntent(fields) require.NoError(t, err) require.Equal(t, []string{"user-1", "user-2"}, intent.RecipientUserIDs) require.Equal(t, `{"game_id":"game-123","game_name":"Nebula Clash","turn_number":54}`, intent.PayloadJSON) require.Equal(t, time.UnixMilli(1775121700000).UTC(), intent.OccurredAt) } func TestDecodeIntentCanonicalizesEquivalentPayloadJSON(t *testing.T) { t.Parallel() fieldsA := map[string]any{ fieldNotificationType: NotificationTypeGameFinished.String(), fieldProducer: ProducerGameMaster.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-1"]`, fieldIdempotencyKey: "game-123:finished", fieldOccurredAtMS: "1775121700001", fieldPayloadJSON: `{"game_id":"game-123","game_name":"Nebula Clash","final_turn_number":54}`, } fieldsB := map[string]any{ fieldNotificationType: NotificationTypeGameFinished.String(), fieldProducer: ProducerGameMaster.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-1"]`, fieldIdempotencyKey: "game-123:finished", fieldOccurredAtMS: "1775121709999", fieldPayloadJSON: `{"final_turn_number":54,"game_name":"Nebula Clash","game_id":"game-123"}`, } intentA, err := DecodeIntent(fieldsA) require.NoError(t, err) intentB, err := DecodeIntent(fieldsB) require.NoError(t, err) require.Equal(t, intentA.PayloadJSON, intentB.PayloadJSON) } func TestDecodeIntentRejectsUnsupportedTopLevelField(t *testing.T) { t.Parallel() fields := map[string]any{ fieldNotificationType: NotificationTypeGameTurnReady.String(), fieldProducer: ProducerGameMaster.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-1"]`, fieldIdempotencyKey: "game-123:turn-54", fieldOccurredAtMS: "1775121700000", fieldPayloadJSON: `{"game_id":"game-123","game_name":"Nebula Clash","turn_number":54}`, "unexpected": "boom", } _, err := DecodeIntent(fields) require.Error(t, err) require.Contains(t, err.Error(), "unsupported fields") require.Equal(t, malformedFailureCodeInvalidIntent(), string(ClassifyDecodeError(err))) } func TestDecodeIntentRejectsDuplicateRecipientUserIDs(t *testing.T) { t.Parallel() fields := map[string]any{ fieldNotificationType: NotificationTypeGameTurnReady.String(), fieldProducer: ProducerGameMaster.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-1","user-1"]`, fieldIdempotencyKey: "game-123:turn-54", fieldOccurredAtMS: "1775121700000", fieldPayloadJSON: `{"game_id":"game-123","game_name":"Nebula Clash","turn_number":54}`, } _, err := DecodeIntent(fields) require.Error(t, err) require.Contains(t, err.Error(), "duplicates user id") require.Equal(t, malformedFailureCodeInvalidIntent(), string(ClassifyDecodeError(err))) } func TestDecodeIntentRejectsInvalidPayloadJSON(t *testing.T) { t.Parallel() fields := map[string]any{ fieldNotificationType: NotificationTypeLobbyInviteCreated.String(), fieldProducer: ProducerGameLobby.String(), fieldAudienceKind: AudienceKindUser.String(), fieldRecipientUserIDs: `["user-1"]`, fieldIdempotencyKey: "invite-created:user-1", fieldOccurredAtMS: "1775121700000", fieldPayloadJSON: `{"game_id":"game-123","game_name":"Nebula Clash","inviter_user_id":"user-2"}`, } _, err := DecodeIntent(fields) require.Error(t, err) require.Contains(t, err.Error(), "payload_json.inviter_name is required") require.Equal(t, malformedFailureCodeInvalidPayload(), string(ClassifyDecodeError(err))) } func TestDecodeIntentRejectsAdminRecipientsField(t *testing.T) { t.Parallel() fields := map[string]any{ fieldNotificationType: NotificationTypeGeoReviewRecommended.String(), fieldProducer: ProducerGeoProfile.String(), fieldAudienceKind: AudienceKindAdminEmail.String(), fieldRecipientUserIDs: `["user-1"]`, fieldIdempotencyKey: "geo:user-1", fieldOccurredAtMS: "1775121700000", fieldPayloadJSON: `{"user_id":"user-1","user_email":"pilot@example.com","observed_country":"DE","usual_connection_country":"PL","review_reason":"country_mismatch"}`, } _, err := DecodeIntent(fields) require.Error(t, err) require.Contains(t, err.Error(), "must not be present") require.Equal(t, malformedFailureCodeInvalidIntent(), string(ClassifyDecodeError(err))) } func malformedFailureCodeInvalidIntent() string { return "invalid_intent" } func malformedFailureCodeInvalidPayload() string { return "invalid_payload" }