mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): validate ticket playlists
This commit is contained in:
@@ -51,6 +51,8 @@ func begin_queue(new_ticket_id: String, new_playlist: String) -> bool:
|
||||
func apply_ticket_update(update: Dictionary) -> bool:
|
||||
if not _has_string(update, "ticket_id") or not update.has("revision") or not _valid_revision(update["revision"]) or not update.has("state"):
|
||||
return _request_resync(self.ticket_id)
|
||||
if update.has("playlist") and not _valid_playlist(String(update["playlist"])):
|
||||
return _request_resync(self.ticket_id)
|
||||
if ticket_id.is_empty() or String(update["ticket_id"]) != ticket_id:
|
||||
return _request_resync(self.ticket_id)
|
||||
var incoming_revision := int(update["revision"])
|
||||
@@ -299,3 +301,7 @@ func _valid_revision(value: Variant) -> bool:
|
||||
if value is float:
|
||||
return is_finite(float(value)) and float(value) >= 0.0 and float(value) == floor(float(value))
|
||||
return false
|
||||
|
||||
|
||||
func _valid_playlist(value: String) -> bool:
|
||||
return value == "casual" or value == "ranked"
|
||||
|
||||
@@ -82,6 +82,14 @@ func test_ticket_and_proposal_revisions_must_be_nonnegative_integers() -> void:
|
||||
assert_true(not state.apply_proposal_update({"proposal_id": "proposal-revision", "revision": -1, "state": "OPEN"}), "negative proposal revision is rejected")
|
||||
|
||||
|
||||
func test_ticket_update_rejects_invalid_playlist_without_mutation() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
state.begin_queue("ticket-playlist", "casual")
|
||||
assert_true(not state.apply_ticket_update({"ticket_id": "ticket-playlist", "revision": 1, "state": "PROPOSED", "playlist": "admin"}), "unknown playlist is rejected")
|
||||
assert_eq(state.phase, MatchmakingState.QUEUED, "invalid playlist cannot change phase")
|
||||
assert_eq(state.playlist, "casual", "invalid playlist cannot change playlist")
|
||||
|
||||
|
||||
func test_proposal_terminal_states_are_visible_and_not_cancellable() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
state.begin_queue("ticket-1", "casual")
|
||||
|
||||
@@ -1546,3 +1546,5 @@ Reconnect recovery now treats `COMPLETED` as terminal, avoiding a needless queue
|
||||
Ticket, proposal, and WebSocket revisions now fail closed unless they are finite, non-negative integers; fractional values are no longer silently truncated into valid revisions. Adversarial client tests cover fractional and negative inputs.
|
||||
|
||||
Proposal updates now enforce the documented `OPEN → ACCEPTED/DECLINED/EXPIRED/CANCELLED` graph, including rejecting higher-revision reopen/accept attempts after terminal decisions while preserving same-state duplicates. Adversarial proposal-transition tests cover accepted and declined terminal paths.
|
||||
|
||||
Ticket projections now validate playlist metadata on every update, rejecting unknown values before either phase or playlist state can mutate. An adversarial higher-revision update test covers this boundary.
|
||||
|
||||
Reference in New Issue
Block a user