package transcode_test import ( "context" "encoding/json" "io" "net/http" "testing" flatbuffers "github.com/google/flatbuffers/go" "scrabble/gateway/internal/transcode" fb "scrabble/pkg/fbs/scrabblefb" ) // TestGameStateIncludesAlphabet checks the include_alphabet flag reaches the backend and // the returned alphabet table plus the index rack (a blank is 255) are encoded into the // StateView (Stage 13). func TestGameStateIncludesAlphabet(t *testing.T) { backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) { if got := r.URL.Query().Get("include_alphabet"); got != "true" { t.Errorf("include_alphabet query = %q, want true", got) } _, _ = w.Write([]byte(`{"game":{"id":"g-1","variant":"scrabble_en","status":"active","players":2,"to_move":0,"seats":[]},"seat":0,"rack":[0,255],"bag_len":50,"hints_remaining":0,"alphabet":[{"index":0,"letter":"a","value":1},{"index":1,"letter":"b","value":3}]}`)) }) defer cleanup() reg := transcode.NewRegistry(backend, nil) op, _ := reg.Lookup(transcode.MsgGameState) b := flatbuffers.NewBuilder(32) gid := b.CreateString("g-1") fb.StateRequestStart(b) fb.StateRequestAddGameId(b, gid) fb.StateRequestAddIncludeAlphabet(b, true) b.Finish(fb.StateRequestEnd(b)) payload, err := op.Handler(context.Background(), transcode.Request{Payload: b.FinishedBytes(), UserID: "u-1"}) if err != nil { t.Fatalf("handler: %v", err) } st := fb.GetRootAsStateView(payload, 0) if st.RackLength() != 2 || st.Rack(0) != 0 || st.Rack(1) != 255 { t.Fatalf("rack indices wrong: len=%d [0]=%d [1]=%d", st.RackLength(), st.Rack(0), st.Rack(1)) } if st.AlphabetLength() != 2 { t.Fatalf("alphabet length = %d, want 2", st.AlphabetLength()) } var e fb.AlphabetEntry st.Alphabet(&e, 0) if e.Index() != 0 || string(e.Letter()) != "a" || e.Value() != 1 { t.Errorf("alphabet[0] = %d/%q/%d, want 0/a/1", e.Index(), e.Letter(), e.Value()) } } // TestGameStateOmitsAlphabetByDefault checks the table is neither requested nor encoded on // the steady-state poll (no include_alphabet flag). func TestGameStateOmitsAlphabetByDefault(t *testing.T) { backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("include_alphabet") == "true" { t.Error("include_alphabet should be unset") } _, _ = w.Write([]byte(`{"game":{"id":"g-1","variant":"scrabble_en","status":"active","players":2,"to_move":0,"seats":[]},"seat":0,"rack":[2,0,19],"bag_len":50,"hints_remaining":0}`)) }) defer cleanup() reg := transcode.NewRegistry(backend, nil) op, _ := reg.Lookup(transcode.MsgGameState) b := flatbuffers.NewBuilder(32) gid := b.CreateString("g-1") fb.StateRequestStart(b) fb.StateRequestAddGameId(b, gid) b.Finish(fb.StateRequestEnd(b)) payload, err := op.Handler(context.Background(), transcode.Request{Payload: b.FinishedBytes(), UserID: "u-1"}) if err != nil { t.Fatalf("handler: %v", err) } st := fb.GetRootAsStateView(payload, 0) if st.AlphabetLength() != 0 { t.Errorf("alphabet length = %d, want 0", st.AlphabetLength()) } if st.RackLength() != 3 { t.Errorf("rack length = %d, want 3", st.RackLength()) } } // TestSubmitPlayForwardsIndexTiles checks PlayTile indices reach the backend as integer // letter fields in the JSON body, blank flag preserved (Stage 13). func TestSubmitPlayForwardsIndexTiles(t *testing.T) { var body struct { Dir string `json:"dir"` Tiles []struct { Row int `json:"row"` Col int `json:"col"` Letter int `json:"letter"` Blank bool `json:"blank"` } `json:"tiles"` } backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) if err := json.Unmarshal(raw, &body); err != nil { t.Fatalf("decode body: %v", err) } _, _ = w.Write([]byte(`{"move":{"player":0,"action":"play","words":["CAT"],"score":9},"game":{"id":"g-5","status":"active","seats":[]}}`)) }) defer cleanup() reg := transcode.NewRegistry(backend, nil) op, _ := reg.Lookup(transcode.MsgGameSubmitPlay) b := flatbuffers.NewBuilder(64) gid := b.CreateString("g-5") dir := b.CreateString("H") fb.PlayTileStart(b) fb.PlayTileAddRow(b, 7) fb.PlayTileAddCol(b, 7) fb.PlayTileAddLetter(b, 2) fb.PlayTileAddBlank(b, true) tile := fb.PlayTileEnd(b) fb.SubmitPlayRequestStartTilesVector(b, 1) b.PrependUOffsetT(tile) tiles := b.EndVector(1) fb.SubmitPlayRequestStart(b) fb.SubmitPlayRequestAddGameId(b, gid) fb.SubmitPlayRequestAddDir(b, dir) fb.SubmitPlayRequestAddTiles(b, tiles) b.Finish(fb.SubmitPlayRequestEnd(b)) if _, err := op.Handler(context.Background(), transcode.Request{Payload: b.FinishedBytes(), UserID: "u-1"}); err != nil { t.Fatalf("handler: %v", err) } if len(body.Tiles) != 1 || body.Tiles[0].Letter != 2 || !body.Tiles[0].Blank || body.Tiles[0].Row != 7 { t.Fatalf("forwarded tiles wrong: %+v", body.Tiles) } } // TestCheckWordForwardsIndices checks the word-check query rides as repeated ?idx= params // and the decoded concrete word echoes back (Stage 13). func TestCheckWordForwardsIndices(t *testing.T) { backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) { if got := r.URL.Query()["idx"]; len(got) != 3 || got[0] != "2" || got[2] != "19" { t.Errorf("idx params = %v, want [2 0 19]", got) } _, _ = w.Write([]byte(`{"word":"cat","legal":true}`)) }) defer cleanup() reg := transcode.NewRegistry(backend, nil) op, _ := reg.Lookup(transcode.MsgGameCheckWord) b := flatbuffers.NewBuilder(32) gid := b.CreateString("g-1") word := b.CreateByteVector([]byte{2, 0, 19}) fb.CheckWordRequestStart(b) fb.CheckWordRequestAddGameId(b, gid) fb.CheckWordRequestAddWord(b, word) b.Finish(fb.CheckWordRequestEnd(b)) payload, err := op.Handler(context.Background(), transcode.Request{Payload: b.FinishedBytes(), UserID: "u-1"}) if err != nil { t.Fatalf("handler: %v", err) } res := fb.GetRootAsWordCheckResult(payload, 0) if string(res.Word()) != "cat" || !res.Legal() { t.Errorf("word check = %q/%v, want cat/true", res.Word(), res.Legal()) } } // TestExchangeForwardsIndices checks rack-exchange indices (blank 255) reach the backend // body (Stage 13). func TestExchangeForwardsIndices(t *testing.T) { var body struct { Tiles []int `json:"tiles"` } backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) _ = json.Unmarshal(raw, &body) _, _ = w.Write([]byte(`{"move":{"player":0,"action":"exchange","count":2},"game":{"id":"g-1","status":"active","seats":[]}}`)) }) defer cleanup() reg := transcode.NewRegistry(backend, nil) op, _ := reg.Lookup(transcode.MsgGameExchange) b := flatbuffers.NewBuilder(32) gid := b.CreateString("g-1") tiles := b.CreateByteVector([]byte{0, 255}) fb.ExchangeRequestStart(b) fb.ExchangeRequestAddGameId(b, gid) fb.ExchangeRequestAddTiles(b, tiles) b.Finish(fb.ExchangeRequestEnd(b)) if _, err := op.Handler(context.Background(), transcode.Request{Payload: b.FinishedBytes(), UserID: "u-1"}); err != nil { t.Fatalf("handler: %v", err) } if len(body.Tiles) != 2 || body.Tiles[0] != 0 || body.Tiles[1] != 255 { t.Errorf("forwarded exchange tiles = %v, want [0 255]", body.Tiles) } }