589 lines
19 KiB
Go
589 lines
19 KiB
Go
package ws_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"github.com/ruby-sea-battle/game-server/internal/auth"
|
|
"github.com/ruby-sea-battle/game-server/internal/game"
|
|
"github.com/ruby-sea-battle/game-server/internal/protocol"
|
|
"github.com/ruby-sea-battle/game-server/internal/ws"
|
|
)
|
|
|
|
var secret = []byte("integration-secret")
|
|
|
|
const publicAddr = "ws://game.example.com/ws"
|
|
|
|
func gameToken(t *testing.T, matchID string, p1, p2 int64, gameServer string, exp time.Time) string {
|
|
t.Helper()
|
|
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &auth.GameClaims{
|
|
MatchID: matchID,
|
|
Player1ID: p1,
|
|
Player2ID: p2,
|
|
GameServer: gameServer,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(exp),
|
|
},
|
|
}).SignedString(secret)
|
|
if err != nil {
|
|
t.Fatalf("sign game token: %v", err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
func userToken(t *testing.T, userID int64) string {
|
|
t.Helper()
|
|
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &auth.UserClaims{
|
|
UserID: userID,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
},
|
|
}).SignedString(secret)
|
|
if err != nil {
|
|
t.Fatalf("sign user token: %v", err)
|
|
}
|
|
return token
|
|
}
|
|
|
|
func startServerWithManager(t *testing.T, joinTimeout time.Duration) (string, *game.Manager) {
|
|
t.Helper()
|
|
manager := game.NewManager(joinTimeout)
|
|
tw := &testWriter{t: t}
|
|
log := slog.New(slog.NewTextHandler(tw, nil))
|
|
handler := ws.NewHandler(manager, secret, publicAddr, []string{"*"}, log)
|
|
srv := httptest.NewServer(handler)
|
|
t.Cleanup(func() {
|
|
srv.Close()
|
|
tw.mu.Lock()
|
|
tw.done = true
|
|
tw.mu.Unlock()
|
|
})
|
|
return "ws" + strings.TrimPrefix(srv.URL, "http"), manager
|
|
}
|
|
|
|
func startServer(t *testing.T, joinTimeout time.Duration) (url string) {
|
|
u, _ := startServerWithManager(t, joinTimeout)
|
|
return u
|
|
}
|
|
|
|
type testWriter struct {
|
|
mu sync.Mutex
|
|
t *testing.T
|
|
done bool
|
|
}
|
|
|
|
func (w *testWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if !w.done {
|
|
w.t.Log(strings.TrimSpace(string(p)))
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func dial(t *testing.T, url string) *websocket.Conn {
|
|
t.Helper()
|
|
c, _, err := websocket.Dial(context.Background(), url, nil)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
t.Cleanup(func() { c.Close(websocket.StatusNormalClosure, "") })
|
|
return c
|
|
}
|
|
|
|
func join(t *testing.T, c *websocket.Conn, gt, ut string) {
|
|
t.Helper()
|
|
msg, _ := json.Marshal([]any{1, gt, ut})
|
|
if err := c.Write(context.Background(), websocket.MessageText, msg); err != nil {
|
|
t.Fatalf("write JOIN: %v", err)
|
|
}
|
|
}
|
|
|
|
func readMsg(t *testing.T, c *websocket.Conn) []any {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, data, err := c.Read(ctx)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
var v []any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
t.Fatalf("unmarshal %s: %v", data, err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func TestTwoPlayersStartMatch(t *testing.T) {
|
|
url := startServer(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-1", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
|
|
for i, c := range []*websocket.Conn{c1, c2} {
|
|
v := readMsg(t, c)
|
|
if v[0] != float64(3) || v[1] != float64(1) || v[2] != float64(1) {
|
|
t.Fatalf("player %d: unexpected message %v", i, v)
|
|
}
|
|
// ACK the state.
|
|
msg, _ := json.Marshal([]any{4, 1})
|
|
if err := c.Write(context.Background(), websocket.MessageText, msg); err != nil {
|
|
t.Fatalf("write ACK: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWrongServerAddressRejected(t *testing.T) {
|
|
url := startServer(t, time.Minute)
|
|
gt := gameToken(t, "m-2", 1, 2, "ws://other.example.com/ws", time.Now().Add(time.Hour))
|
|
|
|
c := dial(t, url)
|
|
join(t, c, gt, userToken(t, 1))
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, _, err := c.Read(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected connection to be closed")
|
|
}
|
|
if code := websocket.CloseStatus(err); code != 4003 {
|
|
t.Fatalf("close code = %d, want 4003", code)
|
|
}
|
|
}
|
|
|
|
func TestJoinTimeoutEndsMatchAndReplay(t *testing.T) {
|
|
url := startServer(t, 200*time.Millisecond)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-3", 1, 2, publicAddr, exp)
|
|
|
|
c1 := dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
|
|
v := readMsg(t, c1) // END
|
|
if v[0] != float64(2) || v[1] != float64(1) || v[2] != float64(-1) || v[3] != "join timeout exceeded" {
|
|
t.Fatalf("unexpected END: %v", v)
|
|
}
|
|
|
|
// Late join with the same token replays END and closes.
|
|
c2 := dial(t, url)
|
|
join(t, c2, gt, userToken(t, 2))
|
|
v = readMsg(t, c2)
|
|
if v[0] != float64(2) || v[2] != float64(-1) {
|
|
t.Fatalf("unexpected replayed END: %v", v)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if _, _, err := c2.Read(ctx); err == nil {
|
|
t.Fatal("expected connection to be closed after END replay")
|
|
}
|
|
}
|
|
|
|
func TestReconnectDuringGame(t *testing.T) {
|
|
url := startServer(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-4", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
readMsg(t, c1) // STATE, not acknowledged
|
|
|
|
// Simulate a dropped connection.
|
|
c1.Close(websocket.StatusAbnormalClosure, "network gone")
|
|
|
|
// Reconnect with the same tokens: unacknowledged STATE is replayed.
|
|
c1b := dial(t, url)
|
|
join(t, c1b, gt, userToken(t, 1))
|
|
v := readMsg(t, c1b)
|
|
if v[0] != float64(3) || v[1] != float64(1) || v[2] != float64(1) {
|
|
t.Fatalf("unexpected replayed message: %v", v)
|
|
}
|
|
}
|
|
|
|
func TestReconnectWithExpiredTokenAllowedIfSameToken(t *testing.T) {
|
|
url := startServer(t, time.Minute)
|
|
// Short expiration: 1200ms
|
|
exp := time.Now().Add(1200 * time.Millisecond)
|
|
gt := gameToken(t, "m-reconnect-exp", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
readMsg(t, c1) // STATE
|
|
readMsg(t, c2) // STATE
|
|
|
|
// Wait for the game token to expire
|
|
time.Sleep(1300 * time.Millisecond)
|
|
|
|
// Simulate disconnect
|
|
c1.Close(websocket.StatusAbnormalClosure, "network gone")
|
|
|
|
// Reconnect with the SAME token (which is now expired)
|
|
c1b := dial(t, url)
|
|
join(t, c1b, gt, userToken(t, 1))
|
|
v := readMsg(t, c1b)
|
|
if v[0] != float64(3) || v[1] != float64(1) || v[2] != float64(1) {
|
|
t.Fatalf("unexpected replayed message on reconnect: %v", v)
|
|
}
|
|
|
|
// But an expired token for a non-existent match is rejected with 4003
|
|
gtExpired := gameToken(t, "m-other", 1, 2, publicAddr, time.Now().Add(-time.Hour))
|
|
cOther := dial(t, url)
|
|
join(t, cOther, gtExpired, userToken(t, 1))
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, _, err := cOther.Read(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected connection to be rejected")
|
|
}
|
|
if code := websocket.CloseStatus(err); code != 4003 {
|
|
t.Fatalf("close code = %d, want 4003", code)
|
|
}
|
|
}
|
|
|
|
func TestDuplicateJoinIgnored(t *testing.T) {
|
|
url := startServer(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-dup-join", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
readMsg(t, c1) // STATE
|
|
|
|
// Send duplicate JOIN on c1
|
|
join(t, c1, gt, userToken(t, 1))
|
|
|
|
// Send ACK after duplicate JOIN; connection must still be open and functioning
|
|
ackMsg, _ := json.Marshal([]any{4, 1})
|
|
if err := c1.Write(context.Background(), websocket.MessageText, ackMsg); err != nil {
|
|
t.Fatalf("write ACK: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNonPositiveIDsRejected(t *testing.T) {
|
|
url := startServer(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
|
|
// Non-positive Player1ID
|
|
gtBadP1 := gameToken(t, "m-bad-p1", 0, 2, publicAddr, exp)
|
|
c := dial(t, url)
|
|
join(t, c, gtBadP1, userToken(t, 2))
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, _, err := c.Read(ctx)
|
|
if err == nil || websocket.CloseStatus(err) != 4003 {
|
|
t.Fatalf("expected 4003 for player1_id = 0, got err = %v", err)
|
|
}
|
|
|
|
// Non-positive UserID
|
|
gtValid := gameToken(t, "m-valid", 1, 2, publicAddr, exp)
|
|
c2 := dial(t, url)
|
|
join(t, c2, gtValid, userToken(t, -1))
|
|
ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel2()
|
|
_, _, err = c2.Read(ctx2)
|
|
if err == nil || websocket.CloseStatus(err) != 4004 {
|
|
t.Fatalf("expected 4004 for user_id = -1, got err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManagerShutdownClosesConnectionsGoingAway(t *testing.T) {
|
|
url, mgr := startServerWithManager(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-shutdown", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
readMsg(t, c1)
|
|
readMsg(t, c2)
|
|
|
|
errCh := make(chan error, 2)
|
|
for _, c := range []*websocket.Conn{c1, c2} {
|
|
go func(conn *websocket.Conn) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, _, err := conn.Read(ctx)
|
|
errCh <- err
|
|
}(c)
|
|
}
|
|
|
|
mgr.Shutdown()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
err := <-errCh
|
|
if err == nil {
|
|
t.Fatalf("client %d: expected connection to be closed", i)
|
|
}
|
|
if code := websocket.CloseStatus(err); code != 1001 {
|
|
t.Fatalf("client %d: close status = %d, want 1001", i, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func validTestShips() []protocol.Ship {
|
|
return []protocol.Ship{
|
|
// 4 size 1
|
|
{Orientation: protocol.OrientationHorizontal, X: 9, Y: 4, Size: 1},
|
|
{Orientation: protocol.OrientationVertical, X: 7, Y: 4, Size: 1},
|
|
{Orientation: protocol.OrientationHorizontal, X: 5, Y: 4, Size: 1},
|
|
{Orientation: protocol.OrientationHorizontal, X: 3, Y: 4, Size: 1},
|
|
// 3 size 2
|
|
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 4, Size: 2},
|
|
{Orientation: protocol.OrientationHorizontal, X: 7, Y: 2, Size: 2},
|
|
{Orientation: protocol.OrientationHorizontal, X: 4, Y: 2, Size: 2},
|
|
// 2 size 3
|
|
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 2, Size: 3},
|
|
{Orientation: protocol.OrientationHorizontal, X: 5, Y: 0, Size: 3},
|
|
// 1 size 4
|
|
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 0, Size: 4},
|
|
}
|
|
}
|
|
|
|
func TestPlaceShipsOverWebSocket(t *testing.T) {
|
|
url, mgr := startServerWithManager(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-ws-placeships", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
|
|
// Both receive StatePlacingShips (seq 1)
|
|
v1 := readMsg(t, c1)
|
|
if v1[0] != float64(protocol.OpState) || v1[1] != float64(1) || v1[2] != float64(protocol.StatePlacingShips) {
|
|
t.Fatalf("c1 unexpected initial state: %v", v1)
|
|
}
|
|
v2 := readMsg(t, c2)
|
|
if v2[0] != float64(protocol.OpState) || v2[1] != float64(1) || v2[2] != float64(protocol.StatePlacingShips) {
|
|
t.Fatalf("c2 unexpected initial state: %v", v2)
|
|
}
|
|
|
|
// ACK seq 1
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(1))
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(1))
|
|
|
|
// Player 1 sends PlaceShips
|
|
ships := validTestShips()
|
|
placeMsg := protocol.BuildPlaceShips(ships)
|
|
if err := c1.Write(context.Background(), websocket.MessageText, placeMsg); err != nil {
|
|
t.Fatalf("write PlaceShips c1: %v", err)
|
|
}
|
|
|
|
// c1 gets StateWaitOpponentReady (3), c2 gets StateOpponentReady (2)
|
|
v1 = readMsg(t, c1)
|
|
if v1[0] != float64(protocol.OpState) || v1[1] != float64(2) || v1[2] != float64(protocol.StateWaitOpponentReady) {
|
|
t.Fatalf("c1 got %v, want StateWaitOpponentReady (3)", v1)
|
|
}
|
|
v2 = readMsg(t, c2)
|
|
if v2[0] != float64(protocol.OpState) || v2[1] != float64(2) || v2[2] != float64(protocol.StateOpponentReady) {
|
|
t.Fatalf("c2 got %v, want StateOpponentReady (2)", v2)
|
|
}
|
|
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(2))
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(2))
|
|
|
|
// Player 2 sends PlaceShips
|
|
if err := c2.Write(context.Background(), websocket.MessageText, placeMsg); err != nil {
|
|
t.Fatalf("write PlaceShips c2: %v", err)
|
|
}
|
|
|
|
// Game starts: Player 1 was first, so c1 gets StateYourTurn (4), c2 gets StateWaitOpponentTurn (5)
|
|
v1 = readMsg(t, c1)
|
|
if v1[0] != float64(protocol.OpState) || v1[1] != float64(3) || v1[2] != float64(protocol.StateYourTurn) {
|
|
t.Fatalf("c1 got %v, want StateYourTurn (4)", v1)
|
|
}
|
|
v2 = readMsg(t, c2)
|
|
if v2[0] != float64(protocol.OpState) || v2[1] != float64(3) || v2[2] != float64(protocol.StateWaitOpponentTurn) {
|
|
t.Fatalf("c2 got %v, want StateWaitOpponentTurn (5)", v2)
|
|
}
|
|
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(3))
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(3))
|
|
|
|
// Check that the match on the server has the placements stored
|
|
match := mgr.GetMatch("m-ws-placeships")
|
|
if match == nil {
|
|
t.Fatal("match not found on manager")
|
|
}
|
|
if len(match.Ships(0)) != 10 || len(match.Ships(1)) != 10 {
|
|
t.Fatalf("ships not stored on match: p1=%d, p2=%d", len(match.Ships(0)), len(match.Ships(1)))
|
|
}
|
|
if _, ok := match.Board(0); !ok {
|
|
t.Fatal("p1 board not placed")
|
|
}
|
|
if _, ok := match.Board(1); !ok {
|
|
t.Fatal("p2 board not placed")
|
|
}
|
|
}
|
|
|
|
func TestFireOverWebSocket(t *testing.T) {
|
|
url, _ := startServerWithManager(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-ws-fire", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
|
|
// Initial StatePlacingShips (seq 1)
|
|
readMsg(t, c1)
|
|
readMsg(t, c2)
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(1))
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(1))
|
|
|
|
// Both place ships
|
|
ships := validTestShips()
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildPlaceShips(ships))
|
|
readMsg(t, c1) // seq 2: StateWaitOpponentReady
|
|
readMsg(t, c2) // seq 2: StateOpponentReady
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(2))
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(2))
|
|
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildPlaceShips(ships))
|
|
readMsg(t, c1) // seq 3: StateYourTurn (p1 starts)
|
|
readMsg(t, c2) // seq 3: StateWaitOpponentTurn
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(3))
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(3))
|
|
|
|
// 1. Player 1 sends invalid position (-1, 0)
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildFire(-1, 0))
|
|
v1 := readMsg(t, c1)
|
|
// [7, 4, 4]
|
|
if v1[0] != float64(protocol.OpFireResult) || v1[1] != float64(4) || v1[2] != float64(protocol.FireResultInvalidPosition) {
|
|
t.Fatalf("c1 invalid position result = %v, want [7, 4, 4]", v1)
|
|
}
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(4))
|
|
|
|
// 2. Player 1 hits ship at (0, 0)
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildFire(0, 0))
|
|
v1 = readMsg(t, c1)
|
|
// [7, 5, 2]
|
|
if v1[0] != float64(protocol.OpFireResult) || v1[1] != float64(5) || v1[2] != float64(protocol.FireResultHit) {
|
|
t.Fatalf("c1 hit result = %v, want [7, 5, 2]", v1)
|
|
}
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(5))
|
|
|
|
// 3. Player 1 misses at (9, 9)
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildFire(9, 9))
|
|
v1Fire := readMsg(t, c1)
|
|
// [7, 6, 1]
|
|
if v1Fire[0] != float64(protocol.OpFireResult) || v1Fire[1] != float64(6) || v1Fire[2] != float64(protocol.FireResultMiss) {
|
|
t.Fatalf("c1 miss result = %v, want [7, 6, 1]", v1Fire)
|
|
}
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(6))
|
|
|
|
// Turn transitions: c1 gets StateWaitOpponentTurn (5) with seq 7
|
|
v1State := readMsg(t, c1)
|
|
if v1State[0] != float64(protocol.OpState) || v1State[1] != float64(7) || v1State[2] != float64(protocol.StateWaitOpponentTurn) {
|
|
t.Fatalf("c1 state after miss = %v, want [3, 7, 5]", v1State)
|
|
}
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(7))
|
|
|
|
// c2 gets StateYourTurn (4) with seq 4
|
|
v2State := readMsg(t, c2)
|
|
if v2State[0] != float64(protocol.OpState) || v2State[1] != float64(4) || v2State[2] != float64(protocol.StateYourTurn) {
|
|
t.Fatalf("c2 state after miss = %v, want [3, 4, 4]", v2State)
|
|
}
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(4))
|
|
|
|
// 4. Player 1 tries to fire while it's Player 2's turn -> ignored
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildFire(5, 5))
|
|
|
|
// 5. Player 2 fires at (9, 4) -> single ship sunk
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildFire(9, 4))
|
|
v2Fire := readMsg(t, c2)
|
|
if v2Fire[0] != float64(protocol.OpFireResult) || v2Fire[1] != float64(5) || v2Fire[2] != float64(protocol.FireResultSunk) {
|
|
t.Fatalf("c2 sunk result = %v, want [7, 5, 3]", v2Fire)
|
|
}
|
|
}
|
|
|
|
func TestFireWinOverWebSocket(t *testing.T) {
|
|
url, _ := startServerWithManager(t, time.Minute)
|
|
exp := time.Now().Add(time.Hour)
|
|
gt := gameToken(t, "m-ws-fire-win", 1, 2, publicAddr, exp)
|
|
|
|
c1, c2 := dial(t, url), dial(t, url)
|
|
join(t, c1, gt, userToken(t, 1))
|
|
join(t, c2, gt, userToken(t, 2))
|
|
|
|
// Initial StatePlacingShips
|
|
readMsg(t, c1)
|
|
readMsg(t, c2)
|
|
|
|
// Both place ships
|
|
ships := validTestShips()
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildPlaceShips(ships))
|
|
readMsg(t, c1)
|
|
readMsg(t, c2)
|
|
|
|
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildPlaceShips(ships))
|
|
readMsg(t, c1) // StateYourTurn
|
|
readMsg(t, c2) // StateWaitOpponentTurn
|
|
|
|
// Player 1 destroys all 10 ships of Player 2
|
|
for i, ship := range ships {
|
|
cells := ship.Cells()
|
|
for j, c := range cells {
|
|
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildFire(c.X, c.Y))
|
|
v := readMsg(t, c1)
|
|
if j < len(cells)-1 {
|
|
if v[0] != float64(protocol.OpFireResult) || v[2] != float64(protocol.FireResultHit) {
|
|
t.Fatalf("ship %d cell %d: want Hit, got %v", i, j, v)
|
|
}
|
|
} else {
|
|
if v[0] != float64(protocol.OpFireResult) || v[2] != float64(protocol.FireResultSunk) {
|
|
t.Fatalf("ship %d cell %d: want Sunk, got %v", i, j, v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Player 1 receives END victory
|
|
v1End := readMsg(t, c1)
|
|
if v1End[0] != float64(protocol.OpEnd) || v1End[2] != float64(protocol.EndCodeVictory) || v1End[3] != "victory" {
|
|
t.Fatalf("c1 want END victory, got %v", v1End)
|
|
}
|
|
|
|
// Player 2 receives END defeat
|
|
v2End := readMsg(t, c2)
|
|
if v2End[0] != float64(protocol.OpEnd) || v2End[2] != float64(protocol.EndCodeDefeat) || v2End[3] != "defeat" {
|
|
t.Fatalf("c2 want END defeat, got %v", v2End)
|
|
}
|
|
|
|
// Connections should be closed with 1000
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
_, _, err1 := c1.Read(ctx)
|
|
if err1 == nil {
|
|
t.Fatal("expected c1 connection to be closed")
|
|
}
|
|
_, _, err2 := c2.Read(ctx)
|
|
if err2 == nil {
|
|
t.Fatal("expected c2 connection to be closed")
|
|
}
|
|
|
|
// Late reconnect gets END replayed and connection closed
|
|
c1Late := dial(t, url)
|
|
join(t, c1Late, gt, userToken(t, 1))
|
|
lateEnd := readMsg(t, c1Late)
|
|
if lateEnd[0] != float64(protocol.OpEnd) || lateEnd[2] != float64(protocol.EndCodeVictory) {
|
|
t.Fatalf("late join want END victory, got %v", lateEnd)
|
|
}
|
|
}
|
|
|
|
|