429 lines
16 KiB
Go
429 lines
16 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 membership blocked",
|
|
build: func() (Intent, error) {
|
|
return NewLobbyMembershipBlockedIntent(metadata, "owner-1", LobbyMembershipBlockedPayload{
|
|
GameID: "game-1",
|
|
GameName: "Nebula Clash",
|
|
MembershipUserID: "user-2",
|
|
MembershipUserName: "player-aabbccdd",
|
|
Reason: "permanent_blocked",
|
|
})
|
|
},
|
|
notificationType: NotificationTypeLobbyMembershipBlocked,
|
|
producer: ProducerGameLobby,
|
|
audienceKind: AudienceKindUser,
|
|
recipientUserIDs: []string{"owner-1"},
|
|
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","membership_user_id":"user-2","membership_user_name":"player-aabbccdd","reason":"permanent_blocked"}`,
|
|
},
|
|
{
|
|
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"}`,
|
|
},
|
|
{
|
|
name: "lobby race name registration eligible",
|
|
build: func() (Intent, error) {
|
|
return NewLobbyRaceNameRegistrationEligibleIntent(metadata, "user-7", LobbyRaceNameRegistrationEligiblePayload{
|
|
GameID: "game-1",
|
|
GameName: "Nebula Clash",
|
|
RaceName: "Skylancer",
|
|
EligibleUntilMs: 1775208100000,
|
|
})
|
|
},
|
|
notificationType: NotificationTypeLobbyRaceNameRegistrationEligible,
|
|
producer: ProducerGameLobby,
|
|
audienceKind: AudienceKindUser,
|
|
recipientUserIDs: []string{"user-7"},
|
|
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","race_name":"Skylancer","eligible_until_ms":1775208100000}`,
|
|
},
|
|
{
|
|
name: "lobby race name registered",
|
|
build: func() (Intent, error) {
|
|
return NewLobbyRaceNameRegisteredIntent(metadata, "user-8", LobbyRaceNameRegisteredPayload{
|
|
RaceName: "Skylancer",
|
|
})
|
|
},
|
|
notificationType: NotificationTypeLobbyRaceNameRegistered,
|
|
producer: ProducerGameLobby,
|
|
audienceKind: AudienceKindUser,
|
|
recipientUserIDs: []string{"user-8"},
|
|
payloadJSON: `{"race_name":"Skylancer"}`,
|
|
},
|
|
{
|
|
name: "lobby race name registration denied",
|
|
build: func() (Intent, error) {
|
|
return NewLobbyRaceNameRegistrationDeniedIntent(metadata, "user-9", LobbyRaceNameRegistrationDeniedPayload{
|
|
GameID: "game-1",
|
|
GameName: "Nebula Clash",
|
|
RaceName: "Skylancer",
|
|
Reason: "capability_not_met",
|
|
})
|
|
},
|
|
notificationType: NotificationTypeLobbyRaceNameRegistrationDenied,
|
|
producer: ProducerGameLobby,
|
|
audienceKind: AudienceKindUser,
|
|
recipientUserIDs: []string{"user-9"},
|
|
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","race_name":"Skylancer","reason":"capability_not_met"}`,
|
|
},
|
|
{
|
|
name: "runtime image pull failed",
|
|
build: func() (Intent, error) {
|
|
return NewRuntimeImagePullFailedIntent(metadata, RuntimeImagePullFailedPayload{
|
|
GameID: "game-1",
|
|
ImageRef: "galaxy/game:1.4.7",
|
|
ErrorCode: "image_pull_failed",
|
|
ErrorMessage: "manifest unknown",
|
|
AttemptedAtMs: 1775121700000,
|
|
})
|
|
},
|
|
notificationType: NotificationTypeRuntimeImagePullFailed,
|
|
producer: ProducerRuntimeManager,
|
|
audienceKind: AudienceKindAdminEmail,
|
|
payloadJSON: `{"game_id":"game-1","image_ref":"galaxy/game:1.4.7","error_code":"image_pull_failed","error_message":"manifest unknown","attempted_at_ms":1775121700000}`,
|
|
},
|
|
{
|
|
name: "runtime container start failed",
|
|
build: func() (Intent, error) {
|
|
return NewRuntimeContainerStartFailedIntent(metadata, RuntimeContainerStartFailedPayload{
|
|
GameID: "game-1",
|
|
ImageRef: "galaxy/game:1.4.7",
|
|
ErrorCode: "container_start_failed",
|
|
ErrorMessage: "OCI runtime create failed",
|
|
AttemptedAtMs: 1775121700001,
|
|
})
|
|
},
|
|
notificationType: NotificationTypeRuntimeContainerStartFailed,
|
|
producer: ProducerRuntimeManager,
|
|
audienceKind: AudienceKindAdminEmail,
|
|
payloadJSON: `{"game_id":"game-1","image_ref":"galaxy/game:1.4.7","error_code":"container_start_failed","error_message":"OCI runtime create failed","attempted_at_ms":1775121700001}`,
|
|
},
|
|
{
|
|
name: "runtime start config invalid",
|
|
build: func() (Intent, error) {
|
|
return NewRuntimeStartConfigInvalidIntent(metadata, RuntimeStartConfigInvalidPayload{
|
|
GameID: "game-1",
|
|
ImageRef: "galaxy/game:1.4.7",
|
|
ErrorCode: "start_config_invalid",
|
|
ErrorMessage: "docker network galaxy-net not found",
|
|
AttemptedAtMs: 1775121700002,
|
|
})
|
|
},
|
|
notificationType: NotificationTypeRuntimeStartConfigInvalid,
|
|
producer: ProducerRuntimeManager,
|
|
audienceKind: AudienceKindAdminEmail,
|
|
payloadJSON: `{"game_id":"game-1","image_ref":"galaxy/game:1.4.7","error_code":"start_config_invalid","error_message":"docker network galaxy-net not found","attempted_at_ms":1775121700002}`,
|
|
},
|
|
}
|
|
|
|
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")
|
|
|
|
_, err = NewRuntimeImagePullFailedIntent(defaultMetadata(), RuntimeImagePullFailedPayload{
|
|
GameID: "game-1",
|
|
ImageRef: "galaxy/game:1.4.7",
|
|
ErrorCode: "",
|
|
ErrorMessage: "manifest unknown",
|
|
AttemptedAtMs: 1775121700000,
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "payload_json.error_code must not be empty")
|
|
|
|
_, err = NewRuntimeContainerStartFailedIntent(defaultMetadata(), RuntimeContainerStartFailedPayload{
|
|
GameID: "game-1",
|
|
ImageRef: "galaxy/game:1.4.7",
|
|
ErrorCode: "container_start_failed",
|
|
ErrorMessage: "OCI runtime create failed",
|
|
AttemptedAtMs: 0,
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "payload_json.attempted_at_ms 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),
|
|
}
|
|
}
|