4cb03736de
Adds the gateway-side translation layer that maps the eight new
ConnectRPC mail commands onto backend's
`/api/v1/user/games/{game_id}/mail/*` REST endpoints.
- `gateway/internal/backendclient/mail_commands.go` defines
`ExecuteMailCommand` and one helper per command (inbox, sent,
message.get, send, broadcast, admin, read, delete). Each helper
decodes the FlatBuffers request envelope, issues the REST call
via the existing `*RESTClient.do`, decodes the JSON body, and
re-encodes a typed FlatBuffers response. Recipient identifiers
travel through unchanged so the new `recipient_race_name`
shortcut introduced in Step 1 reaches backend untouched.
- `routes.go` exposes a `MailRoutes` constructor and a matching
`mailCommandClient` implementing `downstream.Client`.
- `cmd/gateway/main.go` registers the new routes alongside the
existing user / lobby / game-engine routes.
- `mail_commands_test.go` covers the inbox, send-by-race-name, and
read-state paths end-to-end against an `httptest.Server`,
asserting request shapes (path, body, X-User-ID) and the
decoded FlatBuffers response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
210 lines
7.5 KiB
Go
210 lines
7.5 KiB
Go
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
|
|
}
|