mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat(multiplayer): publish ranked profile contract
This commit is contained in:
@@ -1530,3 +1530,5 @@ The ENet gate now auto-detects `/Applications/Godot.app/Contents/MacOS/Godot` wh
|
||||
The client matchmaking projection now preserves the server's `enqueued_at` timestamp through normalization, snapshots, and recovery, and uses it for the displayed queue wait when available. This prevents a client restart or delayed response from resetting the user's perceived wait to local process uptime; a local timer remains the fallback when older responses omit the timestamp. Godot state and normalization tests cover the projection and restore path.
|
||||
|
||||
The ranked profile projection now also carries the active season's authoritative end timestamp from PostgreSQL through the API and Godot client. Ranked matchmaking displays a bounded days-remaining countdown, while providers without an active season remain compatible and omit the countdown.
|
||||
|
||||
The versioned OpenAPI contract now declares the implemented `/profile/ranked` surface and its server-authoritative ranked profile schema, including optional active-season metadata. Contract tests reject omission of this operation, extra response fields, and credential leakage.
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
"/profile": {
|
||||
"get": {"operationId": "getProfile", "responses": {"200": {"description": "Profile", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Profile"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}}}
|
||||
},
|
||||
"/profile/ranked": {
|
||||
"get": {"operationId": "getRankedProfile", "responses": {"200": {"description": "Authoritative ranked profile", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RankedProfile"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}, "503": {"$ref": "#/components/responses/Unavailable"}}}
|
||||
},
|
||||
"/queue/tickets": {
|
||||
"post": {
|
||||
"operationId": "createQueueTicket",
|
||||
@@ -73,6 +76,7 @@
|
||||
"Conflict": {"description": "Revision or idempotency conflict"},
|
||||
"Invalid": {"description": "Invalid state or schema"},
|
||||
"NotFound": {"description": "Resource not found"},
|
||||
"Unavailable": {"description": "Authoritative profile temporarily unavailable"},
|
||||
"Expired": {"description": "Resource expired"}
|
||||
},
|
||||
"schemas": {
|
||||
@@ -80,6 +84,7 @@
|
||||
"SteamLogin": {"type": "object", "required": ["web_api_ticket"], "additionalProperties": false, "properties": {"web_api_ticket": {"type": "string", "minLength": 1, "maxLength": 4096}}},
|
||||
"Session": {"type": "object", "required": ["player_id", "expires_at", "access_token"], "additionalProperties": false, "properties": {"player_id": {"$ref": "#/components/schemas/OpaqueId"}, "expires_at": {"type": "string", "format": "date-time"}, "access_token": {"type": "string"}}},
|
||||
"Profile": {"type": "object", "required": ["player_id", "rating", "rd", "provisional"], "additionalProperties": false, "properties": {"player_id": {"$ref": "#/components/schemas/OpaqueId"}, "rating": {"type": "number"}, "rd": {"type": "number"}, "provisional": {"type": "boolean"}}},
|
||||
"RankedProfile": {"type": "object", "required": ["rating", "rd", "volatility", "ranked_games", "tier", "provisional"], "additionalProperties": false, "properties": {"rating": {"type": "number", "minimum": 0}, "rd": {"type": "number", "minimum": 0}, "volatility": {"type": "number", "minimum": 0}, "ranked_games": {"type": "integer", "minimum": 0}, "tier": {"type": "string", "enum": ["PROVISIONAL", "BRONZE", "SILVER", "GOLD", "PLATINUM", "DIAMOND"]}, "provisional": {"type": "boolean"}, "season_id": {"$ref": "#/components/schemas/OpaqueId"}, "season_ends_at": {"type": "string", "format": "date-time"}}},
|
||||
"QueueCreate": {"type": "object", "required": ["playlist", "client_build", "protocol_version"], "additionalProperties": false, "properties": {"playlist": {"type": "string", "enum": ["casual", "ranked"]}, "client_build": {"type": "string", "minLength": 1, "maxLength": 128}, "protocol_version": {"type": "integer", "minimum": 1}}},
|
||||
"QueueTicket": {"type": "object", "required": ["ticket_id", "player_id", "playlist", "state", "revision", "enqueued_at", "expires_at"], "additionalProperties": false, "properties": {"ticket_id": {"$ref": "#/components/schemas/OpaqueId"}, "player_id": {"$ref": "#/components/schemas/OpaqueId"}, "playlist": {"type": "string", "enum": ["casual", "ranked"]}, "state": {"$ref": "#/components/schemas/QueueState"}, "revision": {"type": "integer", "minimum": 0}, "enqueued_at": {"type": "string", "format": "date-time"}, "expires_at": {"type": "string", "format": "date-time"}}},
|
||||
"QueueState": {"type": "string", "enum": ["QUEUED", "PROPOSED", "ACCEPTED", "ALLOCATING", "PROCESS_READY", "ASSIGNMENT_READY", "ASSIGNED", "CONNECTING", "LIVE", "RESULT_PENDING", "COMPLETED", "CANCELLED", "EXPIRED", "FAILED"]},
|
||||
|
||||
@@ -27,9 +27,16 @@ class ContractTest(unittest.TestCase):
|
||||
"createSteamSession", "getProfile", "createQueueTicket",
|
||||
"heartbeatQueueTicket", "cancelQueueTicket", "acceptProposal",
|
||||
"declineProposal", "getAssignment", "registerServer",
|
||||
"submitMatchResult",
|
||||
"submitMatchResult", "getRankedProfile",
|
||||
} <= operations)
|
||||
|
||||
def test_ranked_profile_contract_is_authoritative_and_optional_season_metadata(self):
|
||||
schema = self.openapi["components"]["schemas"]["RankedProfile"]
|
||||
self.assertEqual(schema["required"], ["rating", "rd", "volatility", "ranked_games", "tier", "provisional"])
|
||||
self.assertFalse(schema["additionalProperties"])
|
||||
self.assertEqual(schema["properties"]["season_ends_at"]["format"], "date-time")
|
||||
self.assertNotIn("access_token", json.dumps(schema).lower())
|
||||
|
||||
def test_mutations_require_idempotency_and_revision(self):
|
||||
parameters = self.openapi["components"]["parameters"]
|
||||
self.assertEqual(parameters["IdempotencyKey"]["name"], "Idempotency-Key")
|
||||
|
||||
Reference in New Issue
Block a user