package transcoder import ( "reflect" "testing" "time" lobbymodel "galaxy/model/lobby" ) // fixedTimes returns deterministic UTC timestamps for round-trip // fixtures. Millisecond precision matches the FlatBuffers `*Ms` ints, // which is what the transcoder preserves. func fixedTimes() (created, updated, ends time.Time) { created = time.Date(2026, time.May, 7, 9, 30, 15, 123_000_000, time.UTC) updated = created.Add(2 * time.Minute) ends = created.Add(48 * time.Hour) return } func TestLobbyMyGamesListRoundTrip(t *testing.T) { t.Parallel() created, updated, ends := fixedTimes() source := &lobbymodel.MyGamesListResponse{ Items: []lobbymodel.GameSummary{ { GameID: "game-private-7c8f", GameName: "First Contact", GameType: "private", Status: "running", OwnerUserID: "user-9912", MinPlayers: 2, MaxPlayers: 8, EnrollmentEndsAt: ends, CreatedAt: created, UpdatedAt: updated, CurrentTurn: 7, }, { GameID: "game-public-aabb", GameName: "Open Lobby", GameType: "public", Status: "enrollment_open", OwnerUserID: "", MinPlayers: 4, MaxPlayers: 12, EnrollmentEndsAt: ends, CreatedAt: created, UpdatedAt: updated, CurrentTurn: 0, }, }, } payload, err := MyGamesListResponseToPayload(source) if err != nil { t.Fatalf("encode: %v", err) } decoded, err := PayloadToMyGamesListResponse(payload) if err != nil { t.Fatalf("decode: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded) } requestBytes, err := MyGamesListRequestToPayload(&lobbymodel.MyGamesListRequest{}) if err != nil { t.Fatalf("encode request: %v", err) } if _, err := PayloadToMyGamesListRequest(requestBytes); err != nil { t.Fatalf("decode request: %v", err) } } func TestLobbyMyGamesListEmpty(t *testing.T) { t.Parallel() payload, err := MyGamesListResponseToPayload(&lobbymodel.MyGamesListResponse{Items: nil}) if err != nil { t.Fatalf("encode: %v", err) } decoded, err := PayloadToMyGamesListResponse(payload) if err != nil { t.Fatalf("decode: %v", err) } if got := len(decoded.Items); got != 0 { t.Fatalf("expected empty items, got %d", got) } } func TestLobbyPublicGamesListRoundTrip(t *testing.T) { t.Parallel() created, updated, ends := fixedTimes() requestSource := &lobbymodel.PublicGamesListRequest{Page: 3, PageSize: 25} requestBytes, err := PublicGamesListRequestToPayload(requestSource) if err != nil { t.Fatalf("encode request: %v", err) } requestDecoded, err := PayloadToPublicGamesListRequest(requestBytes) if err != nil { t.Fatalf("decode request: %v", err) } if !reflect.DeepEqual(requestSource, requestDecoded) { t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded) } responseSource := &lobbymodel.PublicGamesListResponse{ Items: []lobbymodel.GameSummary{ { GameID: "game-public-aabb", GameName: "Open Lobby", GameType: "public", Status: "enrollment_open", OwnerUserID: "", MinPlayers: 4, MaxPlayers: 12, EnrollmentEndsAt: ends, CreatedAt: created, UpdatedAt: updated, }, }, Page: 3, PageSize: 25, Total: 51, } responseBytes, err := PublicGamesListResponseToPayload(responseSource) if err != nil { t.Fatalf("encode response: %v", err) } responseDecoded, err := PayloadToPublicGamesListResponse(responseBytes) if err != nil { t.Fatalf("decode response: %v", err) } if !reflect.DeepEqual(responseSource, responseDecoded) { t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded) } } func TestLobbyMyApplicationsListRoundTrip(t *testing.T) { t.Parallel() created, _, _ := fixedTimes() decided := created.Add(2 * time.Hour) requestBytes, err := MyApplicationsListRequestToPayload(&lobbymodel.MyApplicationsListRequest{}) if err != nil { t.Fatalf("encode request: %v", err) } if _, err := PayloadToMyApplicationsListRequest(requestBytes); err != nil { t.Fatalf("decode request: %v", err) } source := &lobbymodel.MyApplicationsListResponse{ Items: []lobbymodel.ApplicationSummary{ { ApplicationID: "app-1", GameID: "game-public-aabb", ApplicantUserID: "user-9912", RaceName: "Vegan Federation", Status: "pending", CreatedAt: created, DecidedAt: nil, }, { ApplicationID: "app-2", GameID: "game-public-ccdd", ApplicantUserID: "user-9912", RaceName: "Lithic Compact", Status: "approved", CreatedAt: created, DecidedAt: &decided, }, }, } payload, err := MyApplicationsListResponseToPayload(source) if err != nil { t.Fatalf("encode: %v", err) } decoded, err := PayloadToMyApplicationsListResponse(payload) if err != nil { t.Fatalf("decode: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded) } } func TestLobbyMyInvitesListRoundTrip(t *testing.T) { t.Parallel() created, _, _ := fixedTimes() expires := created.Add(72 * time.Hour) decided := created.Add(1 * time.Hour) requestBytes, err := MyInvitesListRequestToPayload(&lobbymodel.MyInvitesListRequest{}) if err != nil { t.Fatalf("encode request: %v", err) } if _, err := PayloadToMyInvitesListRequest(requestBytes); err != nil { t.Fatalf("decode request: %v", err) } source := &lobbymodel.MyInvitesListResponse{ Items: []lobbymodel.InviteSummary{ { InviteID: "invite-user-bound", GameID: "game-private-7c8f", InviterUserID: "user-1111", InvitedUserID: "user-9912", Code: "", RaceName: "Vegan Federation", Status: "pending", CreatedAt: created, ExpiresAt: expires, DecidedAt: nil, }, { InviteID: "invite-code-based", GameID: "game-private-3322", InviterUserID: "user-1111", InvitedUserID: "", Code: "ABCDEF12", RaceName: "Lithic Compact", Status: "accepted", CreatedAt: created, ExpiresAt: expires, DecidedAt: &decided, }, }, } payload, err := MyInvitesListResponseToPayload(source) if err != nil { t.Fatalf("encode: %v", err) } decoded, err := PayloadToMyInvitesListResponse(payload) if err != nil { t.Fatalf("decode: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded) } } func TestLobbyOpenEnrollmentRoundTrip(t *testing.T) { t.Parallel() requestSource := &lobbymodel.OpenEnrollmentRequest{GameID: "game-private-7c8f"} requestBytes, err := OpenEnrollmentRequestToPayload(requestSource) if err != nil { t.Fatalf("encode request: %v", err) } requestDecoded, err := PayloadToOpenEnrollmentRequest(requestBytes) if err != nil { t.Fatalf("decode request: %v", err) } if !reflect.DeepEqual(requestSource, requestDecoded) { t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded) } responseSource := &lobbymodel.OpenEnrollmentResponse{GameID: "game-private-7c8f", Status: "enrollment_open"} responseBytes, err := OpenEnrollmentResponseToPayload(responseSource) if err != nil { t.Fatalf("encode response: %v", err) } responseDecoded, err := PayloadToOpenEnrollmentResponse(responseBytes) if err != nil { t.Fatalf("decode response: %v", err) } if !reflect.DeepEqual(responseSource, responseDecoded) { t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded) } } func TestLobbyGameCreateRoundTrip(t *testing.T) { t.Parallel() _, _, ends := fixedTimes() requestSource := &lobbymodel.GameCreateRequest{ GameName: "First Contact", Description: "First Phase 8 sandbox game", MinPlayers: 2, MaxPlayers: 8, StartGapHours: 24, StartGapPlayers: 2, EnrollmentEndsAt: ends, TurnSchedule: "0 0 * * *", TargetEngineVersion: "v1", } requestBytes, err := GameCreateRequestToPayload(requestSource) if err != nil { t.Fatalf("encode request: %v", err) } requestDecoded, err := PayloadToGameCreateRequest(requestBytes) if err != nil { t.Fatalf("decode request: %v", err) } if !reflect.DeepEqual(requestSource, requestDecoded) { t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded) } created, updated, _ := fixedTimes() responseSource := &lobbymodel.GameCreateResponse{ Game: lobbymodel.GameSummary{ GameID: "game-private-newly-created", GameName: "First Contact", GameType: "private", Status: "draft", OwnerUserID: "user-9912", MinPlayers: 2, MaxPlayers: 8, EnrollmentEndsAt: ends, CreatedAt: created, UpdatedAt: updated, }, } responseBytes, err := GameCreateResponseToPayload(responseSource) if err != nil { t.Fatalf("encode response: %v", err) } responseDecoded, err := PayloadToGameCreateResponse(responseBytes) if err != nil { t.Fatalf("decode response: %v", err) } if !reflect.DeepEqual(responseSource, responseDecoded) { t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded) } } func TestLobbyApplicationSubmitRoundTrip(t *testing.T) { t.Parallel() requestSource := &lobbymodel.ApplicationSubmitRequest{GameID: "game-public-aabb", RaceName: "Vegan Federation"} requestBytes, err := ApplicationSubmitRequestToPayload(requestSource) if err != nil { t.Fatalf("encode request: %v", err) } requestDecoded, err := PayloadToApplicationSubmitRequest(requestBytes) if err != nil { t.Fatalf("decode request: %v", err) } if !reflect.DeepEqual(requestSource, requestDecoded) { t.Fatalf("request round-trip mismatch\nsource: %#v\ndecoded:%#v", requestSource, requestDecoded) } created, _, _ := fixedTimes() responseSource := &lobbymodel.ApplicationSubmitResponse{ Application: lobbymodel.ApplicationSummary{ ApplicationID: "app-3", GameID: "game-public-aabb", ApplicantUserID: "user-9912", RaceName: "Vegan Federation", Status: "pending", CreatedAt: created, DecidedAt: nil, }, } responseBytes, err := ApplicationSubmitResponseToPayload(responseSource) if err != nil { t.Fatalf("encode response: %v", err) } responseDecoded, err := PayloadToApplicationSubmitResponse(responseBytes) if err != nil { t.Fatalf("decode response: %v", err) } if !reflect.DeepEqual(responseSource, responseDecoded) { t.Fatalf("response round-trip mismatch\nsource: %#v\ndecoded:%#v", responseSource, responseDecoded) } } func TestLobbyInviteRedeemAndDeclineRoundTrip(t *testing.T) { t.Parallel() created, _, _ := fixedTimes() expires := created.Add(72 * time.Hour) decided := created.Add(1 * time.Hour) redeemReq := &lobbymodel.InviteRedeemRequest{GameID: "game-private-7c8f", InviteID: "invite-user-bound"} redeemBytes, err := InviteRedeemRequestToPayload(redeemReq) if err != nil { t.Fatalf("encode redeem request: %v", err) } redeemDecoded, err := PayloadToInviteRedeemRequest(redeemBytes) if err != nil { t.Fatalf("decode redeem request: %v", err) } if !reflect.DeepEqual(redeemReq, redeemDecoded) { t.Fatalf("redeem request mismatch") } redeemResp := &lobbymodel.InviteRedeemResponse{ Invite: lobbymodel.InviteSummary{ InviteID: "invite-user-bound", GameID: "game-private-7c8f", InviterUserID: "user-1111", InvitedUserID: "user-9912", Code: "", RaceName: "Vegan Federation", Status: "accepted", CreatedAt: created, ExpiresAt: expires, DecidedAt: &decided, }, } redeemRespBytes, err := InviteRedeemResponseToPayload(redeemResp) if err != nil { t.Fatalf("encode redeem response: %v", err) } redeemRespDecoded, err := PayloadToInviteRedeemResponse(redeemRespBytes) if err != nil { t.Fatalf("decode redeem response: %v", err) } if !reflect.DeepEqual(redeemResp, redeemRespDecoded) { t.Fatalf("redeem response mismatch") } declineReq := &lobbymodel.InviteDeclineRequest{GameID: "game-private-7c8f", InviteID: "invite-user-bound"} declineBytes, err := InviteDeclineRequestToPayload(declineReq) if err != nil { t.Fatalf("encode decline request: %v", err) } declineDecoded, err := PayloadToInviteDeclineRequest(declineBytes) if err != nil { t.Fatalf("decode decline request: %v", err) } if !reflect.DeepEqual(declineReq, declineDecoded) { t.Fatalf("decline request mismatch") } declineResp := &lobbymodel.InviteDeclineResponse{ Invite: lobbymodel.InviteSummary{ InviteID: "invite-user-bound", GameID: "game-private-7c8f", InviterUserID: "user-1111", InvitedUserID: "user-9912", Code: "", RaceName: "Vegan Federation", Status: "declined", CreatedAt: created, ExpiresAt: expires, DecidedAt: &decided, }, } declineRespBytes, err := InviteDeclineResponseToPayload(declineResp) if err != nil { t.Fatalf("encode decline response: %v", err) } declineRespDecoded, err := PayloadToInviteDeclineResponse(declineRespBytes) if err != nil { t.Fatalf("decode decline response: %v", err) } if !reflect.DeepEqual(declineResp, declineRespDecoded) { t.Fatalf("decline response mismatch") } } func TestLobbyErrorResponseRoundTrip(t *testing.T) { t.Parallel() source := &lobbymodel.ErrorResponse{ Error: lobbymodel.ErrorBody{ Code: "conflict", Message: "request conflicts with current state", }, } payload, err := LobbyErrorResponseToPayload(source) if err != nil { t.Fatalf("encode: %v", err) } decoded, err := PayloadToLobbyErrorResponse(payload) if err != nil { t.Fatalf("decode: %v", err) } if !reflect.DeepEqual(source, decoded) { t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded) } } func TestLobbyDecodersRecoverFromCorruption(t *testing.T) { t.Parallel() garbage := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} if _, err := PayloadToMyGamesListResponse(garbage); err == nil { t.Fatal("expected error decoding corrupt my games payload") } if _, err := PayloadToPublicGamesListResponse(garbage); err == nil { t.Fatal("expected error decoding corrupt public games payload") } if _, err := PayloadToApplicationSubmitResponse(garbage); err == nil { t.Fatal("expected error decoding corrupt application submit response") } if _, err := PayloadToInviteRedeemResponse(garbage); err == nil { t.Fatal("expected error decoding corrupt invite redeem response") } if _, err := PayloadToLobbyErrorResponse(garbage); err == nil { t.Fatal("expected error decoding corrupt error payload") } } func TestLobbyDecodersRejectEmptyPayload(t *testing.T) { t.Parallel() if _, err := PayloadToMyGamesListRequest(nil); err == nil { t.Fatal("expected error decoding nil request") } if _, err := PayloadToPublicGamesListResponse(nil); err == nil { t.Fatal("expected error decoding nil response") } if _, err := PayloadToInviteDeclineRequest(nil); err == nil { t.Fatal("expected error decoding nil decline request") } }