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:
funman300
2026-06-25 16:32:37 -07:00
parent 3b7a4928c9
commit b8e766e9dd
6 changed files with 510 additions and 14 deletions
+198
View File
@@ -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);
}
}