6d66545062
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 50s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
The seat display-name snapshot only reached the live-event path (seatNames);
the REST DTO layer still resolved seat names from the account store
(fillSeatNames), so the game screen and lobby list showed the robot's seeded
account name ("Женя") while the your_turn toast showed its per-game name
("Звёздный_Барс2"). Carry the snapshot into gameDTOFromGame and have
fillSeatNames fall back to the account only for a seat with no snapshot (a
pre-snapshot legacy row). Friends and invitations keep account names (the
persistent identity, not a per-game disguise).
147 lines
5.5 KiB
Go
147 lines
5.5 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/account"
|
|
"scrabble/backend/internal/engine"
|
|
"scrabble/backend/internal/game"
|
|
"scrabble/backend/internal/session"
|
|
"scrabble/backend/internal/social"
|
|
)
|
|
|
|
func TestStatusForError(t *testing.T) {
|
|
cases := map[string]struct {
|
|
err error
|
|
wantStatus int
|
|
wantCode string
|
|
}{
|
|
"not a player": {game.ErrNotAPlayer, http.StatusForbidden, "not_a_player"},
|
|
"not your turn": {game.ErrNotYourTurn, http.StatusConflict, "not_your_turn"},
|
|
"nudge own turn": {social.ErrNudgeOnOwnTurn, http.StatusConflict, "nudge_own_turn"},
|
|
"nudge too soon": {social.ErrNudgeTooSoon, http.StatusConflict, "nudge_too_soon"},
|
|
"chat off turn": {social.ErrChatNotYourTurn, http.StatusConflict, "chat_not_your_turn"},
|
|
"chat one/turn": {social.ErrChatAlreadySentThisTurn, http.StatusConflict, "chat_already_sent"},
|
|
"illegal play": {engine.ErrIllegalPlay, http.StatusUnprocessableEntity, "illegal_play"},
|
|
"email taken": {account.ErrEmailTaken, http.StatusConflict, "email_taken"},
|
|
"code mismatch": {account.ErrCodeMismatch, http.StatusUnauthorized, "code_invalid"},
|
|
"session gone": {session.ErrNotFound, http.StatusUnauthorized, "session_invalid"},
|
|
"chat forbidden": {social.ErrForbiddenContent, http.StatusUnprocessableEntity, "chat_rejected"},
|
|
"no hint move": {game.ErrNoHintAvailable, http.StatusConflict, "no_hint_available"},
|
|
"no hints left": {game.ErrNoHintsLeft, http.StatusConflict, "hint_unavailable"},
|
|
"unknown -> 500": {context_deadline, http.StatusInternalServerError, "internal"},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
status, code := statusForError(tc.err)
|
|
if status != tc.wantStatus || code != tc.wantCode {
|
|
t.Fatalf("statusForError(%v) = (%d, %q), want (%d, %q)", tc.err, status, code, tc.wantStatus, tc.wantCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// context_deadline is an arbitrary unmapped error standing in for "anything
|
|
// unrecognised", which must fall through to 500/internal.
|
|
var context_deadline = errNew("boom")
|
|
|
|
type simpleErr string
|
|
|
|
func (e simpleErr) Error() string { return string(e) }
|
|
func errNew(s string) error { return simpleErr(s) }
|
|
|
|
func TestGameDTOFromGame(t *testing.T) {
|
|
gid, aid := uuid.New(), uuid.New()
|
|
g := game.Game{
|
|
ID: gid,
|
|
Variant: engine.VariantEnglish,
|
|
DictVersion: "v1",
|
|
Status: game.StatusActive,
|
|
Players: 2,
|
|
ToMove: 1,
|
|
TurnTimeout: 24 * time.Hour,
|
|
MoveCount: 3,
|
|
Seats: []game.Seat{{Seat: 0, AccountID: aid, Score: 12}},
|
|
}
|
|
dto := gameDTOFromGame(g)
|
|
if dto.ID != gid.String() || dto.Variant != "scrabble_en" || dto.ToMove != 1 || dto.TurnTimeoutSecs != 86400 {
|
|
t.Fatalf("game dto mismatch: %+v", dto)
|
|
}
|
|
if len(dto.Seats) != 1 || dto.Seats[0].AccountID != aid.String() || dto.Seats[0].Score != 12 {
|
|
t.Fatalf("seat dto mismatch: %+v", dto.Seats)
|
|
}
|
|
}
|
|
|
|
func TestProfileResponseForAwayWindow(t *testing.T) {
|
|
acc := account.Account{
|
|
ID: uuid.New(),
|
|
DisplayName: "Kaya",
|
|
PreferredLanguage: "ru",
|
|
TimeZone: "Europe/Moscow",
|
|
AwayStart: time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
AwayEnd: time.Date(0, 1, 1, 7, 30, 0, 0, time.UTC),
|
|
}
|
|
dto := profileResponseFor(acc)
|
|
if dto.AwayStart != "00:00" || dto.AwayEnd != "07:30" {
|
|
t.Fatalf("away window = (%q, %q), want (00:00, 07:30)", dto.AwayStart, dto.AwayEnd)
|
|
}
|
|
if dto.PreferredLanguage != "ru" || dto.TimeZone != "Europe/Moscow" {
|
|
t.Fatalf("profile dto mismatch: %+v", dto)
|
|
}
|
|
}
|
|
|
|
func TestMoveRecordDTOFrom(t *testing.T) {
|
|
rec := engine.MoveRecord{
|
|
Player: 1,
|
|
Action: engine.ActionPlay,
|
|
Dir: engine.Vertical,
|
|
MainRow: 7,
|
|
MainCol: 7,
|
|
Tiles: []engine.TileRecord{{Row: 7, Col: 7, Letter: "A", Blank: false}},
|
|
Words: []string{"AB"},
|
|
Score: 10,
|
|
Total: 10,
|
|
}
|
|
dto := moveRecordDTOFrom(rec)
|
|
if dto.Action != "play" || dto.Dir != "V" || dto.Score != 10 || len(dto.Tiles) != 1 || dto.Tiles[0].Letter != "A" {
|
|
t.Fatalf("move dto mismatch: %+v", dto)
|
|
}
|
|
}
|
|
|
|
// TestGameDTOCarriesSeatSnapshot checks the DTO carries each seat's display-name
|
|
// snapshot (so a disguised robot's per-game name reaches the REST views, not just live
|
|
// events) rather than leaving it for an account lookup.
|
|
func TestGameDTOCarriesSeatSnapshot(t *testing.T) {
|
|
g := game.Game{
|
|
Variant: engine.VariantEnglish, Players: 2, TurnTimeout: time.Hour,
|
|
Seats: []game.Seat{
|
|
{Seat: 0, AccountID: uuid.New(), DisplayName: "Звёздный_Барс2"},
|
|
{Seat: 1, AccountID: uuid.New(), DisplayName: "Тест"},
|
|
},
|
|
}
|
|
dto := gameDTOFromGame(g)
|
|
if dto.Seats[0].DisplayName != "Звёздный_Барс2" || dto.Seats[1].DisplayName != "Тест" {
|
|
t.Fatalf("seat snapshot not carried into DTO: %+v", dto.Seats)
|
|
}
|
|
}
|
|
|
|
// TestFillSeatNamesKeepsSnapshot checks fillSeatNames leaves a seat whose snapshot is
|
|
// already set untouched (no account lookup), so the per-game name is not overwritten by
|
|
// the robot's seeded account name.
|
|
func TestFillSeatNamesKeepsSnapshot(t *testing.T) {
|
|
s := &Server{} // accounts is unused: a present snapshot must skip the lookup
|
|
dto := &gameDTO{Seats: []seatDTO{
|
|
{Seat: 0, AccountID: uuid.New().String(), DisplayName: "Звёздный_Барс2"},
|
|
{Seat: 1, AccountID: uuid.New().String(), DisplayName: "Тест"},
|
|
}}
|
|
s.fillSeatNames(context.Background(), dto, map[string]string{})
|
|
if dto.Seats[0].DisplayName != "Звёздный_Барс2" || dto.Seats[1].DisplayName != "Тест" {
|
|
t.Fatalf("fillSeatNames overwrote a seat snapshot: %+v", dto.Seats)
|
|
}
|
|
}
|