package backendclient_test import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "galaxy/gateway/internal/backendclient" diplomailmodel "galaxy/model/diplomail" commonfbs "galaxy/schema/fbs/common" fbs "galaxy/schema/fbs/diplomail" flatbuffers "github.com/google/flatbuffers/go" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestExecuteMailInboxDecodesItems(t *testing.T) { t.Parallel() gameID := uuid.MustParse("11111111-2222-3333-4444-555555555555") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodGet, r.Method) require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/inbox", r.URL.Path) require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID)) writeJSON(t, w, http.StatusOK, map[string]any{ "items": []map[string]any{ { "message_id": "00000000-0000-0000-0000-000000000001", "game_id": gameID.String(), "kind": "personal", "sender_kind": "player", "sender_user_id": "00000000-0000-0000-0000-000000000010", "sender_username": "alice", "sender_race_name": "AliceRace", "subject": "hi", "body": "hello there", "body_lang": "en", "broadcast_scope": "single", "created_at": "2026-05-15T12:00:00Z", "recipient_user_id": "00000000-0000-0000-0000-000000000020", "recipient_user_name": "bob", "recipient_race_name": "BobRace", }, }, }) })) t.Cleanup(server.Close) client := newRESTClient(t, server) payload := buildInboxRequest(gameID) cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailInbox, payload) result, err := client.ExecuteMailCommand(context.Background(), cmd) require.NoError(t, err) assert.Equal(t, "ok", result.ResultCode) resp := fbs.GetRootAsInboxResponse(result.PayloadBytes, 0) require.Equal(t, 1, resp.ItemsLength()) var item fbs.MailMessage require.True(t, resp.Items(&item, 0)) assert.Equal(t, "00000000-0000-0000-0000-000000000001", string(item.MessageId())) assert.Equal(t, "AliceRace", string(item.SenderRaceName())) assert.Equal(t, "BobRace", string(item.RecipientRaceName())) } func TestExecuteMailSendForwardsRaceName(t *testing.T) { t.Parallel() gameID := uuid.MustParse("22222222-3333-4444-5555-666666666666") var captured struct { Body string RecipientUserID string RecipientRaceName string } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages", r.URL.Path) var req map[string]any raw, err := io.ReadAll(r.Body) require.NoError(t, err) require.NoError(t, json.Unmarshal(raw, &req)) if v, ok := req["body"].(string); ok { captured.Body = v } if v, ok := req["recipient_user_id"].(string); ok { captured.RecipientUserID = v } if v, ok := req["recipient_race_name"].(string); ok { captured.RecipientRaceName = v } writeJSON(t, w, http.StatusCreated, map[string]any{ "message_id": "00000000-0000-0000-0000-000000000099", "game_id": gameID.String(), "kind": "personal", "sender_kind": "player", "sender_user_id": "00000000-0000-0000-0000-000000000010", "sender_race_name": "Senders", "body": captured.Body, "body_lang": "en", "broadcast_scope": "single", "created_at": "2026-05-15T12:00:00Z", "recipient_user_id": "00000000-0000-0000-0000-000000000020", "recipient_race_name": "Receivers", }) })) t.Cleanup(server.Close) client := newRESTClient(t, server) payload := buildSendRequestByRaceName(gameID, "Receivers", "let us talk") cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailSend, payload) result, err := client.ExecuteMailCommand(context.Background(), cmd) require.NoError(t, err) assert.Equal(t, "ok", result.ResultCode) resp := fbs.GetRootAsSendResponse(result.PayloadBytes, 0) require.NotNil(t, resp.Message(nil)) msg := resp.Message(nil) assert.Equal(t, "let us talk", string(msg.Body())) assert.Equal(t, "Senders", string(msg.SenderRaceName())) assert.Equal(t, "Receivers", string(msg.RecipientRaceName())) assert.Empty(t, captured.RecipientUserID) assert.Equal(t, "Receivers", captured.RecipientRaceName) assert.Equal(t, "let us talk", captured.Body) } func TestExecuteMailReadReturnsState(t *testing.T) { t.Parallel() gameID := uuid.MustParse("33333333-4444-5555-6666-777777777777") messageID := uuid.MustParse("00000000-0000-0000-0000-0000000000aa") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages/"+messageID.String()+"/read", r.URL.Path) writeJSON(t, w, http.StatusOK, map[string]any{ "message_id": messageID.String(), "read_at": "2026-05-15T12:34:56Z", }) })) t.Cleanup(server.Close) client := newRESTClient(t, server) payload := buildReadRequest(gameID, messageID) cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailRead, payload) result, err := client.ExecuteMailCommand(context.Background(), cmd) require.NoError(t, err) resp := fbs.GetRootAsReadResponse(result.PayloadBytes, 0) state := resp.State(nil) require.NotNil(t, state) assert.Equal(t, messageID.String(), string(state.MessageId())) assert.NotZero(t, state.ReadAtMs()) } // buildInboxRequest emits a FlatBuffers InboxRequest envelope with // the supplied game_id. func buildInboxRequest(gameID uuid.UUID) []byte { builder := flatbuffers.NewBuilder(64) hi, lo := uuidToHiLo(gameID) fbs.InboxRequestStart(builder) fbs.InboxRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo)) root := fbs.InboxRequestEnd(builder) fbs.FinishInboxRequestBuffer(builder, root) return builder.FinishedBytes() } // buildSendRequestByRaceName emits a FlatBuffers SendRequest that // addresses the recipient by race name rather than user_id. func buildSendRequestByRaceName(gameID uuid.UUID, raceName, body string) []byte { builder := flatbuffers.NewBuilder(128) raceOff := builder.CreateString(raceName) bodyOff := builder.CreateString(body) hi, lo := uuidToHiLo(gameID) fbs.SendRequestStart(builder) fbs.SendRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo)) fbs.SendRequestAddRecipientRaceName(builder, raceOff) fbs.SendRequestAddBody(builder, bodyOff) root := fbs.SendRequestEnd(builder) fbs.FinishSendRequestBuffer(builder, root) return builder.FinishedBytes() } // buildReadRequest emits a FlatBuffers ReadRequest envelope. func buildReadRequest(gameID, messageID uuid.UUID) []byte { builder := flatbuffers.NewBuilder(64) gameHi, gameLo := uuidToHiLo(gameID) msgHi, msgLo := uuidToHiLo(messageID) fbs.ReadRequestStart(builder) fbs.ReadRequestAddGameId(builder, commonfbs.CreateUUID(builder, gameHi, gameLo)) fbs.ReadRequestAddMessageId(builder, commonfbs.CreateUUID(builder, msgHi, msgLo)) root := fbs.ReadRequestEnd(builder) fbs.FinishReadRequestBuffer(builder, root) return builder.FinishedBytes() } // uuidToHiLo splits a 16-byte UUID into the two big-endian uint64 // halves the common.UUID struct uses. func uuidToHiLo(u uuid.UUID) (uint64, uint64) { var hi, lo uint64 for i := 0; i < 8; i++ { hi = (hi << 8) | uint64(u[i]) lo = (lo << 8) | uint64(u[i+8]) } return hi, lo }