180 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|