feat(squad): expose extension-aware squad read/write over HTTP
Add two thin transport routes wrapping the existing extension services
(no new domain logic; Core still owns validation, ownership, the atomic
canonical+extension transaction, the server fingerprint, and staleness):
GET /squad/ext?namespace=<ns> -> read_squad_with_ext
returns {squad, players, extension:{state: fresh|stale|missing,
schema_version, payload, stored_fingerprint, current_fingerprint}}
PUT /squad/replace -> replace_squad_with_extension
body {name, formation, slots[], client_reported, extension};
resolves the active squad in place (creates if none); returns
{squad_id, canonical_fingerprint, slots_written}
A game host needs these to read/persist the FIFA squad extension atomically
over HTTP; the service functions existed but were unreachable. Adds an
integration test (replace -> read Fresh, verbatim payload, idempotent PUT
converges to the same fingerprint, missing-namespace -> Missing) and clears
a pre-existing len_zero lint so the crate is clippy-clean.
This commit is contained in:
@@ -675,7 +675,7 @@ async fn test_draft_pick_advances_session() {
|
||||
assert_eq!(pick1["status"], "active");
|
||||
assert_eq!(pick1["progress"]["filled"], 1);
|
||||
assert_eq!(pick1["current_position"], "RB");
|
||||
assert!(pick1["candidates"].as_array().unwrap().len() >= 1);
|
||||
assert!(!pick1["candidates"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2279,3 +2279,84 @@ async fn test_owned_query_parameter_order_invariance() {
|
||||
);
|
||||
assert_eq!(a["total"], b["total"]);
|
||||
}
|
||||
|
||||
|
||||
async fn json_put(app: &axum::Router, uri: &str, payload: Value) -> (StatusCode, Value) {
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri(uri)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(payload.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let status = resp.status();
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
(status, serde_json::from_slice(&body).unwrap())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_squad_ext_replace_read_roundtrip_and_idempotency() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "SquadExtUser").await;
|
||||
|
||||
// Owned cards from the starter pack.
|
||||
let (_, packs) = json_get(&app, "/packs").await;
|
||||
let pack_id = packs["packs"][0]["pack_id"].as_str().unwrap().to_string();
|
||||
json_post(&app, &format!("/packs/open/{pack_id}"), serde_json::json!({})).await;
|
||||
let (_, coll) = json_get(&app, "/collection").await;
|
||||
let ids: Vec<String> = coll["collection"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.take(2)
|
||||
.map(|c| c["owned_card_id"].as_str().unwrap().to_string())
|
||||
.collect();
|
||||
assert!(ids.len() >= 2, "starter pack should yield >=2 owned cards");
|
||||
|
||||
let payload = "{\"custom\":\"[1,2,3]\",\"kit_numbers\":{}}";
|
||||
let body = serde_json::json!({
|
||||
"name": "OpenFUT",
|
||||
"formation": "f442",
|
||||
"slots": [
|
||||
{"owned_card_id": ids[0], "slot": 0, "is_captain": true, "is_on_bench": false},
|
||||
{"owned_card_id": ids[1], "slot": 1, "is_captain": false, "is_on_bench": false},
|
||||
],
|
||||
"client_reported": {
|
||||
"client_reported_chemistry": 52,
|
||||
"client_reported_rating": 90,
|
||||
"client_reported_star_rating": 90
|
||||
},
|
||||
"extension": {"namespace": "fifa17.squad", "schema_version": 1, "payload": payload},
|
||||
});
|
||||
|
||||
let (s, put) = json_put(&app, "/squad/replace", body.clone()).await;
|
||||
assert_eq!(s, StatusCode::OK, "{put}");
|
||||
assert_eq!(put["slots_written"], 2);
|
||||
let fp = put["canonical_fingerprint"].as_str().unwrap().to_string();
|
||||
|
||||
// Read the canonical squad + opaque extension back: Fresh, payload verbatim.
|
||||
let (s, ext) = json_get(&app, "/squad/ext?namespace=fifa17.squad").await;
|
||||
assert_eq!(s, StatusCode::OK, "{ext}");
|
||||
assert_eq!(ext["extension"]["state"], "fresh");
|
||||
assert_eq!(ext["extension"]["payload"], payload, "opaque payload round-trips verbatim");
|
||||
assert_eq!(ext["extension"]["schema_version"], 1);
|
||||
assert_eq!(ext["extension"]["stored_fingerprint"], fp);
|
||||
assert_eq!(ext["squad"]["formation"], "f442");
|
||||
assert_eq!(ext["players"].as_array().unwrap().len(), 2);
|
||||
|
||||
// Idempotent: an identical replacement converges to the same fingerprint.
|
||||
let (s2, put2) = json_put(&app, "/squad/replace", body).await;
|
||||
assert_eq!(s2, StatusCode::OK);
|
||||
assert_eq!(put2["canonical_fingerprint"], fp, "identical PUT is idempotent");
|
||||
|
||||
// A different namespace has no stored extension: Missing, never fabricated.
|
||||
let (_, other) = json_get(&app, "/squad/ext?namespace=other.ns").await;
|
||||
assert_eq!(other["extension"]["state"], "missing");
|
||||
}
|
||||
Reference in New Issue
Block a user