Files
gomoku/server/state/state_runner_test.go
T
tiennm99 4c068849c4 refactor(server): rename database package to lobby, drop ratel legacy files
Rename
- server/database/ → server/lobby/ (17 import sites + qualified references)
- Package was named "database" but there is no database — it's in-memory
  hashmaps for players/rooms/spectators. "lobby" accurately names its role:
  the multiplayer game lobby state (who is in what room, joining/leaving,
  spectating).

Ratel-online legacy cleanup
- Delete server/README.md — 100% Chinese, entirely about ratel card games
  (斗地主/跑得快/德州扑克/麻将/骗子酒馆/Uno). Zero relevance to gomoku.
- Delete server/build.sh + server/build.ps1 — multi-platform ratel-server
  build scripts with mixed Chinese comments, superseded by Makefile +
  Dockerfile.
- Delete server/docs/ — Chinese Docker deployment guide + quickstart,
  superseded by root docs/deployment-guide.md and root README.md.
- Delete server/demo.gif — ratel card-game demo screenshot.

Comment fixes: update "database" references in consts/const.go and
game/board.go package docs to point at "lobby" instead.

go vet + go test ./... green.
2026-04-11 16:08:46 +07:00

174 lines
4.8 KiB
Go

package state
import (
"testing"
"time"
"github.com/tiennm99/gomoku/server/consts"
"github.com/tiennm99/gomoku/server/lobby"
"github.com/tiennm99/gomoku/server/protocol"
)
// makeTestPlayer creates a Player with wired CmdCh and SendCh for use in tests.
// The player is NOT registered in the store (so RemovePlayer is safe to no-op).
func makeTestPlayer(id int64, name string) *lobby.Player {
p := &lobby.Player{
ID: id,
Name: name,
SendCh: make(chan *protocol.Response, 64),
CmdCh: make(chan *protocol.Request, 16),
}
return p
}
// drainSend returns all responses currently buffered in SendCh without blocking.
func drainSend(p *lobby.Player) []*protocol.Response {
var out []*protocol.Response
for {
select {
case r := <-p.SendCh:
out = append(out, r)
default:
return out
}
}
}
// TestWelcomeTransitionsToSetNickname verifies that welcomeState immediately
// returns StateSetNickname without reading from CmdCh.
func TestWelcomeTransitionsToSetNickname(t *testing.T) {
p := makeTestPlayer(1, "")
s := &welcomeState{}
next, err := s.Next(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if next != consts.StateSetNickname {
t.Errorf("got state %d, want StateSetNickname (%d)", next, consts.StateSetNickname)
}
}
// TestSetNicknameWithNameAlreadySet verifies immediate transition to home when
// player.Name is pre-populated (stateless handler already ran).
func TestSetNicknameWithNameAlreadySet(t *testing.T) {
p := makeTestPlayer(1, "Alice")
s := &setNicknameState{}
next, err := s.Next(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if next != consts.StateHome {
t.Errorf("got state %d, want StateHome (%d)", next, consts.StateHome)
}
}
// TestSetNicknameExitOnClosedChan verifies ErrClientExit when CmdCh is closed.
func TestSetNicknameExitOnClosedChan(t *testing.T) {
p := makeTestPlayer(1, "") // no name
close(p.CmdCh)
s := &setNicknameState{}
_, err := s.Next(p)
if err != ErrClientExit {
t.Errorf("expected ErrClientExit, got %v", err)
}
}
// TestSetNicknameStatefulRequestBeforeName verifies that a stateful request
// arriving on CmdCh before Name is set causes a transition to home with a
// default name assigned, and re-enqueues the request.
func TestSetNicknameStatefulRequestBeforeName(t *testing.T) {
p := makeTestPlayer(1, "") // no name yet
req := &protocol.Request{
Payload: &protocol.Request_CreateRoom{
CreateRoom: &protocol.CreateRoomRequest{},
},
}
p.CmdCh <- req
s := &setNicknameState{}
next, err := s.Next(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if next != consts.StateHome {
t.Errorf("got state %d, want StateHome (%d)", next, consts.StateHome)
}
if p.Name == "" {
t.Error("expected default name to be assigned")
}
// Request should be re-enqueued.
select {
case got := <-p.CmdCh:
if got != req {
t.Error("re-enqueued request does not match original")
}
case <-time.After(100 * time.Millisecond):
t.Error("request was not re-enqueued to CmdCh")
}
}
// TestRunnerExitsOnErrClientExit verifies that Run() terminates when a state
// returns ErrClientExit.
func TestRunnerExitsOnErrClientExit(t *testing.T) {
// Register a fake state that immediately exits.
const fakeState consts.StateID = 99
register(fakeState, stateFunc(func(_ *lobby.Player) (consts.StateID, error) {
return 0, ErrClientExit
}))
p := makeTestPlayer(999, "runner-test")
// Redirect first state to fakeState by temporarily overriding welcome.
origWelcome := registry[consts.StateWelcome]
register(consts.StateWelcome, stateFunc(func(_ *lobby.Player) (consts.StateID, error) {
return fakeState, nil
}))
defer register(consts.StateWelcome, origWelcome)
done := make(chan struct{})
go func() {
Run(p)
close(done)
}()
select {
case <-done:
// Good: goroutine exited.
case <-time.After(2 * time.Second):
t.Fatal("Run() did not exit within 2s after ErrClientExit")
}
}
// stateFunc is a helper to turn a function into a State for test overrides.
type stateFunc func(*lobby.Player) (consts.StateID, error)
func (f stateFunc) Next(p *lobby.Player) (consts.StateID, error) { return f(p) }
// TestHomeExitOnClientExitRequest verifies homeState returns ErrClientExit.
func TestHomeExitOnClientExitRequest(t *testing.T) {
p := makeTestPlayer(1, "Bob")
p.CmdCh <- &protocol.Request{
Payload: &protocol.Request_ClientExit{
ClientExit: &protocol.ClientExitRequest{},
},
}
s := &homeState{}
_, err := s.Next(p)
if err != ErrClientExit {
t.Errorf("expected ErrClientExit, got %v", err)
}
}
// TestHomeExitOnClosedChan verifies homeState returns ErrClientExit when
// CmdCh is closed (simulates disconnect).
func TestHomeExitOnClosedChan(t *testing.T) {
p := makeTestPlayer(1, "Bob")
close(p.CmdCh)
s := &homeState{}
_, err := s.Next(p)
if err != ErrClientExit {
t.Errorf("expected ErrClientExit, got %v", err)
}
}