package server import ( "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 TestParseDirection(t *testing.T) { cases := map[string]struct { in string want engine.Direction ok bool }{ "horizontal": {"H", engine.Horizontal, true}, "vertical": {"V", engine.Vertical, true}, "lowercase": {"h", engine.Horizontal, true}, "trimmed": {" V ", engine.Vertical, true}, "invalid": {"X", 0, false}, "empty": {"", 0, false}, "diagonal-is-not": {"D", 0, false}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { got, ok := parseDirection(tc.in) if ok != tc.ok || (ok && got != tc.want) { t.Fatalf("parseDirection(%q) = (%v, %v), want (%v, %v)", tc.in, got, ok, tc.want, tc.ok) } }) } } 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"}, "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"}, "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 != "english" || 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 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) } }