🎉 live server seems to be working now
This commit is contained in:
227
third_party/sendspin-go/pkg/sendspin/server_client_test.go
vendored
Normal file
227
third_party/sendspin-go/pkg/sendspin/server_client_test.go
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
// ABOUTME: Tests for the ServerClient exported accessor surface
|
||||
// ABOUTME: Guards the shape the Group/GroupRole layer will build on in M2+
|
||||
package sendspin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/pkg/protocol"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// TestServerClient_Accessors confirms that the six exported accessors on
|
||||
// ServerClient return the values the struct was constructed with. These are
|
||||
// the methods the future Group/GroupRole layer will depend on, so their
|
||||
// shape is load-bearing — adding a test now pins it.
|
||||
func TestServerClient_Accessors(t *testing.T) {
|
||||
sc := &ServerClient{
|
||||
id: "client-abc",
|
||||
name: "Living Room",
|
||||
roles: []string{"player@v1", "metadata@v1"},
|
||||
}
|
||||
|
||||
if got := sc.ID(); got != "client-abc" {
|
||||
t.Errorf("ID() = %q, want %q", got, "client-abc")
|
||||
}
|
||||
if got := sc.Name(); got != "Living Room" {
|
||||
t.Errorf("Name() = %q, want %q", got, "Living Room")
|
||||
}
|
||||
roles := sc.Roles()
|
||||
if len(roles) != 2 || roles[0] != "player@v1" || roles[1] != "metadata@v1" {
|
||||
t.Errorf("Roles() = %v, want [player@v1 metadata@v1]", roles)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerClient_RolesReturnsCopy guards the defensive-copy contract.
|
||||
// Mutating the returned slice must not affect a subsequent Roles() call.
|
||||
func TestServerClient_RolesReturnsCopy(t *testing.T) {
|
||||
sc := &ServerClient{roles: []string{"player@v1", "metadata@v1"}}
|
||||
|
||||
first := sc.Roles()
|
||||
first[0] = "tampered"
|
||||
|
||||
second := sc.Roles()
|
||||
if second[0] != "player@v1" {
|
||||
t.Errorf("Roles() returned aliased slice: got %q after mutation, want %q", second[0], "player@v1")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerClient_HasRole covers both exact matches and versioned matches
|
||||
// (e.g., "player" should match "player@v1"). This mirrors the existing
|
||||
// Server.hasRole behavior, which the accessor replaces.
|
||||
func TestServerClient_HasRole(t *testing.T) {
|
||||
sc := &ServerClient{roles: []string{"player@v1", "metadata@v1"}}
|
||||
|
||||
cases := []struct {
|
||||
role string
|
||||
want bool
|
||||
}{
|
||||
{"player", true},
|
||||
{"player@v1", true},
|
||||
{"metadata", true},
|
||||
{"controller", false},
|
||||
{"artwork", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := sc.HasRole(tc.role); got != tc.want {
|
||||
t.Errorf("HasRole(%q) = %v, want %v", tc.role, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerClient_SendBufferFull guards the back-pressure behavior: Send
|
||||
// must not block when the buffered sendChan is full. Returning an error
|
||||
// lets the caller decide whether to drop, log, or disconnect.
|
||||
func TestServerClient_SendBufferFull(t *testing.T) {
|
||||
sc := &ServerClient{
|
||||
sendChan: make(chan interface{}, 1),
|
||||
}
|
||||
|
||||
if err := sc.Send("server/state", map[string]string{"a": "b"}); err != nil {
|
||||
t.Fatalf("first Send should succeed, got %v", err)
|
||||
}
|
||||
if err := sc.Send("server/state", map[string]string{"c": "d"}); err == nil {
|
||||
t.Error("second Send to full buffer should return error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerClient_SendBinaryBufferFull is the same back-pressure check
|
||||
// for the binary path (audio chunks go through here).
|
||||
func TestServerClient_SendBinaryBufferFull(t *testing.T) {
|
||||
sc := &ServerClient{
|
||||
sendChan: make(chan interface{}, 1),
|
||||
}
|
||||
if err := sc.SendBinary([]byte{0x01}); err != nil {
|
||||
t.Fatalf("first SendBinary should succeed, got %v", err)
|
||||
}
|
||||
if err := sc.SendBinary([]byte{0x02}); err == nil {
|
||||
t.Error("second SendBinary to full buffer should return error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerClient_StateAccessors confirms that State/Volume/Muted/Codec
|
||||
// return the mutable playback fields under the client's RWMutex. These
|
||||
// are the fields the M2 Group event bus carries in ClientStateChangedEvent,
|
||||
// so their shape is load-bearing for M3's role handlers.
|
||||
func TestServerClient_StateAccessors(t *testing.T) {
|
||||
sc := &ServerClient{
|
||||
state: "synchronized",
|
||||
volume: 72,
|
||||
muted: true,
|
||||
codec: "opus",
|
||||
}
|
||||
|
||||
if got := sc.State(); got != "synchronized" {
|
||||
t.Errorf("State() = %q, want %q", got, "synchronized")
|
||||
}
|
||||
if got := sc.Volume(); got != 72 {
|
||||
t.Errorf("Volume() = %d, want 72", got)
|
||||
}
|
||||
if got := sc.Muted(); got != true {
|
||||
t.Errorf("Muted() = %v, want true", got)
|
||||
}
|
||||
if got := sc.Codec(); got != "opus" {
|
||||
t.Errorf("Codec() = %q, want %q", got, "opus")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerClient_StateAccessorsConcurrent is a light race-detector bait.
|
||||
// Running Write/Read in parallel under -race should flag any missing lock.
|
||||
func TestServerClient_StateAccessorsConcurrent(t *testing.T) {
|
||||
sc := &ServerClient{state: "synchronized", volume: 50}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
sc.mu.Lock()
|
||||
sc.volume = i % 100
|
||||
sc.mu.Unlock()
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
_ = sc.Volume()
|
||||
_ = sc.State()
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
// TestNewServerClientFromConn_SendAndClose confirms the constructor +
|
||||
// writer + Close lifecycle works: messages enqueued via Send arrive on
|
||||
// the WebSocket, and Close stops the writer without panic.
|
||||
func TestNewServerClientFromConn_SendAndClose(t *testing.T) {
|
||||
// Use an in-process WebSocket pair via httptest.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read the one message we expect.
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
t.Errorf("server read: %v", err)
|
||||
return
|
||||
}
|
||||
var msg protocol.Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
t.Errorf("unmarshal: %v", err)
|
||||
return
|
||||
}
|
||||
if msg.Type != "server/hello" {
|
||||
t.Errorf("got type %q, want server/hello", msg.Type)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
wsURL := "ws" + srv.URL[len("http"):]
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
sc := NewServerClientFromConn(conn, "test-id", "Test", []string{"player@v1"}, nil)
|
||||
|
||||
if err := sc.Send("server/hello", map[string]string{"name": "test"}); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
|
||||
// Give the writer time to flush.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
sc.Close()
|
||||
// Double-close should not panic.
|
||||
sc.Close()
|
||||
}
|
||||
|
||||
// TestCreateAudioChunk confirms the exported helper produces the
|
||||
// correct binary frame format.
|
||||
func TestCreateAudioChunk(t *testing.T) {
|
||||
chunk := CreateAudioChunk(1000000, []byte{0xAA, 0xBB})
|
||||
if chunk[0] != AudioChunkMessageType {
|
||||
t.Errorf("type byte = %d, want %d", chunk[0], AudioChunkMessageType)
|
||||
}
|
||||
if len(chunk) != 9+2 {
|
||||
t.Errorf("len = %d, want 11", len(chunk))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateArtworkChunk confirms channel mapping and frame format.
|
||||
func TestCreateArtworkChunk(t *testing.T) {
|
||||
chunk := CreateArtworkChunk(2, 5000000, []byte{0xFF})
|
||||
expectedType := byte(protocol.ArtworkChannel0MessageType + 2)
|
||||
if chunk[0] != expectedType {
|
||||
t.Errorf("type byte = %d, want %d", chunk[0], expectedType)
|
||||
}
|
||||
if len(chunk) != protocol.BinaryMessageHeaderSize+1 {
|
||||
t.Errorf("len = %d, want %d", len(chunk), protocol.BinaryMessageHeaderSize+1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user