extends "res://tests/test_case.gd" const ControlPlaneClient = preload("res://scripts/control_plane_client.gd") const RankedProfileState = preload("res://scripts/ranked_profile_state.gd") func test_base_url_validation_rejects_ambiguous_or_insecure_values() -> void: assert_true(ControlPlaneClient.is_valid_base_url("http://127.0.0.1:8080"), "local HTTP endpoint is valid") assert_true(ControlPlaneClient.is_valid_base_url("https://match.example"), "HTTPS endpoint is valid") assert_true(not ControlPlaneClient.is_valid_base_url("match.example"), "scheme is required") assert_true(not ControlPlaneClient.is_valid_base_url("http://match.example/"), "trailing slash is normalized before validation") assert_true(not ControlPlaneClient.is_valid_base_url("http://match example"), "whitespace is rejected") assert_true(not ControlPlaneClient.is_valid_base_url("https://user:pass@match.example"), "userinfo is rejected") assert_true(not ControlPlaneClient.is_valid_base_url("https://match.example?token=secret"), "query strings are rejected") var client := ControlPlaneClient.new() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "safe access token configures") assert_true(not client.configure("https://match.example", "token\nforged-header"), "header injection is rejected") assert_true(ControlPlaneClient.is_valid_web_api_ticket("ticket-value"), "ordinary Steam Web API ticket is accepted") assert_true(not ControlPlaneClient.is_valid_web_api_ticket("ticket\nforged"), "ticket header characters are rejected") assert_true(not ControlPlaneClient.is_valid_web_api_ticket(""), "empty Steam ticket is rejected") assert_true(ControlPlaneClient.is_valid_access_token("session-id:opaque-token"), "opaque session format is accepted") assert_true(not ControlPlaneClient.is_valid_access_token(":opaque-token"), "missing session identifier is rejected") assert_true(not ControlPlaneClient.is_valid_access_token("session-id:token\nforged"), "session header injection is rejected") assert_eq(ControlPlaneClient.websocket_url("https://match.example"), "wss://match.example", "TLS control plane uses secure WebSocket") assert_eq(ControlPlaneClient.websocket_url("http://127.0.0.1:8080"), "ws://127.0.0.1:8080", "local control plane uses WebSocket") assert_eq(ControlPlaneClient.websocket_url("match.example"), "", "unscoped URL cannot become a WebSocket URL") var unconfigured := ControlPlaneClient.new() assert_eq(unconfigured.connect_event_stream(), ERR_UNAUTHORIZED, "event stream requires an authenticated session") func test_ticket_normalization_preserves_payload_and_derives_expiry() -> void: var payload := {"ticket_id": "ticket-1", "state": "QUEUED", "expires_at": "2026-08-31T12:00:00Z"} var normalized := ControlPlaneClient.normalize_ticket(payload) assert_eq(normalized["ticket_id"], "ticket-1", "normalization preserves ticket identity") assert_true(normalized.has("expires_at_unix"), "RFC3339 expiry is available to the projection") assert_true(int(normalized["expires_at_unix"]) > 0, "expiry is converted to a positive epoch") assert_true(not payload.has("expires_at_unix"), "normalization does not mutate the HTTP payload") func test_ticket_normalization_derives_authoritative_enqueue_time() -> void: var normalized := ControlPlaneClient.normalize_ticket({"enqueued_at": "1970-01-01T00:16:40Z"}) assert_eq(int(normalized["enqueued_at_unix"]), 1000, "RFC3339 enqueue time is converted to epoch") assert_eq(ControlPlaneClient.normalize_ticket({"enqueued_at": "not-a-timestamp"})["enqueued_at_unix"], -1, "malformed enqueue time remains visibly invalid") assert_eq(ControlPlaneClient.normalize_ticket({"expires_at": 123})["expires_at_unix"], -1, "non-string expiry remains visibly invalid") func test_session_expiry_is_checked_at_the_boundary_and_fails_closed() -> void: assert_true(not ControlPlaneClient.is_session_expired("", 1000), "legacy sessions without an expiry remain compatible") assert_true(not ControlPlaneClient.is_session_expired("1970-01-01T00:16:40Z", 999), "session remains valid before expiry") assert_true(ControlPlaneClient.is_session_expired("1970-01-01T00:16:40Z", 1000), "session expires at the exact boundary") assert_true(ControlPlaneClient.is_session_expired("not-a-timestamp", 1000), "malformed non-empty expiry fails closed") assert_true(ControlPlaneClient.is_valid_rfc3339_timestamp("2026-08-31T12:00:00.123Z"), "fractional RFC3339 timestamp is accepted") assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-02-30T12:00:00Z"), "impossible calendar date is rejected") assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-13-01T12:00:00Z"), "impossible month is rejected") assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-08-31 12:00:00Z"), "space-separated timestamp is rejected") assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-08-31T12:00:00"), "timezone-less timestamp is rejected") var valid_session := {"player_id": "player_1234567890", "access_token": "session-id:opaque-token", "expires_at": "2099-08-31T12:00:00Z"} assert_true(ControlPlaneClient.is_valid_session_response(valid_session), "future session response is accepted") var missing_expiry := valid_session.duplicate() missing_expiry.erase("expires_at") assert_true(not ControlPlaneClient.is_valid_session_response(missing_expiry), "session without expiry is rejected") var malformed_expiry := valid_session.duplicate() malformed_expiry["expires_at"] = "tomorrow" assert_true(not ControlPlaneClient.is_valid_session_response(malformed_expiry), "malformed session expiry is rejected") var expired_session := valid_session.duplicate() expired_session["expires_at"] = "2000-01-01T00:00:00Z" assert_true(not ControlPlaneClient.is_valid_session_response(expired_session), "expired session response is rejected") func test_reconfiguration_discards_the_previous_session_expiry() -> void: var client := ControlPlaneClient.new() client.session_expires_at = "1970-01-01T00:00:01Z" assert_true(client.configure("https://match.example", "new-session:opaque-token"), "new session configures successfully") assert_eq(client.session_expires_at, "", "new credentials do not inherit the old expiry") func test_websocket_event_validation_requires_contract_specific_fields() -> void: var envelope := {"event": "state_changed", "revision": 1, "resource_id": "ticket_123456789", "occurred_at": "2026-08-31T12:00:00Z", "state": "QUEUED"} assert_true(ControlPlaneClient._valid_websocket_event(envelope), "valid state event is accepted") var accepted := envelope.duplicate() accepted["state"] = "ACCEPTED" assert_true(ControlPlaneClient._valid_websocket_event(accepted), "authoritative accepted queue event is accepted") for phase in ["ASSIGNED", "RESULT_PENDING", "COMPLETED"]: var lifecycle := envelope.duplicate() lifecycle["state"] = phase assert_true(ControlPlaneClient._valid_websocket_event(lifecycle), "post-match queue event is accepted: " + phase) var bad_state := envelope.duplicate() bad_state["state"] = "SECRET" assert_true(not ControlPlaneClient._valid_websocket_event(bad_state), "unknown state event is rejected") var assignment := {"event": "assignment_changed", "revision": 0, "resource_id": "match_1234567890", "occurred_at": "2026-08-31T12:00:00Z", "match_id": "match_1234567890", "server_id": "server_123456789"} assert_true(ControlPlaneClient._valid_websocket_event(assignment), "complete assignment event is accepted") var short_assignment_id := assignment.duplicate() short_assignment_id["server_id"] = "server-1" assert_true(not ControlPlaneClient._valid_websocket_event(short_assignment_id), "short assignment server id is rejected") assignment.erase("server_id") assert_true(not ControlPlaneClient._valid_websocket_event(assignment), "incomplete assignment event is rejected") var fractional := envelope.duplicate() fractional["revision"] = 1.5 assert_true(not ControlPlaneClient._valid_websocket_event(fractional), "fractional event revision is rejected") var negative := envelope.duplicate() negative["revision"] = -1 assert_true(not ControlPlaneClient._valid_websocket_event(negative), "negative event revision is rejected") var malformed_time := envelope.duplicate() malformed_time["occurred_at"] = "yesterday" assert_true(not ControlPlaneClient._valid_websocket_event(malformed_time), "malformed event timestamp is rejected") var short_resource := envelope.duplicate() short_resource["resource_id"] = "short" assert_true(not ControlPlaneClient._valid_websocket_event(short_resource), "short resource identifier is rejected") var unsafe_resource := envelope.duplicate() unsafe_resource["resource_id"] = "ticket_123456789/secret" assert_true(not ControlPlaneClient._valid_websocket_event(unsafe_resource), "resource identifier with separators is rejected") var match_state := {"event": "state_changed", "revision": 4, "resource_id": "match_1234567890", "occurred_at": "2026-08-31T12:00:00Z", "state": "ASSIGNMENT_READY", "match_id": "match_1234567890"} assert_true(ControlPlaneClient._valid_websocket_event(match_state), "match-scoped lifecycle event is accepted") match_state["match_id"] = "different_match_123" assert_true(not ControlPlaneClient._valid_websocket_event(match_state), "match lifecycle identity must equal its resource identity") func test_match_assignment_ready_event_recovers_ticket_and_schedules_assignment_fetch() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.state.begin_queue("ticket_assignment_1", "casual"), "queue setup succeeds") client._operation = "queue_heartbeat" var event := {"event": "state_changed", "revision": 4, "resource_id": "match_assignment_1", "occurred_at": "2026-08-31T12:00:00Z", "state": "ASSIGNMENT_READY", "match_id": "match_assignment_1"} client._handle_websocket_packet(JSON.stringify(event).to_utf8_buffer()) assert_eq(client._pending_resync_resource_id, "ticket_assignment_1", "match event requests authoritative ticket recovery") assert_eq(client._pending_assignment_match_id, "match_assignment_1", "assignment lookup no longer depends on a prior assignment GET") assert_eq(client.state.ticket_id, "ticket_assignment_1", "match resource is never projected as a ticket identity") client.free() func test_recovered_assignment_ready_ticket_schedules_fetch_after_missed_revisions() -> void: var client := ControlPlaneClient.new() client._ready() client.player_id = "player_1234567890" client.state.begin_queue("ticket_assignment_1", "casual") assert_true(client.state.apply_ticket_update({"ticket_id": "ticket_assignment_1", "revision": 1, "state": "PROPOSED", "playlist": "casual"}), "proposal setup applies") client._operation = "queue_recover" var recovered := {"ticket_id": "ticket_assignment_1", "player_id": "player_1234567890", "match_id": "match_assignment_1", "playlist": "casual", "state": "ASSIGNMENT_READY", "revision": 5, "enqueued_at": "2026-08-31T12:00:00Z", "expires_at": "2026-08-31T12:01:00Z"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify(recovered).to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.ASSIGNMENT_READY, "REST recovery applies a forward authoritative snapshot") assert_eq(client._pending_assignment_match_id, "match_assignment_1", "recovered snapshot supplies the assignment lookup key") client.free() func test_websocket_reconnect_defers_recovery_while_http_mutation_is_in_flight() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.state.begin_queue("ticket-reconnect", "casual"), "queue setup succeeds") client._operation = "queue_heartbeat" client._set_websocket_status("CONNECTED") assert_eq(client._pending_resync_resource_id, "ticket-reconnect", "reconnect recovery is retained until the mutation completes") client.free() func test_transient_rest_recovery_failure_does_not_end_matchmaking() -> void: var client := ControlPlaneClient.new() client._ready() client.state.begin_queue("ticket_recovery_123", "casual") client._operation = "queue_recover" client._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 0, PackedStringArray(), PackedByteArray()) assert_eq(client.state.phase, MatchmakingState.QUEUED, "network failure during recovery keeps the active search") assert_true(client.state.message.contains("retrying"), "recovery failure remains visible and retryable") client._operation = "proposal_recover" client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), "[]".to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.QUEUED, "malformed transient recovery response does not become terminal") client.free() func test_resync_of_terminal_proposal_recovers_the_ticket() -> void: assert_eq(ControlPlaneClient.resync_target("proposal-terminal-resync", "ticket-terminal-resync", "proposal-terminal-resync", false), "ticket-terminal-resync", "terminal proposal resync targets the requeued ticket") assert_eq(ControlPlaneClient.resync_target("proposal-terminal-resync", "ticket-terminal-resync", "proposal-terminal-resync", true), "proposal-terminal-resync", "open proposal resync retains the proposal target") func test_retryable_mutation_policy_only_retries_safe_failures() -> void: assert_true(ControlPlaneClient.is_retryable_mutation_response(0), "transport failure is retryable") assert_true(ControlPlaneClient.is_retryable_mutation_response(408), "request timeout is retryable") assert_true(ControlPlaneClient.is_retryable_mutation_response(429), "rate limit is retryable") assert_true(ControlPlaneClient.is_retryable_mutation_response(503), "server failure is retryable") assert_true(not ControlPlaneClient.is_retryable_mutation_response(401), "authentication failure is not blindly replayed") assert_true(not ControlPlaneClient.is_retryable_mutation_response(409), "revision/idempotency conflict is not blindly replayed") # multiplayer-next.md 8.43 named "duplicate-action recovery beyond proposals" # and "regional outage retry UI" as remaining. Both mechanisms (can_retry_last_mutation / # retry_last_mutation, and matchmaking.gd's queue button falling back to them) # already existed in the client, but had no test coverage proving the # generic (non-proposal) mutation path actually recovers end to end -- only # is_retryable_mutation_response's pure classification was covered above. func test_generic_mutation_retry_recovers_after_a_transient_failure() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "client configures") assert_true(client.state.begin_queue("ticket-retry-generic", "casual"), "queue setup succeeds") # Simulate what _start_request itself would already have recorded before # a real network call was in flight, the same way the pre-existing # conflict-handler tests above set _operation directly. client._operation = "queue_heartbeat" client._last_mutation = {"operation": "queue_heartbeat", "method": HTTPClient.METHOD_POST, "path": "/v1/queue/ticket-retry-generic/heartbeat", "payload": {"revision": 0}, "key": "heartbeat-retry-key-123456", "expected_revision": 0} assert_true(not client.can_retry_last_mutation(), "a mutation still in flight is never offered as retryable") # A regional outage: the transport itself failed rather than returning a # decoded HTTP status -- exactly the "regional outage retry" case. This # transition is the actual previously-uncovered boundary: nothing tested # that a generic (non-proposal) mutation ever becomes retryable at all, # only is_retryable_mutation_response's pure classification above. # retry_last_mutation's own dispatch is not exercised here: it reaches # HTTPRequest.request(), which needs the node inside a live SceneTree, # and test_runner.tscn runs every test method from within its own # _ready() while the tree is still being built, so that is out of reach # for this harness -- the "not offered at all" boundary below covers the # part of retry_last_mutation this environment can exercise safely. client._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 0, PackedStringArray(), PackedByteArray()) assert_true(client.can_retry_last_mutation(), "a transport failure on a non-proposal mutation is offered as retryable") client.free() func test_generic_mutation_retry_is_not_offered_for_unsafe_failures() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "client configures") assert_true(client.state.begin_queue("ticket-retry-unsafe", "casual"), "queue setup succeeds") client._operation = "queue_cancel" client._last_mutation = {"operation": "queue_cancel", "method": HTTPClient.METHOD_POST, "path": "/v1/queue/ticket-retry-unsafe/cancel", "payload": {}, "key": "cancel-retry-key-123456", "expected_revision": 0} # A 409 is a revision/idempotency conflict, not a transient failure -- # should_recover_queue_after_conflict owns recovering it instead, and a # blind resend would replay a mutation whose precondition already failed. client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 409, PackedStringArray(), JSON.stringify({"error": "revision conflict"}).to_utf8_buffer()) assert_true(not client.can_retry_last_mutation(), "a conflict response is never offered as a blind retry") assert_eq(client.retry_last_mutation(), ERR_INVALID_DATA, "retrying when not offered fails closed rather than resending a stale mutation") client.free() # connect_to_assignment() already existed, fully validated, with its own # assignment_connection_started/assignment_connection_failed signals -- but # nothing anywhere in the client ever called it. A player reaching the # ASSIGNED phase (server confirms the complete roster) with a fetched, fresh # assignment would simply sit on "Your match server is ready" forever, # because the transport was never actually started. This is the wiring fix, # not just new test coverage for existing behavior. func test_client_starts_the_transport_once_the_ticket_reaches_assigned() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "client configures") client.player_id = "player_1234567890" assert_true(client.state.begin_queue("ticket-connect-ready", "casual"), "queue setup succeeds") # The assignment fetch (triggered independently, earlier, by # ASSIGNMENT_READY) has already completed by the time ASSIGNED arrives -- # the common case. client._operation = "assignment" var assignment_payload := {"match_id": "match_connect_1234567890", "server_id": "server_connect_1234567890", "player_id": "player_1234567890", "slot": 0, "expires_at": "2099-08-31T12:00:00Z", "protocol_version": 1, "transport": "enet", "endpoint": "127.0.0.1:65500", "join_authorisation": "opaque-join-token"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify(assignment_payload).to_utf8_buffer()) assert_true(client.assignment.available, "assignment fetch applies") var connect_started := [false] var connect_failed := [false] client.assignment_connection_started.connect(func(_a): connect_started[0] = true) client.assignment_connection_failed.connect(func(_d): connect_failed[0] = true) client._operation = "queue_recover" var ticket_payload := {"ticket_id": "ticket-connect-ready", "player_id": "player_1234567890", "playlist": "casual", "revision": 5, "state": "ASSIGNED", "match_id": "match_connect_1234567890", "enqueued_at": "2026-08-31T12:00:00Z", "expires_at": "2099-08-31T12:00:00Z"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify(ticket_payload).to_utf8_buffer()) # connect_to_assignment() itself calls state.mark_connecting() as part of a # successful attempt, so by the time control returns here phase has # already advanced past ASSIGNED to CONNECTING -- that advancement is # itself the proof the connect was actually attempted. assert_eq(client.state.phase, MatchmakingState.CONNECTING, "reaching ASSIGNED with a ready assignment actually started the transport, rather than sitting idle") assert_true(connect_started[0] or connect_failed[0], "connect_to_assignment's own signal fired") assert_true(client._pending_connect_match_id.is_empty(), "an attempted connect is not left pending") # A duplicate/replayed ASSIGNED event for the same match (e.g. an # at-least-once outbox redelivery) must not fire a second connection # attempt. Called directly against the guarded function rather than # through another full _on_request_completed round-trip: phase has # already moved on to CONNECTING, so both of _connect_when_assigned's own # guards (phase != ASSIGNED, and the _connect_attempted_match_id match) # now independently refuse a second attempt for this match. connect_started[0] = false connect_failed[0] = false client._connect_when_assigned("match_connect_1234567890") assert_true(not connect_started[0] and not connect_failed[0], "a duplicate connect attempt for an already-attempted match is not reattempted") NetworkManager.shutdown() client.free() func test_client_defers_the_connect_until_the_assignment_fetch_completes() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "client configures") assert_true(client.state.begin_queue("ticket-connect-deferred", "casual"), "queue setup succeeds") var connect_started := [false] var connect_failed := [false] client.assignment_connection_started.connect(func(_a): connect_started[0] = true) client.assignment_connection_failed.connect(func(_d): connect_failed[0] = true) # ASSIGNED arrives before the assignment fetch (triggered earlier by # ASSIGNMENT_READY) has actually completed -- the ordering the deferred # path exists for. client.assignment is still the default, unavailable one. client._operation = "queue_recover" var ticket_payload := {"ticket_id": "ticket-connect-deferred", "player_id": "player_1234567890", "playlist": "casual", "revision": 5, "state": "ASSIGNED", "match_id": "match_deferred_1234567890", "enqueued_at": "2026-08-31T12:00:00Z", "expires_at": "2099-08-31T12:00:00Z"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify(ticket_payload).to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.ASSIGNED, "ticket state machine still reaches ASSIGNED") assert_eq(client._pending_connect_match_id, "match_deferred_1234567890", "the connect attempt is deferred until the assignment is actually available") assert_true(not connect_started[0] and not connect_failed[0], "no connection attempt is made before the assignment is ready -- nothing to connect to yet") client.free() # Covers §8.43's "failed reconnect UX": connect_to_assignment()'s own # synchronous failures previously only emitted assignment_connection_failed, # a signal nothing in the client listened to -- state.phase stayed stuck at # ASSIGNED, the UI kept showing "Your match server is ready" forever, and # there was no way back to a fresh search. func test_synchronous_assignment_connection_failure_surfaces_as_a_failed_search() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.state.begin_queue("ticket-connect-unavailable", "casual"), "queue setup succeeds") # client.assignment is still the default, unavailable one. var err := client.connect_to_assignment() assert_eq(err, ERR_UNAUTHORIZED, "connect fails closed when the assignment isn't ready") assert_eq(client.state.phase, MatchmakingState.FAILED, "the failure is surfaced as a failed search rather than leaving the UI stuck at ASSIGNED") assert_true(client.state.message.to_lower().contains("unavailable") or client.state.message.to_lower().contains("expired"), "the failure detail is retained: %s" % client.state.message) client.free() # The likelier real-world failure than the synchronous one above: # NetworkManager.join() returns OK immediately (the attempt started), but the # actual ENet handshake fails asynchronously later -- unreachable server, # refused connection, ENet's own ~5s connect timeout. This is exactly the gap # main_menu.gd's own _on_connection_failed exists to cover for the # direct-join flow; nothing covered it for a matchmaking-driven connect. func test_async_network_connection_failure_after_assignment_ready_surfaces_as_a_failed_search() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "client configures") client.player_id = "player_1234567890" assert_true(client.state.begin_queue("ticket-connect-asyncfail", "casual"), "queue setup succeeds") client._operation = "assignment" var assignment_payload := {"match_id": "match_asyncfail_1234567890", "server_id": "server_asyncfail_1234567890", "player_id": "player_1234567890", "slot": 0, "expires_at": "2099-08-31T12:00:00Z", "protocol_version": 1, "transport": "enet", "endpoint": "127.0.0.1:65501", "join_authorisation": "opaque-join-token"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify(assignment_payload).to_utf8_buffer()) client._operation = "queue_recover" var ticket_payload := {"ticket_id": "ticket-connect-asyncfail", "player_id": "player_1234567890", "playlist": "casual", "revision": 5, "state": "ASSIGNED", "match_id": "match_asyncfail_1234567890", "enqueued_at": "2026-08-31T12:00:00Z", "expires_at": "2099-08-31T12:00:00Z"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify(ticket_payload).to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.CONNECTING, "the transport attempt started") NetworkManager.connection_failed.emit() assert_eq(client.state.phase, MatchmakingState.FAILED, "the async handshake failure is surfaced rather than leaving CONNECTING stuck forever") NetworkManager.shutdown() client.free() func test_network_connection_failure_is_ignored_outside_a_matchmaking_driven_connect() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.state.begin_queue("ticket-unrelated-failure", "casual"), "queue setup succeeds") # state.phase is QUEUED, not CONNECTING -- this connection_failed belongs # to something else (e.g. main_menu.gd's own direct-join flow) and must # not be misattributed to matchmaking. NetworkManager.connection_failed.emit() assert_eq(client.state.phase, MatchmakingState.QUEUED, "an unrelated connection_failed does not fail an active queue search") client.free() func test_rest_resource_identifiers_use_the_opaque_contract_shape() -> void: assert_true(ControlPlaneClient.is_valid_resource_id("ticket_1234567890"), "contract-sized resource id is accepted") assert_true(not ControlPlaneClient.is_valid_resource_id("ticket-1"), "short resource id is rejected") assert_true(not ControlPlaneClient.is_valid_resource_id("ticket_1234567890/path"), "path separator is rejected") func test_queue_revision_conflicts_schedule_authoritative_recovery() -> void: assert_true(ControlPlaneClient.should_recover_queue_after_conflict("queue_heartbeat", 409, "ticket-1"), "stale heartbeat recovers the queue ticket") assert_true(ControlPlaneClient.should_recover_queue_after_conflict("queue_cancel", 409, "ticket-1"), "stale cancellation recovers the queue ticket") assert_true(not ControlPlaneClient.should_recover_queue_after_conflict("queue_create", 409, "ticket-1"), "create conflict uses its own idempotency path") assert_true(not ControlPlaneClient.should_recover_queue_after_conflict("queue_heartbeat", 503, "ticket-1"), "transient outage remains retryable instead of being treated as a revision conflict") assert_true(not ControlPlaneClient.should_recover_queue_after_conflict("queue_cancel", 409, ""), "missing ticket cannot trigger recovery") func test_queue_conflict_response_handler_defers_ticket_recovery() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.state.begin_queue("ticket-handler", "casual"), "queue setup succeeds") client._operation = "queue_heartbeat" client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 409, PackedStringArray(), JSON.stringify({"error": "revision conflict"}).to_utf8_buffer()) assert_eq(client._pending_resync_resource_id, "ticket-handler", "heartbeat conflict queues ticket recovery") client._operation = "queue_cancel" client._pending_resync_resource_id = "" client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 409, PackedStringArray(), JSON.stringify({"error": "revision conflict"}).to_utf8_buffer()) assert_eq(client._pending_resync_resource_id, "ticket-handler", "cancel conflict queues ticket recovery") client.free() # Covers §8.43's "version-mismatch-specific client messaging": a 426 Upgrade # Required on queue_create (the server-side floor added alongside this test) # must surface a distinct, actionable message rather than the server's raw # generic error string, and must not offer a futile "Retry Search" -- the # same client build will fail again identically every time. func test_outdated_client_receives_a_distinct_message_and_no_retry_offer() -> void: var client := ControlPlaneClient.new() client._ready() assert_true(client.configure("https://match.example", "session-id:opaque-token"), "client configures") client._operation = "queue_create" client._last_queue_create = {"ticket_id": "ticket-outdated", "playlist": "casual", "client_build": "build-1", "protocol_version": 4, "key": "outdated-key-123456"} client._on_request_completed(HTTPRequest.RESULT_SUCCESS, HTTPClient.RESPONSE_UPGRADE_REQUIRED, PackedStringArray(), JSON.stringify({"error": "client_outdated"}).to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.FAILED, "outdated client fails the search") assert_true(client.state.message.to_lower().contains("update"), "message tells the player to update rather than repeating the raw server error: %s" % client.state.message) assert_true(not client.can_retry_queue_create(), "retrying with the same outdated client build is never offered") client.free() func test_rest_responses_reject_malformed_resource_identifiers() -> void: var client := ControlPlaneClient.new() client._ready() client._operation = "queue_recover" client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify({"ticket_id": "short", "playlist": "casual", "revision": 0, "state": "QUEUED"}).to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.FAILED, "malformed queue response is not projected") client._operation = "proposal_recover" client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), JSON.stringify({"proposal_id": "proposal/unsafe", "revision": 0, "state": "OPEN"}).to_utf8_buffer()) assert_eq(client.state.phase, MatchmakingState.FAILED, "malformed proposal response is not projected") client.free() func test_queue_response_requires_the_complete_contract_shape() -> void: var valid := {"ticket_id": "ticket_1234567890", "player_id": "player_1234567890", "playlist": "casual", "state": "QUEUED", "revision": 0, "enqueued_at": "2026-08-31T12:00:00Z", "expires_at": "2026-08-31T12:01:00Z"} assert_true(ControlPlaneClient._valid_queue_response(valid), "complete queue response is accepted") var missing_expiry := valid.duplicate() missing_expiry.erase("expires_at") assert_true(not ControlPlaneClient._valid_queue_response(missing_expiry), "queue response without expiry is rejected") var fractional_revision := valid.duplicate() fractional_revision["revision"] = 1.5 assert_true(not ControlPlaneClient._valid_queue_response(fractional_revision), "fractional queue revision is rejected") var malformed_player := valid.duplicate() malformed_player["player_id"] = "player/unsafe" assert_true(not ControlPlaneClient._valid_queue_response(malformed_player), "unsafe queue player id is rejected") var assigned := valid.duplicate() assigned["state"] = "ASSIGNMENT_READY" assigned["match_id"] = "match_1234567890" assert_true(ControlPlaneClient._valid_queue_response(assigned), "recovered assignment-ready ticket carries its match lookup identity") var premature_match := valid.duplicate() premature_match["match_id"] = "match_1234567890" assert_true(not ControlPlaneClient._valid_queue_response(premature_match), "pre-match ticket cannot smuggle a match identity") assigned["match_id"] = "match/unsafe" assert_true(not ControlPlaneClient._valid_queue_response(assigned), "unsafe recovered match identity is rejected") var proposed := valid.duplicate() proposed["state"] = "PROPOSED" proposed["proposal_id"] = "proposal_12345678" assert_true(ControlPlaneClient._valid_queue_response(proposed), "recovered proposed ticket carries its proposal lookup identity") var client := ControlPlaneClient.new() client._ready() client.state.begin_queue("ticket_1234567890", "casual") client._queue_proposal_if_ready(proposed) assert_eq(client._pending_proposal_id, "proposal_12345678", "recovered proposal is queued for authoritative fetch") assert_eq(client.state.proposal_id, "proposal_12345678", "recovered proposal identity becomes the active projection") client.free() func test_proposal_response_requires_structured_unique_participants() -> void: var base := {"proposal_id": "proposal_1234567890", "expires_at": "2099-08-31T12:00:00Z", "participants": [ {"player_id": "player_1234567890", "response": "PENDING", "team": 0, "slot": 0}, {"player_id": "player_1234567891", "response": "PENDING", "team": 1, "slot": 3} ]} assert_true(ControlPlaneClient._valid_proposal_response(base), "structured proposal participants are accepted") var duplicate := base.duplicate(true) duplicate["participants"][1]["player_id"] = "player_1234567890" assert_true(not ControlPlaneClient._valid_proposal_response(duplicate), "duplicate participant identity is rejected") var fractional_slot := base.duplicate(true) fractional_slot["participants"][0]["slot"] = 0.5 assert_true(not ControlPlaneClient._valid_proposal_response(fractional_slot), "fractional participant slot is rejected") var malformed_expiry := base.duplicate(true) malformed_expiry["expires_at"] = "tomorrow" assert_true(not ControlPlaneClient._valid_proposal_response(malformed_expiry), "malformed proposal expiry is rejected") var missing_expiry := base.duplicate(true) missing_expiry.erase("expires_at") assert_true(not ControlPlaneClient._valid_proposal_response(missing_expiry), "missing proposal expiry is rejected") assert_true(int(ControlPlaneClient.normalize_proposal(base)["expires_at_unix"]) > 0, "proposal expiry is normalized") func test_assignment_endpoint_split_never_accepts_url_or_bad_port() -> void: var endpoint := ControlPlaneClient._split_assignment_endpoint("127.0.0.1:31001") assert_eq(endpoint["host"], "127.0.0.1", "assignment host is separated from the port") assert_eq(endpoint["port"], 31001, "assignment port is parsed as an integer") for unsafe in ["127.0.0.1", "127.0.0.1:0", "127.0.0.1:65536", "127.0.0.1:31001/path", "https://127.0.0.1:31001"]: assert_true(ControlPlaneClient._split_assignment_endpoint(unsafe).is_empty(), "unsafe endpoint is rejected: %s" % unsafe) func test_ranked_profile_is_backend_display_data_and_rejects_unsafe_values() -> void: var profile := RankedProfileState.new() assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "GOLD", "provisional": true, "season_id": "season_1234567890"}), "valid profile applies") assert_eq(profile.display_text(), "Provisional · 3 ranked games", "provisional status overrides tier presentation") assert_true(not profile.apply({"rating": -1.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "GOLD", "provisional": false}), "negative rating is rejected") assert_true(not profile.available, "unsafe response is not displayed") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "", "provisional": false}), "empty tier is rejected") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "MASTER", "provisional": false}), "unknown tier is rejected") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "GOLD", "provisional": "false"}), "string boolean is rejected") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3.5, "tier": "GOLD", "provisional": false}), "fractional ranked games is rejected") func test_ranked_profile_projects_and_bounds_season_countdown() -> void: var profile := RankedProfileState.new() assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": "season_1234567890", "season_ends_at": "1970-01-03T00:00:00Z"}), "season end applies") assert_true(profile.display_text(1000).contains("Season ends in 2d"), "countdown rounds up remaining season time") assert_true(profile.display_text(300000).contains("Season ends in 0d"), "expired season countdown is clamped") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_ends_at": "not-a-timestamp"}), "malformed season expiry is rejected") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_ends_at": 123}), "non-string season expiry is rejected") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": "short"}), "short season identifier is rejected") assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": 123}), "non-string season identifier is rejected") # The client had no probe support at all, so even with the backend wired a real # player could never acquire the RTT evidence the matcher requires. func test_probe_region_validation_rejects_unknown_regions() -> void: assert_true(ControlPlaneClient.is_valid_probe_region("EU"), "EU is a placement region") assert_true(ControlPlaneClient.is_valid_probe_region("NA"), "NA is a placement region") for region in ["", "eu", "APAC", "EU/NA", "../EU"]: assert_true(not ControlPlaneClient.is_valid_probe_region(region), "rejects %s" % region) func test_probe_requests_require_a_session() -> void: var client = ControlPlaneClient.new() client.base_url = "http://127.0.0.1:8080" client.access_token = "" assert_eq(client.request_probe_challenge("EU"), ERR_UNAUTHORIZED, "probing without a session is refused") assert_eq(client.submit_probe_answer("EU", "bm9uY2U=", "bG9j"), ERR_UNAUTHORIZED, "answering without a session is refused") client.free() func test_probe_answer_rejects_empty_nonce_or_location() -> void: var client = ControlPlaneClient.new() client.base_url = "http://127.0.0.1:8080" client.access_token = "session-1234567890:token-1234567890" assert_eq(client.submit_probe_answer("EU", "", "bG9j"), ERR_INVALID_PARAMETER, "an empty nonce is refused") assert_eq(client.submit_probe_answer("EU", "bm9uY2U=", ""), ERR_INVALID_PARAMETER, "an empty location is refused") assert_eq(client.request_probe_challenge("APAC"), ERR_INVALID_PARAMETER, "an unknown region is refused") client.free() func test_opaque_location_payload_is_never_empty() -> void: # The backend rejects an empty opaque location, and without a Steam runtime # there is nothing real to report -- but the RTT the backend measures is # what actually drives placement, so the probe must still be answerable. var payload := ControlPlaneClient.opaque_location_payload() assert_true(not payload.is_empty(), "a probe answer always carries a location blob") assert_true(not Marshalls.base64_to_raw(payload).is_empty(), "the location blob is valid base64") func test_probe_challenge_response_without_a_nonce_is_a_failure() -> void: var client = ControlPlaneClient.new() client.base_url = "http://127.0.0.1:8080" client.access_token = "session-1234567890:token-1234567890" var failures: Array = [] client.request_failed.connect(func(operation: String, _code: int, detail: String): failures.append([operation, detail])) client._operation = "probe_challenge_EU" client._on_request_completed(HTTPRequest.RESULT_SUCCESS, 201, PackedStringArray(), JSON.stringify({"region": "EU"}).to_utf8_buffer()) assert_eq(failures.size(), 1, "a challenge with no nonce is reported as a failure") client.free() # The game started with an empty token against a loopback default and no # production code ever called configure() or login_steam(), so every # matchmaking request failed ERR_UNAUTHORIZED before reaching the network. func test_has_session_reflects_token_and_expiry() -> void: var client = ControlPlaneClient.new() assert_true(not client.has_session(), "a fresh client has no session") client.access_token = "session-1234567890:token-1234567890" client.session_expires_at = "2099-01-01T00:00:00Z" assert_true(client.has_session(), "a valid unexpired token is a session") client.session_expires_at = "2000-01-01T00:00:00Z" assert_true(not client.has_session(), "an expired token is not a session") client.free() func test_configured_base_url_falls_back_to_the_development_default() -> void: # Release builds set COSMIC_CLASH_CONTROL_PLANE_URL; without it the # loopback default keeps local development working. var resolved := ControlPlaneClient.configured_base_url() assert_true(ControlPlaneClient.is_valid_base_url(resolved), "the resolved endpoint is always usable") if OS.get_environment(ControlPlaneClient.BASE_URL_ENV).strip_edges().is_empty(): assert_eq(resolved, ControlPlaneClient.DEFAULT_BASE_URL, "falls back to the development default")