Phase 7 (Bridge): response shaper, diff tool, expanded mapper
- shaper.rs: shape_response() dispatches on core_path to wrap Core JSON in FUT envelope format; shapes auth (/ut/auth → sid/pid/ phishingToken/persona envelope), profile, club, squad, market, packs; unknown paths pass through unchanged - proxy.rs: calls shape_response() on every successful Core response before returning to the FIFA 23 client - mapper.rs: expanded from 6 to 18 speculative FUT endpoint mappings covering auth, profile/settings, club/usermassinfo, item/collection, squad (GET+PUT), packs+purchase, transfer market+watchlist+bid, objectives, events, squad battles, and match result submission - admin.rs: GET /_bridge/captures/diff?a=<id>&b=<id> compares two captures; reports method/path/status changes, header diffs, and structured JSON body diffs (key-level for objects, length for others) - main.rs: registers /_bridge/captures/diff route - Unit tests: 4 shaper tests, 6 mapper tests (10 total, all passing) - All 13 integration tests still passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+198
@@ -0,0 +1,198 @@
|
||||
//! Response shaper: transforms OpenFUT Core responses into the envelope format
|
||||
//! FIFA 23 expects, based on the Core path that was called.
|
||||
//!
|
||||
//! These are best-guess wrappers derived from typical EA FUT API patterns.
|
||||
//! They will be refined as real traffic captures come in.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Shape a Core JSON response into the format FIFA 23 expects for this path.
|
||||
/// Returns the shaped response or the original unchanged if no shaping is defined.
|
||||
pub fn shape_response(core_path: &str, core_response: Value) -> Value {
|
||||
match core_path {
|
||||
"/auth/local" => shape_auth_response(core_response),
|
||||
"/profile" => shape_profile_response(core_response),
|
||||
"/club" => shape_club_response(core_response),
|
||||
"/squad" => shape_squad_response(core_response),
|
||||
"/market" => shape_market_response(core_response),
|
||||
"/packs" => shape_packs_response(core_response),
|
||||
_ => core_response,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps Core auth response in the FUT auth envelope format.
|
||||
///
|
||||
/// EA's `/ut/auth` returns a top-level object with a SID, PID, and persona.
|
||||
/// The `sid` is used as a session token in subsequent `X-UT-SID` headers.
|
||||
fn shape_auth_response(core: Value) -> Value {
|
||||
let profile_id = core
|
||||
.get("profile")
|
||||
.and_then(|p| p.get("id"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let display_name = core
|
||||
.get("profile")
|
||||
.and_then(|p| p.get("username"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("FUT Player");
|
||||
|
||||
let fake_sid = format!("ofut-sid-{}", Uuid::new_v4().simple());
|
||||
let fake_pid = format!("1{}", &Uuid::new_v4().simple().to_string()[..9]);
|
||||
let phishing_token = format!("ptkn-{}", Uuid::new_v4().simple());
|
||||
|
||||
json!({
|
||||
"sid": fake_sid,
|
||||
"pid": {
|
||||
"pidId": fake_pid,
|
||||
"email": format!("{}@openfut.local", display_name.to_lowercase().replace(' ', "_")),
|
||||
"emailStatus": "VERIFIED",
|
||||
"strength": "STRONG",
|
||||
"dob": "1990-01-01",
|
||||
"country": "US",
|
||||
},
|
||||
"phishingToken": phishing_token,
|
||||
"persona": {
|
||||
"personaId": profile_id,
|
||||
"displayName": display_name,
|
||||
"isEmailVerified": true,
|
||||
},
|
||||
"userAccountInfo": {
|
||||
"userAccountInfoSummary": [{
|
||||
"personaId": profile_id,
|
||||
"displayName": display_name,
|
||||
"clubs": [{ "clubType": "FUT", "supported": true }],
|
||||
}],
|
||||
},
|
||||
"openfut": true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wraps Core profile response in the FUT user settings envelope.
|
||||
fn shape_profile_response(core: Value) -> Value {
|
||||
let profile = core.get("profile").cloned().unwrap_or(core.clone());
|
||||
|
||||
json!({
|
||||
"settings": {
|
||||
"personaId": profile.get("id").cloned().unwrap_or(json!("unknown")),
|
||||
"displayName": profile.get("username").cloned().unwrap_or(json!("FUT Player")),
|
||||
"level": profile.get("level").cloned().unwrap_or(json!(1)),
|
||||
"xp": profile.get("xp").cloned().unwrap_or(json!(0)),
|
||||
},
|
||||
"openfut": true,
|
||||
"_raw": profile,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wraps Core club response in the FUT usermassinfo envelope.
|
||||
fn shape_club_response(core: Value) -> Value {
|
||||
let club = core.get("club").cloned().unwrap_or(core.clone());
|
||||
|
||||
json!({
|
||||
"pilluserinfo": {
|
||||
"clubId": club.get("id").cloned().unwrap_or(json!("unknown")),
|
||||
"credits": club.get("coins").cloned().unwrap_or(json!(0)),
|
||||
"purchased": 0,
|
||||
"sold": 0,
|
||||
},
|
||||
"userInfo": {
|
||||
"duplicationInfo": { "isDuplicate": false },
|
||||
"purchasedPacksInfo": { "premium": 0, "standard": 0 },
|
||||
},
|
||||
"openfut": true,
|
||||
"_raw": club,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wraps Core squad response in the FUT squad envelope.
|
||||
fn shape_squad_response(core: Value) -> Value {
|
||||
let squad = core.get("squad").cloned().unwrap_or(core.clone());
|
||||
let players = core.get("players").cloned().unwrap_or(json!([]));
|
||||
|
||||
json!({
|
||||
"squad": {
|
||||
"squadId": squad.get("id").cloned().unwrap_or(json!("unknown")),
|
||||
"name": squad.get("name").cloned().unwrap_or(json!("My Squad")),
|
||||
"formation": squad.get("formation").cloned().unwrap_or(json!("4-4-2")),
|
||||
"players": players,
|
||||
"rating": 0,
|
||||
"chemistry": 0,
|
||||
},
|
||||
"openfut": true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wraps Core market response in the FUT transfer market envelope.
|
||||
fn shape_market_response(core: Value) -> Value {
|
||||
let listings = core.get("listings").cloned().unwrap_or(json!([]));
|
||||
let total = core
|
||||
.get("total")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
|
||||
json!({
|
||||
"auctionInfo": listings,
|
||||
"bidTokens": {},
|
||||
"credits": 0,
|
||||
"currencies": [
|
||||
{ "name": "COINS", "funds": 0, "finalFunds": 0 },
|
||||
],
|
||||
"duplicateItemIdList": [],
|
||||
"itemData": [],
|
||||
"total": total,
|
||||
"openfut": true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wraps Core packs response in the FUT pack store envelope.
|
||||
fn shape_packs_response(core: Value) -> Value {
|
||||
let packs = core.get("packs").cloned().unwrap_or(json!([]));
|
||||
|
||||
json!({
|
||||
"purchasedPacks": {
|
||||
"packs": packs,
|
||||
},
|
||||
"openfut": true,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_shape_auth_response_has_required_fields() {
|
||||
let core = json!({
|
||||
"profile": { "id": "prof_001", "username": "TestPlayer" }
|
||||
});
|
||||
let shaped = shape_auth_response(core);
|
||||
assert!(shaped.get("sid").is_some());
|
||||
assert!(shaped.get("phishingToken").is_some());
|
||||
assert!(shaped.get("persona").is_some());
|
||||
assert_eq!(shaped["persona"]["displayName"], "TestPlayer");
|
||||
assert_eq!(shaped["openfut"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shape_response_dispatches_correctly() {
|
||||
let core = json!({ "profile": { "id": "x", "username": "u" } });
|
||||
let shaped = shape_response("/auth/local", core);
|
||||
assert!(shaped.get("sid").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shape_response_passthrough_for_unknown_path() {
|
||||
let core = json!({ "foo": "bar" });
|
||||
let shaped = shape_response("/some/unknown/path", core.clone());
|
||||
assert_eq!(shaped, core);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shape_market_response_has_auction_info() {
|
||||
let core = json!({ "listings": [{ "id": "l1" }], "total": 1 });
|
||||
let shaped = shape_market_response(core);
|
||||
assert!(shaped["auctionInfo"].as_array().is_some());
|
||||
assert_eq!(shaped["total"], 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user