140 lines
3.4 KiB
Go
140 lines
3.4 KiB
Go
// ABOUTME: Integration test for server-initiated client discovery
|
|
// ABOUTME: Fakes an mDNS advertisement and verifies the server dials + handshakes
|
|
|
|
package sendspin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Sendspin/sendspin-go/pkg/protocol"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/hashicorp/mdns"
|
|
)
|
|
|
|
func TestServerDiscoversAndDialsClient(t *testing.T) {
|
|
// mDNS multicast is unreliable in some environments (CI sandboxes,
|
|
// Windows boxes with strict firewalls or Hyper-V virtual switches).
|
|
// Opt in with SENDSPIN_MULTICAST_TESTS=1 when running locally.
|
|
if os.Getenv("SENDSPIN_MULTICAST_TESTS") == "" {
|
|
t.Skip("requires multicast; set SENDSPIN_MULTICAST_TESTS=1 to enable")
|
|
}
|
|
|
|
// 1. Stand up a fake "player" HTTP server that upgrades to WS and
|
|
// sends client/hello.
|
|
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
|
|
playerReady := make(chan string, 1) // receives serverID via server/hello
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
t.Errorf("upgrade: %v", err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
hello := protocol.Message{
|
|
Type: "client/hello",
|
|
Payload: protocol.ClientHello{
|
|
ClientID: "test-player-1",
|
|
Name: "Test Player",
|
|
SupportedRoles: []string{"player@v1"},
|
|
},
|
|
}
|
|
data, _ := json.Marshal(hello)
|
|
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
|
t.Errorf("write hello: %v", err)
|
|
return
|
|
}
|
|
|
|
_, resp, err := conn.ReadMessage()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var msg protocol.Message
|
|
if err := json.Unmarshal(resp, &msg); err != nil {
|
|
return
|
|
}
|
|
if msg.Type == "server/hello" {
|
|
playerReady <- "ok"
|
|
}
|
|
|
|
// Block until the server closes us.
|
|
for {
|
|
if _, _, err := conn.ReadMessage(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
_, portStr, err := net.SplitHostPort(strings.TrimPrefix(ts.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("split host: %v", err)
|
|
}
|
|
port, _ := strconv.Atoi(portStr)
|
|
|
|
// 2. Advertise an mDNS _sendspin._tcp record pointing at httptest.
|
|
ips := []net.IP{net.ParseIP("127.0.0.1")}
|
|
svc, err := mdns.NewMDNSService(
|
|
"test-player-instance",
|
|
"_sendspin._tcp",
|
|
"",
|
|
"",
|
|
port,
|
|
ips,
|
|
[]string{"path=/", "name=Test Player"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("mdns service: %v", err)
|
|
}
|
|
mdnsServer, err := mdns.NewServer(&mdns.Config{Zone: svc})
|
|
if err != nil {
|
|
t.Fatalf("mdns server: %v", err)
|
|
}
|
|
defer mdnsServer.Shutdown()
|
|
|
|
// 3. Start a Sendspin server with DiscoverClients=true.
|
|
source := NewTestTone(48000, 2)
|
|
srv, err := NewServer(ServerConfig{
|
|
Port: findFreePort(t),
|
|
Name: "Test Server",
|
|
Source: source,
|
|
EnableMDNS: false,
|
|
DiscoverClients: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewServer: %v", err)
|
|
}
|
|
|
|
serverDone := make(chan error, 1)
|
|
go func() { serverDone <- srv.Start() }()
|
|
defer srv.Stop()
|
|
|
|
// 4. Wait for the player to confirm it received server/hello.
|
|
select {
|
|
case <-playerReady:
|
|
// success
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("timed out waiting for dialed handshake")
|
|
}
|
|
}
|
|
|
|
// findFreePort returns a TCP port that is free at call time.
|
|
func findFreePort(t *testing.T) int {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer l.Close()
|
|
return l.Addr().(*net.TCPAddr).Port
|
|
}
|