mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): reconcile authoritative initial connections
This commit is contained in:
@@ -23,6 +23,41 @@ func TestSupervisorDefaultHTTPClientHasRequestDeadline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocatedChildReceivesConnectionReportingEnvironmentWithoutCommandSecrets(t *testing.T) {
|
||||
s, err := New(Config{
|
||||
Command: []string{"game-server"}, ControlPlaneURL: "https://control.example",
|
||||
ServerID: "server-1", ProtocolVersion: 1, ImageDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.lastGameServer.ObjectMeta.Annotations = map[string]string{"cosmic-clash.io/workload-token": "signed-workload-token"}
|
||||
environment, err := s.controlPlaneChildEnvironment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(environment, "\n")
|
||||
if !strings.Contains(joined, ChildControlPlaneURLEnv+"=https://control.example") || !strings.Contains(joined, ChildWorkloadTokenEnv+"=signed-workload-token") || strings.Contains(joined, ChildAdmissionSignalEnv) {
|
||||
t.Fatalf("child connection-reporting environment = %v", environment)
|
||||
}
|
||||
if strings.Contains(strings.Join(s.config.Command, " "), "signed-workload-token") {
|
||||
t.Fatal("workload token leaked into child command arguments")
|
||||
}
|
||||
s.config.AdmissionURL = "http://127.0.0.1:7780/initial-connect-ready"
|
||||
environment, err = s.controlPlaneChildEnvironment()
|
||||
if err != nil || !strings.Contains(strings.Join(environment, "\n"), ChildAdmissionSignalEnv+"=1") {
|
||||
t.Fatalf("child admission signal environment = %v err=%v", environment, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupervisorRejectsUnsafeControlPlaneOrigins(t *testing.T) {
|
||||
for _, raw := range []string{"control.example", "https://user:secret@control.example", "https://control.example/path", "https://control.example?token=secret"} {
|
||||
if _, err := New(Config{Command: []string{"game-server"}, ControlPlaneURL: raw, ServerID: "server-1", ProtocolVersion: 1, ImageDigest: "sha256:" + strings.Repeat("a", 64)}); err == nil {
|
||||
t.Fatalf("unsafe control-plane URL accepted: %q", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithAllocatedConfigOverridesAuthoritativeChildFlags(t *testing.T) {
|
||||
command := []string{
|
||||
"game-server", "--", "--allocated-mode", "--match-id=stale-match",
|
||||
@@ -265,6 +300,7 @@ func TestControlPlaneRegistrationReportsProcessReadyThenAssignmentReady(t *testi
|
||||
func TestAssignmentReadyRegistrationRetriesUntilTheControlPlaneCatchesUp(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
assignmentReadyAttempts := 0
|
||||
admissionCalled := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/gameserver":
|
||||
@@ -288,6 +324,15 @@ func TestAssignmentReadyRegistrationRetriesUntilTheControlPlaneCatchesUp(t *test
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case r.URL.Path == "/initial-connect-ready":
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if assignmentReadyAttempts != 3 || r.Header.Get("Authorization") != "Bearer control-token-123456" {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
admissionCalled = true
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
@@ -301,13 +346,14 @@ func TestAssignmentReadyRegistrationRetriesUntilTheControlPlaneCatchesUp(t *test
|
||||
s, err := New(Config{
|
||||
Command: []string{"/bin/sh", "-c", "true"}, SDKBaseURL: server.URL, ReadyURL: server.URL + "/ready-probe", ReadyTimeout: time.Second, PollInterval: time.Millisecond,
|
||||
ControlPlaneURL: server.URL, WorkloadTokenPath: tokenPath, ServerID: "server-1", MatchID: "match-1", ProtocolVersion: 1, ImageDigest: "sha256:aa",
|
||||
DrainURL: server.URL + "/drain", AdmissionURL: server.URL + "/initial-connect-ready", DrainToken: "control-token-123456",
|
||||
AssignmentReadyAttempts: 5, AssignmentReadyBackoff: time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Start must still succeed -- a slow-to-propagate assignment-ready must
|
||||
// never be treated as a Start() failure (which would kill the child).
|
||||
// A transient conflict is retried inside Start; the assignment only becomes
|
||||
// visible after the durable transition eventually succeeds.
|
||||
if err := s.Start(context.Background()); err != nil {
|
||||
t.Fatalf("Start failed despite assignment-ready eventually succeeding: %v", err)
|
||||
}
|
||||
@@ -319,6 +365,42 @@ func TestAssignmentReadyRegistrationRetriesUntilTheControlPlaneCatchesUp(t *test
|
||||
if assignmentReadyAttempts != 3 {
|
||||
t.Fatalf("assignment-ready attempts = %d, want exactly 3 (2 conflicts then success)", assignmentReadyAttempts)
|
||||
}
|
||||
if !admissionCalled {
|
||||
t.Fatal("initial-connect clock was not armed after durable assignment readiness")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistentAssignmentReadyFailureFailsClosed(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/gameserver":
|
||||
_, _ = w.Write([]byte(`{"object_meta":{"annotations":{"cosmic-clash.io/match-id":"match-1","cosmic-clash.io/workload-token":"workload-token"}},"status":{"address":"127.0.0.1","ports":[{"name":"game","port":31001}]}}`))
|
||||
case "/ready-probe", "/ready":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case "/v1/servers/server-1/register":
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if strings.Contains(string(body), `"assignment_ready":true`) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
s, err := New(Config{
|
||||
Command: []string{"/bin/sh", "-c", "sleep 30"}, SDKBaseURL: server.URL, ReadyURL: server.URL + "/ready-probe",
|
||||
ControlPlaneURL: server.URL, ServerID: "server-1", ProtocolVersion: 1, ImageDigest: "sha256:aa",
|
||||
ReadyTimeout: time.Second, PollInterval: time.Millisecond, AssignmentReadyAttempts: 2, AssignmentReadyBackoff: time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Start(context.Background()); err == nil || !strings.Contains(err.Error(), "assignment-ready registration did not succeed") {
|
||||
t.Fatalf("persistent assignment-ready failure did not fail closed: %v", err)
|
||||
}
|
||||
_ = s.Wait()
|
||||
}
|
||||
|
||||
func TestControlPlaneRegistrationFallsBackToGameServerAnnotationForMatchID(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user