Files
Piik/internal/client/loopback/server_test.go
T
AAA da47189fc3 refactor: unify Go runtime and audited frontend
Squash integration of the reviewed Go core, frontend audit fixes, release tooling, and deployment hygiene. CI passed all Server/Client jobs.
2026-09-06 23:26:01 +08:00

491 lines
15 KiB
Go

package loopback
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
)
const testOrigin = "https://share.bonfire.icu"
func TestStartServesHealthAndStrictControlHandshake(t *testing.T) {
expectedMedia := NativeMediaCapabilities{
Video: true, ProcessAudio: false, SystemAudio: true, HardwareH264: true,
}
server := startTestServerWithOptions(t, Options{
AllowedOrigins: []string{testOrigin},
NativeMedia: expectedMedia,
})
endpoint := server.Endpoint()
var health Health
response := doHealthRequest(t, endpoint, endpoint.Host, testOrigin)
if response.StatusCode != http.StatusOK {
t.Fatalf("health status = %d", response.StatusCode)
}
if err := json.NewDecoder(response.Body).Decode(&health); err != nil {
t.Fatal(err)
}
response.Body.Close()
if health.Protocol != ProtocolVersion || health.Service != ServiceName ||
health.Port != endpoint.Port || health.InstanceToken != endpoint.InstanceToken ||
health.NativeMedia != expectedMedia {
t.Fatalf("health = %+v", health)
}
connection := dialControl(t, endpoint, endpoint.InstanceToken, testOrigin)
defer connection.CloseNow()
writeControl(t, connection, requestJSON("request_hello", "hello"))
var ready controlMessage
readControl(t, connection, &ready)
if ready.Type != "ready" || ready.ID != "request_hello" {
t.Fatalf("ready = %+v", ready)
}
writeControl(t, connection, requestJSON("request_ping", "ping"))
var pong controlMessage
readControl(t, connection, &pong)
if pong.Type != "pong" || pong.ID != "request_ping" {
t.Fatalf("pong = %+v", pong)
}
}
func TestHealthRejectsUnexpectedOriginAndHost(t *testing.T) {
server := startTestServer(t, testOrigin)
endpoint := server.Endpoint()
response := doHealthRequest(t, endpoint, endpoint.Host, "https://other.example")
response.Body.Close()
if response.StatusCode != http.StatusForbidden {
t.Fatalf("bad origin status = %d", response.StatusCode)
}
response = doHealthRequest(t, endpoint, "127.0.0.2:"+strconv.Itoa(endpoint.Port), testOrigin)
response.Body.Close()
if response.StatusCode != http.StatusNotFound {
t.Fatalf("bad host status = %d", response.StatusCode)
}
}
func TestHealthAcceptsTheLocalHostAlias(t *testing.T) {
server := startTestServer(t, testOrigin)
endpoint := server.Endpoint()
response := doHealthRequest(t, endpoint,
"localhost:"+strconv.Itoa(endpoint.Port),
"http://localhost:"+strconv.Itoa(endpoint.Port),
)
response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("localhost health status = %d", response.StatusCode)
}
}
func TestPresentationUpdatesOnlyLanguageWithoutClaimingMediaControl(t *testing.T) {
languages := make(chan string, 3)
var server *Server
server = startTestServerWithOptions(t, Options{
AllowedOrigins: []string{testOrigin},
Presentation: func(language string) {
server.SetAllowedOrigins([]string{testOrigin})
languages <- language
},
})
endpoint := server.Endpoint()
connection := dialControl(t, endpoint, endpoint.InstanceToken, testOrigin)
defer connection.CloseNow()
writeControl(t, connection, requestJSON("request_hello", "hello"))
var ready controlMessage
readControl(t, connection, &ready)
client := &http.Client{Timeout: time.Second}
for _, input := range []struct {
method, path, origin, body, language string
status int
}{
{http.MethodPost, "/presentation", testOrigin, `{"language":"zh"}`, "zh", http.StatusNoContent},
{http.MethodPost, "/presentation", testOrigin, `{"language":"en"}`, "en", http.StatusNoContent},
{http.MethodPost, "/presentation", endpoint.URL, `{"language":"vis"}`, "vis", http.StatusNoContent},
{http.MethodOptions, "/presentation", testOrigin, "", "", http.StatusNoContent},
{http.MethodPost, "/presentation", "https://other.example", `{"language":"en"}`, "", http.StatusForbidden},
{http.MethodOptions, "/presentation", "https://other.example", "", "", http.StatusForbidden},
{http.MethodOptions, "/health", testOrigin, "", "", http.StatusMethodNotAllowed},
{http.MethodGet, "/presentation", testOrigin, "", "", http.StatusMethodNotAllowed},
{http.MethodPost, "/presentation", testOrigin, `{"language":"other"}`, "", http.StatusBadRequest},
{http.MethodPost, "/presentation", testOrigin, `{"language":"en","capture":true}`, "", http.StatusBadRequest},
{http.MethodPost, "/presentation", testOrigin, `{"language":"en"}{}`, "", http.StatusBadRequest},
{http.MethodPost, "/presentation", testOrigin, strings.Repeat(" ", 257) + `{"language":"en"}`, "", http.StatusBadRequest},
} {
request, err := http.NewRequest(input.method, endpoint.URL+input.path, strings.NewReader(input.body))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Origin", input.origin)
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != input.status {
t.Fatalf("%s %s status = %d, want %d", input.method, input.path, response.StatusCode, input.status)
}
if input.method == http.MethodOptions && input.status == http.StatusNoContent &&
(response.Header.Get("Access-Control-Allow-Origin") != testOrigin ||
response.Header.Get("Access-Control-Allow-Methods") != "POST" ||
response.Header.Get("Access-Control-Allow-Headers") != "Content-Type") {
t.Fatal("presentation preflight did not preserve its bounded origin/method scope")
}
select {
case language := <-languages:
if language != input.language {
t.Fatalf("unexpected presentation callback: %q", language)
}
default:
if input.language != "" {
t.Fatal("presentation callback missing")
}
}
}
writeControl(t, connection, requestJSON("request_ping", "ping"))
var pong controlMessage
readControl(t, connection, &pong)
if pong.Type != "pong" {
t.Fatal("presentation changed the active media control")
}
}
func TestOnlyOneControlSessionIsClaimed(t *testing.T) {
server := startTestServer(t, testOrigin)
endpoint := server.Endpoint()
first := dialControl(t, endpoint, endpoint.InstanceToken, testOrigin)
defer first.CloseNow()
second, response, err := websocket.Dial(context.Background(), websocketURL(endpoint)+"/control", &websocket.DialOptions{
HTTPHeader: http.Header{"Origin": []string{testOrigin}},
Subprotocols: []string{ControlSubprotocol + "." + endpoint.InstanceToken},
})
if second != nil {
second.CloseNow()
}
if err == nil || response == nil || response.StatusCode != http.StatusConflict {
t.Fatalf("second control dial: conn=%v response=%v err=%v", second, response, err)
}
}
func TestControlSessionSharesOneBoundedSocketForResponsesAndEvents(t *testing.T) {
extension := &testControlSession{
events: make(chan any, 1),
closed: make(chan struct{}),
}
server := startTestServerWithOptions(t, Options{
AllowedOrigins: []string{testOrigin},
NewControl: func() ControlSession {
return extension
},
})
connection := dialControl(t, server.Endpoint(), server.Endpoint().InstanceToken, testOrigin)
writeControl(t, connection, requestJSON("request_hello", "hello"))
var ready controlMessage
readControl(t, connection, &ready)
extension.events <- controlMessage{
Version: ProtocolVersion, ID: "event_123456", Type: "extension-event",
}
writeControl(t, connection, requestJSON("request_extension", "extension"))
received := map[string]bool{}
for len(received) < 2 {
var message controlMessage
readControl(t, connection, &message)
received[message.Type] = true
}
if !received["extension-event"] || !received["extension-response"] {
t.Fatalf("control messages = %+v", received)
}
connection.CloseNow()
select {
case <-extension.closed:
case <-time.After(time.Second):
t.Fatal("control extension was not closed with its socket")
}
}
func TestControlClosesTheSocketWhenItsReaderRejectsAFrame(t *testing.T) {
server := startTestServer(t, testOrigin)
connection := dialControl(t, server.Endpoint(), server.Endpoint().InstanceToken, testOrigin)
defer connection.CloseNow()
writeControl(t, connection, requestJSON("request_hello", "hello"))
var ready controlMessage
readControl(t, connection, &ready)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := connection.Write(ctx, websocket.MessageBinary, []byte("invalid")); err != nil {
t.Fatal(err)
}
if _, _, err := connection.Read(ctx); err == nil || ctx.Err() != nil {
t.Fatalf("server did not close its rejected control socket: %v", err)
}
}
func TestControlCloseCancelsAHandlerWaitingForPicker(t *testing.T) {
extension := &blockingControlSession{
startedSignal: make(chan struct{}),
closedSignal: make(chan struct{}),
}
server := startTestServerWithOptions(t, Options{
AllowedOrigins: []string{testOrigin},
NewControl: func() ControlSession {
return extension
},
})
connection := dialControl(t, server.Endpoint(), server.Endpoint().InstanceToken, testOrigin)
writeControl(t, connection, requestJSON("request_hello", "hello"))
var ready controlMessage
readControl(t, connection, &ready)
writeControl(t, connection, requestJSON("request_extension", "extension"))
select {
case <-extension.startedSignal:
case <-time.After(time.Second):
t.Fatal("control handler did not start")
}
connection.CloseNow()
select {
case <-extension.closedSignal:
case <-time.After(time.Second):
t.Fatal("closing the control socket did not release the picker handler")
}
deadline := time.Now().Add(time.Second)
for {
second, response, err := websocket.Dial(context.Background(),
websocketURL(server.Endpoint())+"/control", &websocket.DialOptions{
HTTPHeader: http.Header{"Origin": []string{testOrigin}},
Subprotocols: []string{ControlSubprotocol + "." + server.Endpoint().InstanceToken},
})
if err == nil {
second.CloseNow()
return
}
if response != nil && response.Body != nil {
response.Body.Close()
}
if time.Now().After(deadline) {
t.Fatalf("control session was not released: %v", err)
}
time.Sleep(10 * time.Millisecond)
}
}
func TestControlSessionProcessesARequestBurstInOrder(t *testing.T) {
extension := &testControlSession{
events: make(chan any, 1),
closed: make(chan struct{}),
}
server := startTestServerWithOptions(t, Options{
AllowedOrigins: []string{testOrigin},
NewControl: func() ControlSession {
return extension
},
})
connection := dialControl(t, server.Endpoint(), server.Endpoint().InstanceToken, testOrigin)
defer connection.CloseNow()
writeControl(t, connection, requestJSON("request_hello", "hello"))
var ready controlMessage
readControl(t, connection, &ready)
const burst = 32
for index := 0; index < burst; index++ {
writeControl(t, connection, requestJSON(
fmt.Sprintf("request_extension_%d", index), "extension",
))
}
seen := make(map[string]bool, burst)
for len(seen) < burst {
var response controlMessage
readControl(t, connection, &response)
seen[response.ID] = true
}
for index := 0; index < burst; index++ {
if !seen[fmt.Sprintf("request_extension_%d", index)] {
t.Fatalf("request %d was not processed", index)
}
}
}
func TestStartAlwaysBindsToIPv4Loopback(t *testing.T) {
server := startTestServer(t, testOrigin)
host, _, err := net.SplitHostPort(server.Endpoint().Host)
if err != nil || host != "127.0.0.1" {
t.Fatalf("helper host = %q, %v", host, err)
}
}
func TestCloseStopsTheControlSession(t *testing.T) {
server := startTestServer(t, testOrigin)
endpoint := server.Endpoint()
connection := dialControl(t, endpoint, endpoint.InstanceToken, testOrigin)
if err := server.Close(); err != nil {
t.Fatal(err)
}
readContext, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if _, _, err := connection.Read(readContext); err == nil {
t.Fatal("control connection remained open after server close")
}
connection.CloseNow()
}
func TestUnexpectedServeFailureIsReported(t *testing.T) {
server := startTestServer(t, testOrigin)
if err := server.listener.Close(); err != nil {
t.Fatal(err)
}
select {
case err := <-server.Done():
if err == nil {
t.Fatal("unexpected listener close was reported as a clean shutdown")
}
case <-time.After(time.Second):
t.Fatal("serve failure was not reported")
}
}
func startTestServer(t *testing.T, origin string) *Server {
t.Helper()
return startTestServerWithOptions(t, Options{AllowedOrigins: []string{origin}})
}
func startTestServerWithOptions(t *testing.T, options Options) *Server {
t.Helper()
port := freePort(t)
options.PortStart = port
options.PortEnd = port
server, err := Start(context.Background(), options)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close() })
return server
}
func freePort(t *testing.T) int {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port
}
func doHealthRequest(t *testing.T, endpoint Endpoint, host, origin string) *http.Response {
t.Helper()
request, err := http.NewRequest(http.MethodGet, endpoint.URL+"/health", nil)
if err != nil {
t.Fatal(err)
}
request.Host = host
if origin != "" {
request.Header.Set("Origin", origin)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func dialControl(t *testing.T, endpoint Endpoint, token, origin string) *websocket.Conn {
t.Helper()
connection, _, err := websocket.Dial(context.Background(), websocketURL(endpoint)+"/control", &websocket.DialOptions{
HTTPHeader: http.Header{"Origin": []string{origin}},
Subprotocols: []string{ControlSubprotocol + "." + token},
})
if err != nil {
t.Fatal(err)
}
return connection
}
func websocketURL(endpoint Endpoint) string {
return strings.Replace(endpoint.URL, "http://", "ws://", 1)
}
func writeControl(t *testing.T, connection *websocket.Conn, value map[string]any) {
t.Helper()
if err := wsjson.Write(context.Background(), connection, value); err != nil {
t.Fatal(err)
}
}
func readControl(t *testing.T, connection *websocket.Conn, value any) {
t.Helper()
contextWithDeadline, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := wsjson.Read(contextWithDeadline, connection, value); err != nil {
t.Fatal(err)
}
}
func requestJSON(id, messageType string) map[string]any {
return map[string]any{
"version": ProtocolVersion,
"id": id,
"type": messageType,
}
}
type testControlSession struct {
events chan any
closed chan struct{}
}
type blockingControlSession struct {
started sync.Once
closed sync.Once
startedSignal chan struct{}
closedSignal chan struct{}
}
func (session *blockingControlSession) Handle(ctx context.Context, _ []byte) (any, error) {
session.started.Do(func() { close(session.startedSignal) })
<-ctx.Done()
return nil, ctx.Err()
}
func (session *blockingControlSession) Events() <-chan any {
return nil
}
func (session *blockingControlSession) Close() error {
session.closed.Do(func() { close(session.closedSignal) })
return nil
}
func (session *testControlSession) Handle(_ context.Context, payload []byte) (any, error) {
message, err := decodeEnvelope(payload)
if err != nil || message.Type != "extension" {
return nil, errors.New("unexpected extension request")
}
return controlMessage{
Version: ProtocolVersion, ID: message.ID, Type: "extension-response",
}, nil
}
func (session *testControlSession) Events() <-chan any {
return session.events
}
func (session *testControlSession) Close() error {
select {
case <-session.closed:
default:
close(session.closed)
}
return nil
}