capsule AI-native Unix-like composition layer

src/server/internal/ws/hub_test.go

2,860 bytes · 142 lines · capsule://quake0day/[email protected] raw on github

package ws

import (
	"testing"
	"time"

	"github.com/gorilla/websocket"
)

func newTestClient(hub *Hub, sessionID string) *Client {
	return &Client{
		SessionID: sessionID,
		Conn:      &websocket.Conn{}, // nil-safe for non-network tests
		Send:      make(chan []byte, 64),
		hub:       hub,
	}
}

func TestHubRegisterUnregister(t *testing.T) {
	hub := NewHub()
	client := newTestClient(hub, "session-1")

	hub.Register(client)

	hub.mu.RLock()
	clients, ok := hub.clients["session-1"]
	hub.mu.RUnlock()

	if !ok || len(clients) != 1 {
		t.Fatal("expected 1 client registered for session-1")
	}

	hub.Unregister(client)

	hub.mu.RLock()
	_, ok = hub.clients["session-1"]
	hub.mu.RUnlock()

	if ok {
		t.Error("expected session-1 to be removed after last client unregisters")
	}
}

func TestHubBroadcast(t *testing.T) {
	hub := NewHub()
	c1 := newTestClient(hub, "s1")
	c2 := newTestClient(hub, "s1")
	c3 := newTestClient(hub, "s2") // different session

	hub.Register(c1)
	hub.Register(c2)
	hub.Register(c3)

	msg := []byte(`{"type":"test"}`)
	hub.Broadcast("s1", msg)

	// c1 and c2 should receive the message
	select {
	case received := <-c1.Send:
		if string(received) != string(msg) {
			t.Errorf("c1 got %s, want %s", received, msg)
		}
	case <-time.After(time.Second):
		t.Error("c1 did not receive message")
	}

	select {
	case received := <-c2.Send:
		if string(received) != string(msg) {
			t.Errorf("c2 got %s, want %s", received, msg)
		}
	case <-time.After(time.Second):
		t.Error("c2 did not receive message")
	}

	// c3 should not receive the message
	select {
	case <-c3.Send:
		t.Error("c3 should not receive message from different session")
	case <-time.After(50 * time.Millisecond):
		// expected
	}

	// Cleanup
	hub.Unregister(c1)
	hub.Unregister(c2)
	hub.Unregister(c3)
}

func TestHubBroadcastNoSession(t *testing.T) {
	hub := NewHub()
	// Should not panic
	hub.Broadcast("nonexistent", []byte("hello"))
}

func TestHubBroadcastJSON(t *testing.T) {
	hub := NewHub()
	c := newTestClient(hub, "s1")
	hub.Register(c)

	hub.BroadcastJSON("s1", map[string]string{"type": "test", "data": "hello"})

	select {
	case msg := <-c.Send:
		if len(msg) == 0 {
			t.Error("expected non-empty JSON message")
		}
	case <-time.After(time.Second):
		t.Error("did not receive JSON broadcast")
	}

	hub.Unregister(c)
}

func TestHubMultipleSessionsIsolated(t *testing.T) {
	hub := NewHub()
	c1 := newTestClient(hub, "session-a")
	c2 := newTestClient(hub, "session-b")

	hub.Register(c1)
	hub.Register(c2)

	hub.Broadcast("session-a", []byte("for-a"))

	select {
	case <-c1.Send:
		// expected
	case <-time.After(time.Second):
		t.Error("c1 should receive message")
	}

	select {
	case <-c2.Send:
		t.Error("c2 should not receive message for session-a")
	case <-time.After(50 * time.Millisecond):
		// expected
	}

	hub.Unregister(c1)
	hub.Unregister(c2)
}