feat: add authenticated matchmaking event stream

This commit is contained in:
Josh Creek
2026-08-31 23:05:04 +01:00
parent d77563147c
commit d258f17852
4 changed files with 378 additions and 2 deletions
+96
View File
@@ -1,8 +1,13 @@
package api
import (
"bufio"
"context"
"encoding/binary"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
@@ -161,6 +166,97 @@ func TestDocumentedContractRoutesAdaptToServiceAPI(t *testing.T) {
}
}
func TestAuthenticatedWebSocketDeliversOnlyTargetedRevisionedEvents(t *testing.T) {
service := &Service{SessionBackend: &sessionBackendSpy{}}
server := httptest.NewServer(service.Handler())
defer server.Close()
invalid, err := http.NewRequest(http.MethodGet, server.URL+"/v1/events", nil)
if err != nil {
t.Fatal(err)
}
invalid.Header.Set("Upgrade", "websocket")
invalid.Header.Set("Connection", "Upgrade")
invalid.Header.Set("Sec-WebSocket-Key", "not-a-websocket-key")
invalid.Header.Set("Authorization", "Bearer session-1:token-1")
invalidResponse, err := server.Client().Do(invalid)
if err != nil {
t.Fatal(err)
}
_ = invalidResponse.Body.Close()
if invalidResponse.StatusCode != http.StatusBadRequest {
t.Fatalf("invalid handshake status = %d", invalidResponse.StatusCode)
}
connection, err := net.Dial("tcp", strings.TrimPrefix(server.URL, "http://"))
if err != nil {
t.Fatal(err)
}
defer connection.Close()
_, err = io.WriteString(connection, "GET /v1/events HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nAuthorization: Bearer session-1:token-1\r\n\r\n")
if err != nil {
t.Fatal(err)
}
reader := bufio.NewReader(connection)
status, err := reader.ReadString('\n')
if err != nil {
t.Fatal(err)
}
if !strings.Contains(status, "101 Switching Protocols") {
t.Fatalf("handshake status = %q", status)
}
for {
line, err := reader.ReadString('\n')
if err != nil {
t.Fatal(err)
}
if line == "\r\n" {
break
}
}
time.Sleep(10 * time.Millisecond)
if err := service.PublishControlPlaneEvent(ControlPlaneEvent{Event: "state_changed", Revision: 1, ResourceID: "ticket-1234567890123456", OccurredAt: time.Unix(1000, 0).UTC(), State: "QUEUED", PlayerID: "player-1"}); err != nil {
t.Fatal(err)
}
if err := service.PublishControlPlaneEvent(ControlPlaneEvent{Event: "state_changed", Revision: 2, ResourceID: "ticket-1234567890123456", OccurredAt: time.Unix(1001, 0).UTC(), State: "PROPOSED", PlayerID: "player-2"}); err != nil {
t.Fatal(err)
}
first, err := readServerWebSocketFrame(reader)
if err != nil {
t.Fatal(err)
}
var event ControlPlaneEvent
if err := json.Unmarshal(first, &event); err != nil {
t.Fatal(err)
}
if event.PlayerID != "" || event.Revision != 1 || event.ResourceID != "ticket-1234567890123456" || event.State != "QUEUED" {
t.Fatalf("event = %+v", event)
}
}
func readServerWebSocketFrame(reader *bufio.Reader) ([]byte, error) {
first, err := reader.ReadByte()
if err != nil {
return nil, err
}
second, err := reader.ReadByte()
if err != nil {
return nil, err
}
if first&0x0f != 0x1 || second&0x80 != 0 {
return nil, errors.New("unexpected server websocket frame")
}
length := int(second & 0x7f)
if length == 126 {
var extended uint16
if err := binary.Read(reader, binary.BigEndian, &extended); err != nil {
return nil, err
}
length = int(extended)
}
payload := make([]byte, length)
_, err = io.ReadFull(reader, payload)
return payload, err
}
func TestQueueAPIRejectsUnauthenticatedUnknownAndOversizedInput(t *testing.T) {
service := &Service{Sessions: domain.NewSessionStore(), Queue: domain.NewQueue(), Candidate: func(string, string) (domain.Candidate, error) { return domain.Candidate{}, nil }}
server := httptest.NewServer(service.Handler())