feat: notification service
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user