239 lines
6.2 KiB
Go
239 lines
6.2 KiB
Go
package hub
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// newTestConn returns a real *websocket.Conn (client side) backed by a local
|
|
// httptest server. The server-side handler blocks on a done channel until the
|
|
// test ends; cleanup closes the client conn first, then the server, avoiding
|
|
// the httptest.Server.Close() hang that occurs when the handler is blocked on
|
|
// ReadMessage.
|
|
func newTestConn(t *testing.T) *websocket.Conn {
|
|
t.Helper()
|
|
done := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
up := websocket.Upgrader{}
|
|
c, err := up.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
t.Errorf("upgrade: %v", err)
|
|
return
|
|
}
|
|
<-done
|
|
_ = c.Close()
|
|
}))
|
|
cli, _, err := websocket.DefaultDialer.Dial(strings.Replace(srv.URL, "http:", "ws:", 1), nil)
|
|
if err != nil {
|
|
close(done)
|
|
srv.Close()
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
close(done)
|
|
_ = cli.Close()
|
|
srv.Close()
|
|
})
|
|
return cli
|
|
}
|
|
|
|
// TestRegisterAndUnregister verifies basic Hub register/unregister bookkeeping.
|
|
func TestRegisterAndUnregister(t *testing.T) {
|
|
h := NewHub(5)
|
|
conn := newTestConn(t)
|
|
|
|
c, err := h.Register("user-1", conn)
|
|
if err != nil {
|
|
t.Fatalf("Register: %v", err)
|
|
}
|
|
if c.UserID != "user-1" {
|
|
t.Errorf("UserID = %q, want user-1", c.UserID)
|
|
}
|
|
if c.ConnID == "" {
|
|
t.Error("ConnID should be non-empty")
|
|
}
|
|
if h.ActiveConnections() != 1 {
|
|
t.Errorf("ActiveConnections = %d, want 1", h.ActiveConnections())
|
|
}
|
|
if h.UserCount() != 1 {
|
|
t.Errorf("UserCount = %d, want 1", h.UserCount())
|
|
}
|
|
if !h.HasUser("user-1") {
|
|
t.Error("HasUser(user-1) = false, want true")
|
|
}
|
|
|
|
h.Unregister(c)
|
|
if h.ActiveConnections() != 0 {
|
|
t.Errorf("after Unregister ActiveConnections = %d, want 0", h.ActiveConnections())
|
|
}
|
|
if h.HasUser("user-1") {
|
|
t.Error("HasUser(user-1) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestRegisterTooManyConnections verifies the per-user cap is enforced.
|
|
func TestRegisterTooManyConnections(t *testing.T) {
|
|
h := NewHub(2)
|
|
for i := 0; i < 2; i++ {
|
|
conn := newTestConn(t)
|
|
if _, err := h.Register("user-cap", conn); err != nil {
|
|
t.Fatalf("Register #%d: %v", i, err)
|
|
}
|
|
}
|
|
conn := newTestConn(t)
|
|
_, err := h.Register("user-cap", conn)
|
|
if err != ErrTooManyConnections {
|
|
t.Errorf("Register over cap err = %v, want ErrTooManyConnections", err)
|
|
}
|
|
}
|
|
|
|
// TestRegisterWhenClosing verifies that CloseAll rejects further Register.
|
|
func TestRegisterWhenClosing(t *testing.T) {
|
|
h := NewHub(5)
|
|
h.CloseAll()
|
|
conn := newTestConn(t)
|
|
_, err := h.Register("user-x", conn)
|
|
if err != ErrHubClosing {
|
|
t.Errorf("Register after CloseAll err = %v, want ErrHubClosing", err)
|
|
}
|
|
if !h.IsClosing() {
|
|
t.Error("IsClosing = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestSendToUser verifies message delivery to a single user.
|
|
func TestSendToUser(t *testing.T) {
|
|
h := NewHub(5)
|
|
conn := newTestConn(t)
|
|
c, err := h.Register("user-send", conn)
|
|
if err != nil {
|
|
t.Fatalf("Register: %v", err)
|
|
}
|
|
defer h.Unregister(c)
|
|
|
|
msg := []byte(`{"type":"message","event":"test"}`)
|
|
delivered := h.SendToUser("user-send", msg)
|
|
if delivered != 1 {
|
|
t.Errorf("SendToUser delivered = %d, want 1", delivered)
|
|
}
|
|
|
|
// Unknown user: 0 delivered.
|
|
delivered = h.SendToUser("user-absent", msg)
|
|
if delivered != 0 {
|
|
t.Errorf("SendToUser absent delivered = %d, want 0", delivered)
|
|
}
|
|
}
|
|
|
|
// TestBroadcast verifies message delivery to all connections.
|
|
func TestBroadcast(t *testing.T) {
|
|
h := NewHub(5)
|
|
conn1 := newTestConn(t)
|
|
conn2 := newTestConn(t)
|
|
c1, _ := h.Register("u1", conn1)
|
|
c2, _ := h.Register("u2", conn2)
|
|
defer h.Unregister(c1)
|
|
defer h.Unregister(c2)
|
|
|
|
msg := []byte(`{"type":"message","event":"broadcast"}`)
|
|
reached := h.Broadcast(msg)
|
|
if reached != 2 {
|
|
t.Errorf("Broadcast reached = %d, want 2", reached)
|
|
}
|
|
}
|
|
|
|
// TestPresenceHooks verifies that Register/Unregister call the hooks.
|
|
func TestPresenceHooks(t *testing.T) {
|
|
h := NewHub(5)
|
|
var mu sync.Mutex
|
|
registered := []string{}
|
|
unregistered := []string{}
|
|
h.SetPresenceHooks(
|
|
func(userID, connID string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
registered = append(registered, userID)
|
|
},
|
|
func(userID, connID string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
unregistered = append(unregistered, userID)
|
|
},
|
|
)
|
|
|
|
conn := newTestConn(t)
|
|
c, _ := h.Register("hook-user", conn)
|
|
h.Unregister(c)
|
|
|
|
if len(registered) != 1 || registered[0] != "hook-user" {
|
|
t.Errorf("registered = %v, want [hook-user]", registered)
|
|
}
|
|
if len(unregistered) != 1 || unregistered[0] != "hook-user" {
|
|
t.Errorf("unregistered = %v, want [hook-user]", unregistered)
|
|
}
|
|
}
|
|
|
|
// TestForEachUser verifies iteration over online users (used by ISSUE-058 rebuild).
|
|
func TestForEachUser(t *testing.T) {
|
|
h := NewHub(5)
|
|
conn1 := newTestConn(t)
|
|
conn2 := newTestConn(t)
|
|
c1, _ := h.Register("u-a", conn1)
|
|
c2, _ := h.Register("u-b", conn2)
|
|
defer h.Unregister(c1)
|
|
defer h.Unregister(c2)
|
|
|
|
seen := map[string]bool{}
|
|
h.ForEachUser(func(userID string) {
|
|
seen[userID] = true
|
|
})
|
|
if len(seen) != 2 || !seen["u-a"] || !seen["u-b"] {
|
|
t.Errorf("ForEachUser seen = %v, want {u-a, u-b}", seen)
|
|
}
|
|
}
|
|
|
|
// TestConnectionSendOnClosed verifies Send returns false after Close.
|
|
func TestConnectionSendOnClosed(t *testing.T) {
|
|
h := NewHub(5)
|
|
conn := newTestConn(t)
|
|
c, _ := h.Register("u-close", conn)
|
|
c.Close()
|
|
if ok := c.Send([]byte("x")); ok {
|
|
t.Error("Send after Close = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestConnectionSendChannelFull verifies Send returns false when channel is full.
|
|
func TestConnectionSendChannelFull(t *testing.T) {
|
|
h := NewHub(5)
|
|
conn := newTestConn(t)
|
|
c, _ := h.Register("u-full", conn)
|
|
defer h.Unregister(c)
|
|
// Fill the send channel (cap 64).
|
|
for i := 0; i < sendBufferSize; i++ {
|
|
if !c.Send([]byte("x")) {
|
|
t.Fatalf("Send #%d returned false unexpectedly", i)
|
|
}
|
|
}
|
|
// Next Send should drop.
|
|
if ok := c.Send([]byte("overflow")); ok {
|
|
t.Error("Send on full channel = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestNewHubDefaultMaxConns verifies NewHub applies a default when maxConns <= 0.
|
|
func TestNewHubDefaultMaxConns(t *testing.T) {
|
|
h := NewHub(0)
|
|
if h.maxConnsPerUser != 5 {
|
|
t.Errorf("NewHub(0) maxConnsPerUser = %d, want default 5", h.maxConnsPerUser)
|
|
}
|
|
h2 := NewHub(-1)
|
|
if h2.maxConnsPerUser != 5 {
|
|
t.Errorf("NewHub(-1) maxConnsPerUser = %d, want default 5", h2.maxConnsPerUser)
|
|
}
|
|
}
|