package transcoder import ( "reflect" "strconv" "strings" "testing" "time" usermodel "galaxy/model/user" userfbs "galaxy/schema/fbs/user" flatbuffers "github.com/google/flatbuffers/go" ) func TestUserRequestPayloadRoundTrips(t *testing.T) { t.Parallel() getPayload, err := GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{}) if err != nil { t.Fatalf("encode get my account request: %v", err) } getDecoded, err := PayloadToGetMyAccountRequest(getPayload) if err != nil { t.Fatalf("decode get my account request: %v", err) } if !reflect.DeepEqual(&usermodel.GetMyAccountRequest{}, getDecoded) { t.Fatalf("get my account request mismatch: %#v", getDecoded) } profileSource := &usermodel.UpdateMyProfileRequest{DisplayName: "NovaPrime"} profilePayload, err := UpdateMyProfileRequestToPayload(profileSource) if err != nil { t.Fatalf("encode update my profile request: %v", err) } profileDecoded, err := PayloadToUpdateMyProfileRequest(profilePayload) if err != nil { t.Fatalf("decode update my profile request: %v", err) } if !reflect.DeepEqual(profileSource, profileDecoded) { t.Fatalf("update my profile request mismatch\nsource: %#v\ndecoded:%#v", profileSource, profileDecoded) } settingsSource := &usermodel.UpdateMySettingsRequest{ PreferredLanguage: "en-US", TimeZone: "Europe/Kaliningrad", } settingsPayload, err := UpdateMySettingsRequestToPayload(settingsSource) if err != nil { t.Fatalf("encode update my settings request: %v", err) } settingsDecoded, err := PayloadToUpdateMySettingsRequest(settingsPayload) if err != nil { t.Fatalf("decode update my settings request: %v", err) } if !reflect.DeepEqual(settingsSource, settingsDecoded) { t.Fatalf("update my settings request mismatch\nsource: %#v\ndecoded:%#v", settingsSource, settingsDecoded) } } func TestUserSessionsPayloadRoundTrips(t *testing.T) { t.Parallel() emptyList, err := ListMySessionsRequestToPayload(&usermodel.ListMySessionsRequest{}) if err != nil { t.Fatalf("encode list-my-sessions request: %v", err) } if _, err := PayloadToListMySessionsRequest(emptyList); err != nil { t.Fatalf("decode list-my-sessions request: %v", err) } revokeAll, err := RevokeAllMySessionsRequestToPayload(&usermodel.RevokeAllMySessionsRequest{}) if err != nil { t.Fatalf("encode revoke-all-my-sessions request: %v", err) } if _, err := PayloadToRevokeAllMySessionsRequest(revokeAll); err != nil { t.Fatalf("decode revoke-all-my-sessions request: %v", err) } revokeReq := &usermodel.RevokeMySessionRequest{DeviceSessionID: "device-7c8f"} revokePayload, err := RevokeMySessionRequestToPayload(revokeReq) if err != nil { t.Fatalf("encode revoke-my-session request: %v", err) } revokeDecoded, err := PayloadToRevokeMySessionRequest(revokePayload) if err != nil { t.Fatalf("decode revoke-my-session request: %v", err) } if !reflect.DeepEqual(revokeReq, revokeDecoded) { t.Fatalf("revoke-my-session request mismatch\nsource: %#v\ndecoded:%#v", revokeReq, revokeDecoded) } now := time.Date(2026, time.April, 9, 10, 0, 0, 0, time.UTC) revokedAt := now.Add(time.Minute) lastSeenAt := now.Add(time.Second) listResp := &usermodel.ListMySessionsResponse{ Items: []usermodel.DeviceSession{ { DeviceSessionID: "ds-1", UserID: "user-1", Status: "active", ClientPublicKey: "AAAAAAAAAAA=", CreatedAt: now, LastSeenAt: &lastSeenAt, }, { DeviceSessionID: "ds-2", UserID: "user-1", Status: "revoked", CreatedAt: now, RevokedAt: &revokedAt, }, }, } listPayload, err := ListMySessionsResponseToPayload(listResp) if err != nil { t.Fatalf("encode list-my-sessions response: %v", err) } listDecoded, err := PayloadToListMySessionsResponse(listPayload) if err != nil { t.Fatalf("decode list-my-sessions response: %v", err) } if !reflect.DeepEqual(listResp, listDecoded) { t.Fatalf("list-my-sessions response mismatch\nsource: %#v\ndecoded:%#v", listResp, listDecoded) } revokeResp := &usermodel.RevokeMySessionResponse{ Session: usermodel.DeviceSession{ DeviceSessionID: "ds-1", UserID: "user-1", Status: "revoked", CreatedAt: now, RevokedAt: &revokedAt, }, } revokeRespPayload, err := RevokeMySessionResponseToPayload(revokeResp) if err != nil { t.Fatalf("encode revoke-my-session response: %v", err) } revokeRespDecoded, err := PayloadToRevokeMySessionResponse(revokeRespPayload) if err != nil { t.Fatalf("decode revoke-my-session response: %v", err) } if !reflect.DeepEqual(revokeResp, revokeRespDecoded) { t.Fatalf("revoke-my-session response mismatch\nsource: %#v\ndecoded:%#v", revokeResp, revokeRespDecoded) } revokeAllResp := &usermodel.RevokeAllMySessionsResponse{ Summary: usermodel.DeviceSessionRevocationSummary{ UserID: "user-1", RevokedCount: 3, }, } revokeAllPayload, err := RevokeAllMySessionsResponseToPayload(revokeAllResp) if err != nil { t.Fatalf("encode revoke-all-my-sessions response: %v", err) } revokeAllDecoded, err := PayloadToRevokeAllMySessionsResponse(revokeAllPayload) if err != nil { t.Fatalf("decode revoke-all-my-sessions response: %v", err) } if !reflect.DeepEqual(revokeAllResp, revokeAllDecoded) { t.Fatalf("revoke-all-my-sessions response mismatch\nsource: %#v\ndecoded:%#v", revokeAllResp, revokeAllDecoded) } } func TestAccountResponsePayloadRoundTrip(t *testing.T) { t.Parallel() now := time.Date(2026, time.April, 9, 10, 0, 0, 0, time.UTC) expiresAt := now.Add(30 * 24 * time.Hour) limitExpiresAt := now.Add(90 * 24 * time.Hour) source := &usermodel.AccountResponse{ Account: usermodel.Account{ UserID: "user-123", Email: "pilot@example.com", UserName: "player-abcdefgh", DisplayName: "PilotNova", PreferredLanguage: "en", TimeZone: "Europe/Kaliningrad", DeclaredCountry: "DE", Entitlement: usermodel.EntitlementSnapshot{ PlanCode: "paid_monthly", IsPaid: true, Source: "billing", Actor: usermodel.ActorRef{Type: "billing", ID: "invoice-1"}, ReasonCode: "renewal", StartsAt: now, EndsAt: &expiresAt, UpdatedAt: now, }, ActiveSanctions: []usermodel.ActiveSanction{ { SanctionCode: "profile_update_block", Scope: "lobby", ReasonCode: "manual_block", Actor: usermodel.ActorRef{Type: "admin", ID: "admin-1"}, AppliedAt: now, ExpiresAt: &expiresAt, }, }, ActiveLimits: []usermodel.ActiveLimit{ { LimitCode: "max_owned_private_games", Value: 3, ReasonCode: "manual_override", Actor: usermodel.ActorRef{Type: "admin", ID: "admin-1"}, AppliedAt: now, ExpiresAt: &limitExpiresAt, }, }, CreatedAt: now, UpdatedAt: now.Add(time.Hour), }, } payload, err := AccountResponseToPayload(source) if err != nil { t.Fatalf("encode account response: %v", err) } decoded, err := PayloadToAccountResponse(payload) if err != nil { t.Fatalf("decode account response: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("account response mismatch\nsource: %#v\ndecoded:%#v", source, decoded) } } func TestErrorResponsePayloadRoundTrip(t *testing.T) { t.Parallel() source := &usermodel.ErrorResponse{ Error: usermodel.ErrorBody{ Code: "conflict", Message: "request conflicts with current state", }, } payload, err := ErrorResponseToPayload(source) if err != nil { t.Fatalf("encode error response: %v", err) } decoded, err := PayloadToErrorResponse(payload) if err != nil { t.Fatalf("decode error response: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("error response mismatch\nsource: %#v\ndecoded:%#v", source, decoded) } } func TestUserPayloadEncodersRejectNilInputs(t *testing.T) { t.Parallel() tests := []struct { name string call func() error }{ { name: "get my account request", call: func() error { _, err := GetMyAccountRequestToPayload(nil) return err }, }, { name: "update my profile request", call: func() error { _, err := UpdateMyProfileRequestToPayload(nil) return err }, }, { name: "update my settings request", call: func() error { _, err := UpdateMySettingsRequestToPayload(nil) return err }, }, { name: "account response", call: func() error { _, err := AccountResponseToPayload(nil) return err }, }, { name: "error response", call: func() error { _, err := ErrorResponseToPayload(nil) return err }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if err := tt.call(); err == nil { t.Fatal("expected error") } }) } } func TestUserPayloadDecodersRejectEmptyPayloads(t *testing.T) { t.Parallel() tests := []struct { name string call func() error }{ { name: "get my account request", call: func() error { _, err := PayloadToGetMyAccountRequest(nil) return err }, }, { name: "update my profile request", call: func() error { _, err := PayloadToUpdateMyProfileRequest(nil) return err }, }, { name: "update my settings request", call: func() error { _, err := PayloadToUpdateMySettingsRequest(nil) return err }, }, { name: "account response", call: func() error { _, err := PayloadToAccountResponse(nil) return err }, }, { name: "error response", call: func() error { _, err := PayloadToErrorResponse(nil) return err }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if err := tt.call(); err == nil { t.Fatal("expected error") } }) } } func TestUserPayloadDecodersRecoverFromGarbagePayloads(t *testing.T) { t.Parallel() tests := []struct { name string call func() error }{ { name: "get my account request", call: func() error { _, err := PayloadToGetMyAccountRequest([]byte{0x01, 0x02, 0x03}) return err }, }, { name: "update my profile request", call: func() error { _, err := PayloadToUpdateMyProfileRequest([]byte{0x01, 0x02, 0x03}) return err }, }, { name: "update my settings request", call: func() error { _, err := PayloadToUpdateMySettingsRequest([]byte{0x01, 0x02, 0x03}) return err }, }, { name: "account response", call: func() error { _, err := PayloadToAccountResponse([]byte{0x01, 0x02, 0x03}) return err }, }, { name: "error response", call: func() error { _, err := PayloadToErrorResponse([]byte{0x01, 0x02, 0x03}) return err }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if err := tt.call(); err == nil { t.Fatal("expected error") } }) } } func TestPayloadToAccountResponseRejectsMissingAccount(t *testing.T) { t.Parallel() builder := flatbuffers.NewBuilder(64) userfbs.AccountResponseStart(builder) offset := userfbs.AccountResponseEnd(builder) userfbs.FinishAccountResponseBuffer(builder, offset) _, err := PayloadToAccountResponse(builder.FinishedBytes()) if err == nil { t.Fatal("expected error for missing account") } if !strings.Contains(err.Error(), "account is missing") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToAccountResponseRejectsMissingEntitlement(t *testing.T) { t.Parallel() payload := buildAccountResponsePayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { userID := builder.CreateString("user-123") email := builder.CreateString("pilot@example.com") userName := builder.CreateString("player-abcdefgh") preferredLanguage := builder.CreateString("en") timeZone := builder.CreateString("Europe/Kaliningrad") userfbs.AccountViewStart(builder) userfbs.AccountViewAddUserId(builder, userID) userfbs.AccountViewAddEmail(builder, email) userfbs.AccountViewAddUserName(builder, userName) userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage) userfbs.AccountViewAddTimeZone(builder, timeZone) userfbs.AccountViewAddCreatedAtMs(builder, 1) userfbs.AccountViewAddUpdatedAtMs(builder, 2) return userfbs.AccountViewEnd(builder) }) _, err := PayloadToAccountResponse(payload) if err == nil { t.Fatal("expected error for missing entitlement") } if !strings.Contains(err.Error(), "entitlement is missing") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToAccountResponseRejectsOverflowLimitValue(t *testing.T) { t.Parallel() if strconv.IntSize == 64 { t.Skip("int overflow from int64 is not possible on 64-bit runtime") } maxInt := int64(int(^uint(0) >> 1)) overflow := maxInt + 1 nowMS := int64(1) payload := buildAccountResponsePayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT { actorType := builder.CreateString("admin") userfbs.ActorRefStart(builder) userfbs.ActorRefAddType(builder, actorType) actorOffset := userfbs.ActorRefEnd(builder) planCode := builder.CreateString("free") source := builder.CreateString("auth_registration") reasonCode := builder.CreateString("initial_free_entitlement") userfbs.EntitlementSnapshotStart(builder) userfbs.EntitlementSnapshotAddPlanCode(builder, planCode) userfbs.EntitlementSnapshotAddSource(builder, source) userfbs.EntitlementSnapshotAddActor(builder, actorOffset) userfbs.EntitlementSnapshotAddReasonCode(builder, reasonCode) userfbs.EntitlementSnapshotAddStartsAtMs(builder, nowMS) userfbs.EntitlementSnapshotAddUpdatedAtMs(builder, nowMS) entitlementOffset := userfbs.EntitlementSnapshotEnd(builder) limitCode := builder.CreateString("max_owned_private_games") limitReasonCode := builder.CreateString("manual_override") userfbs.ActiveLimitStart(builder) userfbs.ActiveLimitAddLimitCode(builder, limitCode) userfbs.ActiveLimitAddValue(builder, overflow) userfbs.ActiveLimitAddReasonCode(builder, limitReasonCode) userfbs.ActiveLimitAddActor(builder, actorOffset) userfbs.ActiveLimitAddAppliedAtMs(builder, nowMS) limitOffset := userfbs.ActiveLimitEnd(builder) userfbs.AccountViewStartActiveLimitsVector(builder, 1) builder.PrependUOffsetT(limitOffset) limitsVector := builder.EndVector(1) userID := builder.CreateString("user-123") email := builder.CreateString("pilot@example.com") userName := builder.CreateString("player-abcdefgh") preferredLanguage := builder.CreateString("en") timeZone := builder.CreateString("Europe/Kaliningrad") userfbs.AccountViewStart(builder) userfbs.AccountViewAddUserId(builder, userID) userfbs.AccountViewAddEmail(builder, email) userfbs.AccountViewAddUserName(builder, userName) userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage) userfbs.AccountViewAddTimeZone(builder, timeZone) userfbs.AccountViewAddEntitlement(builder, entitlementOffset) userfbs.AccountViewAddActiveLimits(builder, limitsVector) userfbs.AccountViewAddCreatedAtMs(builder, nowMS) userfbs.AccountViewAddUpdatedAtMs(builder, nowMS) return userfbs.AccountViewEnd(builder) }) _, err := PayloadToAccountResponse(payload) if err == nil { t.Fatal("expected overflow error") } if !strings.Contains(err.Error(), "overflows int") { t.Fatalf("unexpected error: %v", err) } } func TestPayloadToErrorResponseRejectsMissingError(t *testing.T) { t.Parallel() builder := flatbuffers.NewBuilder(64) userfbs.ErrorResponseStart(builder) offset := userfbs.ErrorResponseEnd(builder) userfbs.FinishErrorResponseBuffer(builder, offset) _, err := PayloadToErrorResponse(builder.FinishedBytes()) if err == nil { t.Fatal("expected error for missing error body") } if !strings.Contains(err.Error(), "error is missing") { t.Fatalf("unexpected error: %v", err) } } func buildAccountResponsePayload(accountBuilder func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte { builder := flatbuffers.NewBuilder(256) accountOffset := accountBuilder(builder) userfbs.AccountResponseStart(builder) userfbs.AccountResponseAddAccount(builder, accountOffset) responseOffset := userfbs.AccountResponseEnd(builder) userfbs.FinishAccountResponseBuffer(builder, responseOffset) return builder.FinishedBytes() }