feat: user service
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
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{RaceName: "Nova Prime"}
|
||||
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 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",
|
||||
RaceName: "Pilot Nova",
|
||||
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")
|
||||
raceName := builder.CreateString("Pilot Nova")
|
||||
preferredLanguage := builder.CreateString("en")
|
||||
timeZone := builder.CreateString("Europe/Kaliningrad")
|
||||
|
||||
userfbs.AccountViewStart(builder)
|
||||
userfbs.AccountViewAddUserId(builder, userID)
|
||||
userfbs.AccountViewAddEmail(builder, email)
|
||||
userfbs.AccountViewAddRaceName(builder, raceName)
|
||||
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")
|
||||
raceName := builder.CreateString("Pilot Nova")
|
||||
preferredLanguage := builder.CreateString("en")
|
||||
timeZone := builder.CreateString("Europe/Kaliningrad")
|
||||
|
||||
userfbs.AccountViewStart(builder)
|
||||
userfbs.AccountViewAddUserId(builder, userID)
|
||||
userfbs.AccountViewAddEmail(builder, email)
|
||||
userfbs.AccountViewAddRaceName(builder, raceName)
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user