Files
galaxy-game/pkg/notificationintent/intent_test.go
T
2026-04-22 08:49:45 +02:00

299 lines
10 KiB
Go

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),
}
}