Files
2026-05-07 00:58:53 +03:00

577 lines
16 KiB
Go

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