Files
galaxy-game/notification/internal/api/intentstream/contract_test.go
T
2026-04-22 08:49:45 +02:00

146 lines
5.2 KiB
Go

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