Files
chess/internal/server/service/import_test.go
T
lixen 664cf9654e
test / test (push) Canceled after 0s
test / e2e (push) Canceled after 0s
v0.16.0 import PGN, delete games, two-layout web client
2026-10-19 21:02:39 -04:00

180 lines
6.6 KiB
Go

package service
import (
"errors"
"strings"
"testing"
"github.com/lixenwraith/chess/internal/server/core"
"github.com/lixenwraith/chess/internal/server/storage"
)
const importPGN = `[Event "World Championship"]
[Site "Reykjavik"]
[Date "1972.07.11"]
[Round "1"]
[White "Spassky, Boris"]
[Black "Fischer, Robert"]
[Result "1-0"]
1. d4 Nf6 2. c4 e6 3. Nf3 d5 1-0
`
func TestImportGameStoresReplaysAndLists(t *testing.T) {
svc := newPersistentTestService(t)
user, _, err := svc.RegisterUser("alice", "", "Password1")
if err != nil {
t.Fatal(err)
}
imported, err := svc.ImportGame(user.UserID, importPGN)
if err != nil {
t.Fatal(err)
}
if imported.Plies != 6 || imported.Result != "white_wins" {
t.Fatalf("imported = %+v", imported)
}
history, err := svc.GetGameHistory(imported.GameID)
if err != nil {
t.Fatal(err)
}
if history.Origin != "imported" || history.Event != "World Championship" || history.Site != "Reykjavik" ||
history.Round != "1" || history.Players.White.Name != "Spassky, Boris" || history.Players.Black.Name != "Fischer, Robert" ||
history.Termination != "resignation" || history.PGNResult != "1-0" ||
history.StartTimeUTC.Format("2006-01-02") != "1972-07-11" {
t.Fatalf("history = %+v", history)
}
var san []string
for _, m := range history.Moves {
san = append(san, m.SAN)
}
if strings.Join(san, " ") != "d4 Nf6 c4 e6 Nf3 d5" {
t.Errorf("SAN = %v", san)
}
pgn, err := svc.GetGamePGN(imported.GameID, -1)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{`[Event "World Championship"]`, `[Site "Reykjavik"]`, `[Round "1"]`, `[White "Spassky, Boris"]`, "1. d4 Nf6 2. c4 e6 3. Nf3 d5 { White wins by resignation. } 1-0"} {
if !strings.Contains(pgn.Text, want) {
t.Errorf("PGN lacks %q:\n%s", want, pgn.Text)
}
}
page, err := svc.GetUserGames(user.UserID, UserGamesOptions{Limit: 10})
if err != nil {
t.Fatal(err)
}
if len(page.Games) != 1 || page.Games[0].GameID != imported.GameID || page.Games[0].Origin != "imported" ||
page.Games[0].Event != "World Championship" || page.Games[0].MoveCount != 6 {
t.Fatalf("listing = %+v", page.Games)
}
// Imported games have no claimed colour.
page, _ = svc.GetUserGames(user.UserID, UserGamesOptions{Limit: 10, Color: "white"})
if len(page.Games) != 0 {
t.Errorf("imported game listed under a colour filter")
}
// Outcomes from the final position.
for name, tc := range map[string]struct{ pgn, result, termination string }{
"mate": {"1. f3 e5 2. g4 Qh4# 0-1", "black_wins", "checkmate"},
"stalemate": {"[FEN \"7k/8/6K1/5Q2/8/8/8/8 w - - 0 1\"]\n[SetUp \"1\"]\n\n1. Qf7 1/2-1/2", "stalemate", "stalemate"},
"agreed": {"1. e4 e5 1/2-1/2", "draw", "agreement"},
"ongoing": {"1. e4 e5 *", "", ""},
"material": {"[FEN \"4k3/8/8/8/8/8/8/3RK3 w - - 0 1\"]\n[SetUp \"1\"]\n\n1. Rd8+ Kxd8 1/2-1/2", "draw", "insufficient_material"},
} {
got, err := svc.ImportGame(user.UserID, tc.pgn)
if err != nil {
t.Errorf("%s: %v", name, err)
continue
}
h, _ := svc.GetGameHistory(got.GameID)
if h.Result != tc.result || h.Termination != tc.termination {
t.Errorf("%s: result %q %q, want %q %q", name, h.Result, h.Termination, tc.result, tc.termination)
}
}
// Illegal moves and impossible results are refused.
for name, bad := range map[string]string{
"illegal": "1. e4 e5 2. Ke2 Ke7 3. Kd3 Qh4",
"mate but draw": "1. f3 e5 2. g4 Qh4# 1/2-1/2",
"mate but white": "1. f3 e5 2. g4 Qh4# 1-0",
"over but star": "1. f3 e5 2. g4 Qh4# *",
"bad fen": "[FEN \"nonsense\"]\n[SetUp \"1\"]\n\n1. e4 *",
"not pgn": "hello world",
} {
if _, err := svc.ImportGame(user.UserID, bad); !errors.Is(err, ErrInvalidPGN) {
t.Errorf("%s: error = %v, want ErrInvalidPGN", name, err)
}
}
}
func TestRemoveGameDeletesOwnAndReleasesShared(t *testing.T) {
svc := newPersistentTestService(t)
alice, _, err := svc.RegisterUser("alice", "", "Password1")
if err != nil {
t.Fatal(err)
}
bob, _, err := svc.RegisterUser("bob", "", "Password1")
if err != nil {
t.Fatal(err)
}
human := core.PlayerConfig{Type: core.PlayerHuman}
// A game alice alone claims: deleted outright and unloaded.
own := playLine(t, svc, alice.UserID, human, []string{"e2e4"}, core.StateOngoing)
outcome, err := svc.RemoveGame(own, alice.UserID)
if err != nil || outcome != storage.RemovedGame {
t.Fatalf("own game: %v %v", outcome, err)
}
if _, err := svc.GetGameView(own); err == nil {
t.Error("deleted game still in memory")
}
if _, err := svc.GetGameHistory(own); !errors.Is(err, ErrGameNotFound) {
t.Errorf("deleted game history: %v", err)
}
// An imported game: deleted by its importer, unknown to anyone else.
imported, err := svc.ImportGame(alice.UserID, "1. e4 e5 *")
if err != nil {
t.Fatal(err)
}
if _, err := svc.RemoveGame(imported.GameID, bob.UserID); !errors.Is(err, ErrGameNotFound) {
t.Errorf("bob removing alice's import: %v", err)
}
if outcome, err := svc.RemoveGame(imported.GameID, alice.UserID); err != nil || outcome != storage.RemovedGame {
t.Errorf("alice removing her import: %v %v", outcome, err)
}
// A game both claim: alice's removal releases her side; bob keeps it.
shared := playLine(t, svc, alice.UserID, human, []string{"e2e4"}, core.StateOngoing)
if err := svc.ApplyMoveWithState(shared, MoveCommit{
ExpectedFEN: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", ExpectedState: core.StateOngoing,
ExpectedTurn: core.ColorBlack, ActorUserID: bob.UserID, MoveUCI: "e7e5",
NewFEN: "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", State: core.StateOngoing,
}); err != nil {
t.Fatal(err)
}
outcome, err = svc.RemoveGame(shared, alice.UserID)
if err != nil || outcome != storage.RemovedClaim {
t.Fatalf("shared game: %v %v", outcome, err)
}
view, err := svc.GetGameView(shared)
if err != nil || view.WhitePlayer.ClaimedBy != "" || view.BlackPlayer.ClaimedBy != bob.UserID {
t.Errorf("shared game after release: %v %+v", err, view.WhitePlayer)
}
history, _ := svc.GetGameHistory(shared)
if history.Players.White.ClaimedBy != "" || history.Players.White.Name != "" || history.Players.Black.Name != "bob" {
t.Errorf("stored claims after release: %+v %+v", history.Players.White, history.Players.Black)
}
if page, _ := svc.GetUserGames(alice.UserID, UserGamesOptions{Limit: 10}); len(page.Games) != 0 {
t.Errorf("alice still lists %d games", len(page.Games))
}
if page, _ := svc.GetUserGames(bob.UserID, UserGamesOptions{Limit: 10}); len(page.Games) != 1 {
t.Errorf("bob lists %d games", len(page.Games))
}
if _, err := svc.RemoveGame(shared, alice.UserID); !errors.Is(err, ErrGameNotFound) {
t.Errorf("second removal: %v", err)
}
}