mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat(multiplayer): deliver allocated server rosters
This commit is contained in:
+28
-10
@@ -96,6 +96,7 @@ type AssignmentView struct {
|
||||
}
|
||||
|
||||
type AssignmentProvider func(context.Context, string, string, time.Time) (AssignmentView, error)
|
||||
type RosterProvider func(context.Context, domain.WorkloadBinding, time.Time) ([][]byte, error)
|
||||
|
||||
type Service struct {
|
||||
Sessions *domain.SessionStore
|
||||
@@ -113,6 +114,7 @@ type Service struct {
|
||||
ResultSubmitter ResultSubmitter
|
||||
ServerRegistrar ServerRegistrar
|
||||
Assignment AssignmentProvider
|
||||
Roster RosterProvider
|
||||
Now func() time.Time
|
||||
Proposals map[string]*domain.Proposal
|
||||
ProposalBackend ProposalBackend
|
||||
@@ -461,22 +463,17 @@ type serverRegistrationRequest struct {
|
||||
}
|
||||
|
||||
func (s *Service) serverMutation(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed")
|
||||
return
|
||||
}
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/servers/"), "/")
|
||||
if len(parts) != 2 || parts[0] == "" || (parts[1] != "result" && parts[1] != "register") {
|
||||
if len(parts) != 2 || parts[0] == "" || (parts[1] != "result" && parts[1] != "register" && parts[1] != "roster") {
|
||||
writeError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
if s.WorkloadVerify == nil || (parts[1] == "result" && s.ResultSubmitter == nil) || (parts[1] == "register" && s.ServerRegistrar == nil) {
|
||||
writeError(w, http.StatusServiceUnavailable, "server_unavailable")
|
||||
if parts[1] == "roster" && r.Method != http.MethodGet || parts[1] != "roster" && r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed")
|
||||
return
|
||||
}
|
||||
key := r.Header.Get("Idempotency-Key")
|
||||
if len(key) < 16 || len(key) > 128 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_idempotency_key")
|
||||
if s.WorkloadVerify == nil || (parts[1] == "result" && s.ResultSubmitter == nil) || (parts[1] == "register" && s.ServerRegistrar == nil) || (parts[1] == "roster" && s.Roster == nil) {
|
||||
writeError(w, http.StatusServiceUnavailable, "server_unavailable")
|
||||
return
|
||||
}
|
||||
partsAuth := strings.Fields(r.Header.Get("Authorization"))
|
||||
@@ -491,6 +488,27 @@ func (s *Service) serverMutation(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
if parts[1] == "roster" {
|
||||
roster, err := s.Roster(r.Context(), binding, now)
|
||||
if err != nil || len(roster) == 0 {
|
||||
writeError(w, http.StatusUnprocessableEntity, "roster_unavailable")
|
||||
return
|
||||
}
|
||||
encodedRoster := make([]json.RawMessage, 0, len(roster))
|
||||
for _, envelope := range roster {
|
||||
encodedRoster = append(encodedRoster, json.RawMessage(envelope))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(encodedRoster); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
key := r.Header.Get("Idempotency-Key")
|
||||
if len(key) < 16 || len(key) > 128 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_idempotency_key")
|
||||
return
|
||||
}
|
||||
if parts[1] == "register" {
|
||||
var input serverRegistrationRequest
|
||||
if !decodeBody(w, r, &input) {
|
||||
|
||||
@@ -329,6 +329,44 @@ func TestAuthenticatedWebSocketDeliversOnlyTargetedRevisionedEvents(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerRosterRequiresWorkloadBindingAndReturnsRawSignedEnvelopes(t *testing.T) {
|
||||
now := time.Unix(1000, 0).UTC()
|
||||
service := &Service{
|
||||
Now: func() time.Time { return now },
|
||||
WorkloadVerify: func(token string, at time.Time) (domain.WorkloadBinding, error) {
|
||||
if token != "workload-token" || !at.Equal(now) {
|
||||
t.Fatal("unexpected workload verification input")
|
||||
}
|
||||
return domain.WorkloadBinding{ServerID: "server-1", MatchID: "match-1", AllocationID: "allocation-1"}, nil
|
||||
},
|
||||
Roster: func(_ context.Context, binding domain.WorkloadBinding, at time.Time) ([][]byte, error) {
|
||||
if binding.ServerID != "server-1" || binding.MatchID != "match-1" || !at.Equal(now) {
|
||||
t.Fatal("unexpected roster binding")
|
||||
}
|
||||
return [][]byte{[]byte(`{"authorisation":{"player_id":"player-1"},"signature":"sig"}`)}, nil
|
||||
},
|
||||
}
|
||||
server := httptest.NewServer(service.Handler())
|
||||
defer server.Close()
|
||||
request, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/servers/server-1/roster", nil)
|
||||
request.Header.Set("Authorization", "Bearer workload-token")
|
||||
response, err := server.Client().Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("roster status=%d", response.StatusCode)
|
||||
}
|
||||
var roster []json.RawMessage
|
||||
if err := json.NewDecoder(response.Body).Decode(&roster); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(roster) != 1 || !bytes.Contains(roster[0], []byte(`"player_id":"player-1"`)) {
|
||||
t.Fatalf("roster=%s", roster[0])
|
||||
}
|
||||
}
|
||||
|
||||
func readServerWebSocketFrame(reader *bufio.Reader) ([]byte, error) {
|
||||
first, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user