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"}, "nudge own turn": {social.ErrNudgeOnOwnTurn, http.StatusConflict, "nudge_own_turn"}, "nudge too soon": {social.ErrNudgeTooSoon, http.StatusConflict, "nudge_too_soon"}, "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 != "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 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) } }