package net import ( "bytes" "errors" "io" "net" "strings" "sync" "testing" "time" ) func TestHubConcurrency(t *testing.T) { h := NewHub() h.OnRemove(func(s *Session) {}) var wg sync.WaitGroup const n = 50 for i := 0; i < n; i++ { wg.Add(1) go func() { defer wg.Done() s := &Session{} h.Add(s) h.EnterRoom(s, 1) h.PlayersInRoom(1) h.LeaveRoom(s) h.HardRemove(s) }() } for i := 0; i < n; i++ { wg.Add(1) go func() { defer wg.Done() h.AllSessions() }() } wg.Wait() } type testConn struct{ data []byte } func (c *testConn) ReadMessage() (string, error) { return "", nil } func (c *testConn) Write(b []byte) (int, error) { c.data = append(c.data, b...); return len(b), nil } func (c *testConn) SetWriteDeadline(time.Time) error { return nil } func (c *testConn) Close() error { return nil } func (c *testConn) SetEcho(bool) error { return nil } // fakeNetConn is a net.Conn stub for the session read loop: Read reports the // peer hung up so the read loop ends immediately, Write captures what the // server sent, and read deadlines are recorded so a test can assert the // transport was armed. type fakeNetConn struct { mu sync.Mutex written bytes.Buffer deadline time.Time } func newFakeNetConn() *fakeNetConn { return &fakeNetConn{} } func (c *fakeNetConn) Read([]byte) (int, error) { return 0, io.EOF } func (c *fakeNetConn) Write(b []byte) (int, error) { c.mu.Lock() defer c.mu.Unlock() return c.written.Write(b) } func (c *fakeNetConn) Close() error { return nil } func (c *fakeNetConn) LocalAddr() net.Addr { return nil } func (c *fakeNetConn) RemoteAddr() net.Addr { return nil } func (c *fakeNetConn) SetDeadline(t time.Time) error { return c.SetReadDeadline(t) } func (c *fakeNetConn) SetReadDeadline(t time.Time) error { c.mu.Lock() defer c.mu.Unlock() c.deadline = t return nil } func (c *fakeNetConn) SetWriteDeadline(time.Time) error { return nil } func (c *fakeNetConn) output() string { c.mu.Lock() defer c.mu.Unlock() return c.written.String() } func (c *fakeNetConn) readDeadline() time.Time { c.mu.Lock() defer c.mu.Unlock() return c.deadline } // TestEnterRoomClearsCasinoWatch: detailed table watch is scoped to the room // it was set in, so entering a room stops a stale watch instead of silently // resuming it when the player returns. func TestEnterRoomClearsCasinoWatch(t *testing.T) { h := NewHub() s := &Session{} h.Add(s) s.CasinoWatchRoom = 7 s.CasinoWatchTable = "bj1" h.EnterRoom(s, 9) if s.CasinoWatchRoom != 0 || s.CasinoWatchTable != "" { t.Fatalf("watch survived a room change: room=%d table=%q", s.CasinoWatchRoom, s.CasinoWatchTable) } } // TestConnectionCapRefusalKeepsTheServerDrained: at maxConnections a new // connection is refused without holding a slot (the counter stays at the cap), // and the refused peer is told why. Once the admitted sessions end, the live // count is back to zero. func TestConnectionCapRefusalKeepsTheServerDrained(t *testing.T) { s := &Server{hub: NewHub()} s.handler = func(*Session, string) {} for i := 0; i < maxConnections; i++ { if !s.admitConnection() { t.Fatalf("connection %d of %d refused below the cap", i+1, maxConnections) } } refused := newFakeNetConn() s.handleSession(&Session{Conn: newTCPConn(refused)}, s.handler) if got := s.conns.Load(); got != maxConnections { t.Fatalf("live connection count after a refusal = %d, want %d (the refused connection held a slot)", got, maxConnections) } if !strings.Contains(refused.output(), "full capacity") { t.Fatalf("refused connection was not told the server is full: %q", refused.output()) } for i := 0; i < maxConnections; i++ { s.releaseConnection() } if got := s.conns.Load(); got != 0 { t.Fatalf("live connection count after every session ended = %d, want 0", got) } } // TestAdmittedSessionReleasesItsSlot: an admitted session whose client hangs // up returns its slot, so a client cannot leak capacity by disconnecting. func TestAdmittedSessionReleasesItsSlot(t *testing.T) { s := &Server{hub: NewHub()} s.handler = func(*Session, string) {} s.handleSession(&Session{Conn: newTCPConn(newFakeNetConn())}, s.handler) if got := s.conns.Load(); got != 0 { t.Fatalf("live connection count after the session ended = %d, want 0", got) } } // TestArmReadDeadlineArmsTheTransport: the idle window is applied to the real // transport (not just computed), and the window itself depends on the protocol // state: pre-auth prompts get the short one. func TestArmReadDeadlineArmsTheTransport(t *testing.T) { rec := newFakeNetConn() sess := &Session{Conn: newTCPConn(rec)} before := time.Now() armReadDeadline(sess, loginReadTimeout) got := rec.readDeadline() if got.Before(before.Add(loginReadTimeout)) || got.After(time.Now().Add(loginReadTimeout)) { t.Fatalf("armed read deadline = %v, want about %v", got, before.Add(loginReadTimeout)) } sess.SetState(StateAccountName) if got := sessionIdleTimeout(sess); got != loginReadTimeout { t.Fatalf("account-name window = %v, want %v", got, loginReadTimeout) } sess.SetState(StatePassword) if got := sessionIdleTimeout(sess); got != loginReadTimeout { t.Fatalf("password window = %v, want %v", got, loginReadTimeout) } sess.SetState(StateGame) if got := sessionIdleTimeout(sess); got != idleReadTimeout { t.Fatalf("in-game window = %v, want %v", got, idleReadTimeout) } } // TestIdleSessionDroppedWithoutAffectingActiveOne: an idle socket is dropped // once its armed read deadline expires, while a client that keeps sending // messages survives well past that window because every read re-arms the // deadline. func TestIdleSessionDroppedWithoutAffectingActiveOne(t *testing.T) { s := &Server{hub: NewHub()} var mu sync.Mutex served := 0 s.handler = func(*Session, string) { mu.Lock() served++ mu.Unlock() } const window = 200 * time.Millisecond s.idleTimeout = func(*Session) time.Duration { return window } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer ln.Close() go func() { for { conn, err := ln.Accept() if err != nil { return } sess := &Session{Conn: newTCPConn(conn)} sess.SetState(StateAccountName) s.hub.Add(sess) go s.handleSession(sess, s.handler) } }() idle, err := net.Dial("tcp", ln.Addr().String()) if err != nil { t.Fatal(err) } defer idle.Close() active, err := net.Dial("tcp", ln.Addr().String()) if err != nil { t.Fatal(err) } defer active.Close() // The active client talks for five windows' worth of messages: it can only // be served to the end if each message re-armed its read deadline. sent := 10 done := make(chan struct{}) go func() { defer close(done) for i := 0; i < sent; i++ { if _, err := active.Write([]byte("look\r\n")); err != nil { return } time.Sleep(window / 4) } }() // The silent client is dropped: its socket is closed by the server as soon // as the armed deadline expires. if err := idle.SetReadDeadline(time.Now().Add(10 * window)); err != nil { t.Fatal(err) } buf := make([]byte, 4096) for { n, err := idle.Read(buf) if n > 0 { continue } if err == nil { continue } if errors.Is(err, io.EOF) { break } t.Fatalf("idle client was not dropped after its read deadline: %v", err) } <-done mu.Lock() got := served mu.Unlock() if got < sent-2 { t.Fatalf("the active client was served %d of %d messages; its read deadline was not re-armed per message", got, sent) } } func TestSessionWriteConcurrency(t *testing.T) { conn := &testConn{} sess := &Session{Conn: conn} var wg sync.WaitGroup const n = 50 for i := 0; i < n; i++ { wg.Add(1) go func() { defer wg.Done() sess.Write("hello") sess.WriteLine("world") sess.Writef("formatted %d", 42) }() } wg.Wait() }