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:
@@ -4,4 +4,5 @@ pub mod error;
|
||||
pub mod mapper;
|
||||
pub mod proxy;
|
||||
pub mod routes;
|
||||
pub mod shaper;
|
||||
pub mod tls;
|
||||
|
||||
@@ -48,6 +48,10 @@ async fn main() -> Result<()> {
|
||||
get(routes::admin::get_unknown_endpoints),
|
||||
)
|
||||
.route("/_bridge/status", get(routes::admin::get_endpoint_status))
|
||||
.route(
|
||||
"/_bridge/captures/diff",
|
||||
get(routes::admin::get_capture_diff),
|
||||
)
|
||||
.fallback(any(openfut_bridge::proxy::catch_all_handler))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(CorsLayer::permissive())
|
||||
|
||||
+169
-12
@@ -11,43 +11,81 @@ pub struct CoreMapping {
|
||||
|
||||
/// Attempt to map an incoming FIFA 23 request to an OpenFUT Core call.
|
||||
/// Returns Some(mapping) if the route is known, None otherwise.
|
||||
///
|
||||
/// Mappings are speculative — refined as real traffic captures come in.
|
||||
pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
||||
let m = method.to_uppercase();
|
||||
let p = path.trim_end_matches('/');
|
||||
|
||||
// These are speculative mappings based on common FUT API patterns.
|
||||
// They will be refined as reverse engineering progresses.
|
||||
let known: &[(&str, &str, CoreMapping)] = &[
|
||||
// Auth
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────
|
||||
(
|
||||
"POST",
|
||||
"/ut/auth",
|
||||
CoreMapping {
|
||||
method: "POST",
|
||||
core_path: "/auth/local",
|
||||
notes: "FUT auth → Core local auth",
|
||||
notes: "FUT login → Core local auth (shaped into EA envelope format)",
|
||||
},
|
||||
),
|
||||
// ── Profile / Settings ───────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/user/settings",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/profile",
|
||||
notes: "FUT settings → Core profile",
|
||||
notes: "FUT user settings → Core profile",
|
||||
},
|
||||
),
|
||||
// Club
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/user/accountinfo",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/profile",
|
||||
notes: "FUT account info → Core profile",
|
||||
},
|
||||
),
|
||||
// ── Club / Mass info ──────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/usermassinfo",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/club",
|
||||
notes: "FUT mass info → Core club",
|
||||
notes: "FUT mass info (club + coins) → Core club",
|
||||
},
|
||||
),
|
||||
// Squad
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/club",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/club",
|
||||
notes: "FUT club info → Core club",
|
||||
},
|
||||
),
|
||||
// ── Cards / Collection ────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/item",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/cards/collection",
|
||||
notes: "FUT collection → Core player card collection",
|
||||
},
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/club/stats",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/stats",
|
||||
notes: "FUT club stats → Core statistics summary",
|
||||
},
|
||||
),
|
||||
// ── Squad ─────────────────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/squad/active",
|
||||
@@ -57,7 +95,16 @@ pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
||||
notes: "FUT active squad → Core squad",
|
||||
},
|
||||
),
|
||||
// Packs
|
||||
(
|
||||
"PUT",
|
||||
"/ut/game/fut/squad/active",
|
||||
CoreMapping {
|
||||
method: "POST",
|
||||
core_path: "/squad",
|
||||
notes: "FUT save squad → Core save squad",
|
||||
},
|
||||
),
|
||||
// ── Packs ─────────────────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/store/packdetails",
|
||||
@@ -67,14 +114,80 @@ pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
||||
notes: "FUT pack store → Core pack list",
|
||||
},
|
||||
),
|
||||
// Transfer market (guessed)
|
||||
(
|
||||
"POST",
|
||||
"/ut/game/fut/store/purchase",
|
||||
CoreMapping {
|
||||
method: "POST",
|
||||
core_path: "/packs/buy",
|
||||
notes: "FUT pack purchase → Core pack buy (guess)",
|
||||
},
|
||||
),
|
||||
// ── Transfer Market ───────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/transfermarket",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/market",
|
||||
notes: "FUT transfer market → Core NPC market",
|
||||
notes: "FUT transfer market search → Core NPC market",
|
||||
},
|
||||
),
|
||||
(
|
||||
"POST",
|
||||
"/ut/game/fut/trade/bid",
|
||||
CoreMapping {
|
||||
method: "POST",
|
||||
core_path: "/market/buy",
|
||||
notes: "FUT bid/buy now → Core market buy",
|
||||
},
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/trade/watchlist",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/market",
|
||||
notes: "FUT watchlist → Core market (approximation)",
|
||||
},
|
||||
),
|
||||
// ── Objectives ────────────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/objectives",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/objectives",
|
||||
notes: "FUT objectives → Core objectives list",
|
||||
},
|
||||
),
|
||||
// ── Events ────────────────────────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/events",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/events",
|
||||
notes: "FUT events → Core events list",
|
||||
},
|
||||
),
|
||||
// ── Matches / Squad Battles ───────────────────────────────────────────────
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fut/squadbattle/opponent",
|
||||
CoreMapping {
|
||||
method: "GET",
|
||||
core_path: "/matches/opponent",
|
||||
notes: "FUT squad battles opponent → Core match opponent generator",
|
||||
},
|
||||
),
|
||||
(
|
||||
"POST",
|
||||
"/ut/game/fut/result",
|
||||
CoreMapping {
|
||||
method: "POST",
|
||||
core_path: "/matches",
|
||||
notes: "FUT match result submit → Core match submission",
|
||||
},
|
||||
),
|
||||
];
|
||||
@@ -89,7 +202,7 @@ pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
||||
}
|
||||
|
||||
/// Return a safe placeholder response for unknown endpoints.
|
||||
/// This prevents the FIFA 23 client from crashing while we log traffic.
|
||||
/// Prevents the FIFA 23 client from crashing while we log traffic.
|
||||
pub fn placeholder_response(method: &str, path: &str) -> serde_json::Value {
|
||||
tracing::warn!(
|
||||
"UNKNOWN ENDPOINT: {} {} — returning placeholder",
|
||||
@@ -103,3 +216,47 @@ pub fn placeholder_response(method: &str, path: &str) -> serde_json::Value {
|
||||
"path": path,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_known_auth_maps() {
|
||||
let m = map_to_core("POST", "/ut/auth");
|
||||
assert!(m.is_some());
|
||||
assert_eq!(m.unwrap().core_path, "/auth/local");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_known_market_maps() {
|
||||
let m = map_to_core("GET", "/ut/game/fut/transfermarket");
|
||||
assert!(m.is_some());
|
||||
assert_eq!(m.unwrap().core_path, "/market");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_events_maps() {
|
||||
let m = map_to_core("GET", "/ut/game/fut/events");
|
||||
assert!(m.is_some());
|
||||
assert_eq!(m.unwrap().core_path, "/events");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_squad_put_maps() {
|
||||
let m = map_to_core("PUT", "/ut/game/fut/squad/active");
|
||||
assert!(m.is_some());
|
||||
assert_eq!(m.unwrap().method, "POST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_returns_none() {
|
||||
assert!(map_to_core("GET", "/ut/game/fut/some/unknown/path").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_slash_trimmed() {
|
||||
let m = map_to_core("POST", "/ut/auth/");
|
||||
assert!(m.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ use crate::{
|
||||
config::Config,
|
||||
error::BridgeResult,
|
||||
mapper::{map_to_core, placeholder_response},
|
||||
shaper::shape_response,
|
||||
};
|
||||
|
||||
const CAPTURE_DEDUP_SECS: u64 = 1;
|
||||
@@ -118,7 +119,7 @@ pub async fn catch_all_handler(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((body, status)) => (body, status),
|
||||
Ok((body, status)) => (shape_response(mapping.core_path, body), status),
|
||||
Err(e) => {
|
||||
tracing::error!("Core request failed: {e}");
|
||||
(serde_json::json!({ "error": e.to_string() }), 502)
|
||||
|
||||
+136
-1
@@ -1,11 +1,12 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
extract::{Query, State},
|
||||
http::{header, StatusCode},
|
||||
response::{
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
IntoResponse, Json, Response,
|
||||
},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::convert::Infallible;
|
||||
use tokio_stream::{wrappers::BroadcastStream, StreamExt};
|
||||
@@ -136,6 +137,140 @@ pub async fn get_endpoint_status(State(state): State<ProxyState>) -> BridgeResul
|
||||
})))
|
||||
}
|
||||
|
||||
/// Query params for the diff endpoint.
|
||||
#[derive(Deserialize)]
|
||||
pub struct DiffQuery {
|
||||
pub a: String,
|
||||
pub b: String,
|
||||
}
|
||||
|
||||
/// Diff two captures by ID: shows what changed between them.
|
||||
///
|
||||
/// Usage: GET /_bridge/captures/diff?a=<uuid>&b=<uuid>
|
||||
pub async fn get_capture_diff(
|
||||
State(state): State<ProxyState>,
|
||||
Query(params): Query<DiffQuery>,
|
||||
) -> BridgeResult<Json<Value>> {
|
||||
let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?;
|
||||
|
||||
let find = |id: &str| -> Option<&crate::capture::CapturedRequest> {
|
||||
captures.iter().find(|c| c.id == id)
|
||||
};
|
||||
|
||||
let cap_a = find(¶ms.a)
|
||||
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a)))?;
|
||||
let cap_b = find(¶ms.b)
|
||||
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b)))?;
|
||||
|
||||
// Structural JSON diff of request bodies
|
||||
let body_diff = diff_json_strings(cap_a.body.as_deref(), cap_b.body.as_deref());
|
||||
let resp_diff = diff_json_strings(
|
||||
cap_a.response_body.as_deref(),
|
||||
cap_b.response_body.as_deref(),
|
||||
);
|
||||
|
||||
// Header diff: keys present in one but not the other
|
||||
let headers_a: std::collections::HashSet<String> =
|
||||
cap_a.headers.iter().map(|(k, _)| k.clone()).collect();
|
||||
let headers_b: std::collections::HashSet<String> =
|
||||
cap_b.headers.iter().map(|(k, _)| k.clone()).collect();
|
||||
|
||||
let headers_only_in_a: Vec<_> = headers_a.difference(&headers_b).collect();
|
||||
let headers_only_in_b: Vec<_> = headers_b.difference(&headers_a).collect();
|
||||
|
||||
// Headers present in both but with different values
|
||||
let headers_changed: Vec<Value> = cap_a
|
||||
.headers
|
||||
.iter()
|
||||
.filter_map(|(k, va)| {
|
||||
cap_b.headers.iter().find(|(kb, _)| kb == k).and_then(|(_, vb)| {
|
||||
if va != vb {
|
||||
Some(json!({ "header": k, "a": va, "b": vb }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(json!({
|
||||
"a": { "id": cap_a.id, "timestamp": cap_a.timestamp, "method": cap_a.method, "path": cap_a.path },
|
||||
"b": { "id": cap_b.id, "timestamp": cap_b.timestamp, "method": cap_b.method, "path": cap_b.path },
|
||||
"method_changed": cap_a.method != cap_b.method,
|
||||
"path_changed": cap_a.path != cap_b.path,
|
||||
"status_a": cap_a.response_status,
|
||||
"status_b": cap_b.response_status,
|
||||
"status_changed": cap_a.response_status != cap_b.response_status,
|
||||
"headers": {
|
||||
"only_in_a": headers_only_in_a,
|
||||
"only_in_b": headers_only_in_b,
|
||||
"changed": headers_changed,
|
||||
},
|
||||
"body_diff": body_diff,
|
||||
"response_diff": resp_diff,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Shallow JSON diff of two optional JSON strings.
|
||||
///
|
||||
/// If both parse as JSON objects, lists keys that differ or are absent.
|
||||
/// Falls back to string comparison if either is not valid JSON.
|
||||
fn diff_json_strings(a: Option<&str>, b: Option<&str>) -> Value {
|
||||
match (a, b) {
|
||||
(None, None) => json!({ "same": true, "note": "both empty" }),
|
||||
(Some(_), None) => json!({ "same": false, "note": "b has no body" }),
|
||||
(None, Some(_)) => json!({ "same": false, "note": "a has no body" }),
|
||||
(Some(a_str), Some(b_str)) => {
|
||||
if a_str == b_str {
|
||||
return json!({ "same": true });
|
||||
}
|
||||
// Try to parse as JSON objects for a structured diff
|
||||
let va: Option<serde_json::Map<String, Value>> =
|
||||
serde_json::from_str(a_str).ok();
|
||||
let vb: Option<serde_json::Map<String, Value>> =
|
||||
serde_json::from_str(b_str).ok();
|
||||
|
||||
match (va, vb) {
|
||||
(Some(ma), Some(mb)) => {
|
||||
let keys_a: std::collections::HashSet<&str> =
|
||||
ma.keys().map(|s| s.as_str()).collect();
|
||||
let keys_b: std::collections::HashSet<&str> =
|
||||
mb.keys().map(|s| s.as_str()).collect();
|
||||
|
||||
let only_in_a: Vec<&str> = keys_a.difference(&keys_b).copied().collect();
|
||||
let only_in_b: Vec<&str> = keys_b.difference(&keys_a).copied().collect();
|
||||
let changed: Vec<Value> = keys_a
|
||||
.intersection(&keys_b)
|
||||
.filter_map(|k| {
|
||||
let va = ma.get(*k)?;
|
||||
let vb = mb.get(*k)?;
|
||||
if va != vb {
|
||||
Some(json!({ "key": k, "a": va, "b": vb }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
json!({
|
||||
"same": false,
|
||||
"type": "json_object_diff",
|
||||
"keys_only_in_a": only_in_a,
|
||||
"keys_only_in_b": only_in_b,
|
||||
"keys_changed": changed,
|
||||
})
|
||||
}
|
||||
_ => json!({
|
||||
"same": false,
|
||||
"type": "string_diff",
|
||||
"a_len": a_str.len(),
|
||||
"b_len": b_str.len(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple HTML admin dashboard.
|
||||
pub async fn get_admin_dashboard() -> impl IntoResponse {
|
||||
let html = include_str!("../admin.html");
|
||||
|
||||
+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