test(fifa17): replay retail purchased/items BUY + route matrix via dispatch
- pure_economy_routes (NEVER-BOTH + no-fallback) gains POST/GET purchased/items, lowercase tradepile, tradePile/counts -> all asserted Rust-owned, proxy 0. - retail_purchased_items_buy_debits_core_through_dispatch: the exact round-2 live failure -> POST /purchased/items now debits Core, reveal shows minted items, repeat reveal idempotent, Python proxy count 0.
This commit is contained in:
@@ -1006,6 +1006,24 @@ fn pure_economy_routes() -> Vec<(&'static str, String, Vec<u8>)> {
|
|||||||
br#"{"packId":70}"#.to_vec(),
|
br#"{"packId":70}"#.to_vec(),
|
||||||
),
|
),
|
||||||
("GET", "/ut/v2/game/fifa17/purchased".into(), b"".to_vec()),
|
("GET", "/ut/v2/game/fifa17/purchased".into(), b"".to_vec()),
|
||||||
|
// ── Round-2 retail shapes: the confirmed BUY uses POST /purchased/items,
|
||||||
|
// reveal GET /purchased/items; hub tile polls lowercase tradepile + /counts. ──
|
||||||
|
(
|
||||||
|
"POST",
|
||||||
|
"/ut/game/fifa17/purchased/items".into(),
|
||||||
|
br#"{"packId":1}"#.to_vec(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"GET",
|
||||||
|
"/ut/game/fifa17/purchased/items".into(),
|
||||||
|
b"".to_vec(),
|
||||||
|
),
|
||||||
|
("GET", "/ut/game/fifa17/tradepile".into(), b"".to_vec()),
|
||||||
|
(
|
||||||
|
"GET",
|
||||||
|
"/ut/game/fifa17/tradePile/counts".into(),
|
||||||
|
b"".to_vec(),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,3 +1324,94 @@ async fn retail_v2_store_flow_matches_v1_through_dispatch() {
|
|||||||
h.abort();
|
h.abort();
|
||||||
std::fs::remove_dir_all(&dir).ok();
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────── Retail /purchased/items BUY sequence (round-2 S2 regression) ─
|
||||||
|
//
|
||||||
|
// The rejected candidate 47ced22 sent the confirmed retail Store BUY
|
||||||
|
// (POST /ut/game/fifa17/purchased/items) to Python and left Core coins unchanged.
|
||||||
|
// This replays the exact live sequence through real dispatch and asserts the BUY
|
||||||
|
// debits Core, the reveal shows the minted items, and Python proxy count is 0.
|
||||||
|
|
||||||
|
fn retail_purchased_items_flow(base: &str, dir: &std::path::Path) {
|
||||||
|
let mock = start_mock_python();
|
||||||
|
let (server, client, _r, _s) = build_econ_server(base, dir, &mock.url);
|
||||||
|
let calls0 = mock.calls.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
|
// Store screen catalogue (v1 /all) — Rust.
|
||||||
|
let pg = server.handle_with_ip(
|
||||||
|
"GET",
|
||||||
|
"/ut/game/fifa17/store/purchasegroup/all",
|
||||||
|
&[],
|
||||||
|
b"",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(pg.status, 200, "purchasegroup/all Rust-owned");
|
||||||
|
|
||||||
|
// THE CONFIRMED RETAIL BUY: POST /ut/game/fifa17/purchased/items must debit Core.
|
||||||
|
let bal0 = client.balance().unwrap();
|
||||||
|
let buy = server.handle_with_ip(
|
||||||
|
"POST",
|
||||||
|
"/ut/game/fifa17/purchased/items",
|
||||||
|
&[],
|
||||||
|
br#"{"packId":1}"#,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(buy.status, 200, "purchased/items BUY handled by Rust (200)");
|
||||||
|
assert!(
|
||||||
|
!buy.body.windows(10).any(|w| w == b"__python__"),
|
||||||
|
"purchased/items BUY is Rust-owned (no Python marker)"
|
||||||
|
);
|
||||||
|
let bal1 = client.balance().unwrap();
|
||||||
|
assert!(
|
||||||
|
bal1 < bal0,
|
||||||
|
"purchased/items BUY debited Core ({bal0} -> {bal1}) — the round-2 S2 was NO debit"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reveal poll: GET /ut/game/fifa17/purchased/items — Rust, shows the minted items.
|
||||||
|
let reveal = server.handle_with_ip("GET", "/ut/game/fifa17/purchased/items", &[], b"", None);
|
||||||
|
assert_eq!(reveal.status, 200, "purchased/items reveal Rust-owned");
|
||||||
|
let rv: Value = serde_json::from_slice(&reveal.body).unwrap();
|
||||||
|
let revealed = rv["itemData"].as_array().map_or(0, |a| a.len());
|
||||||
|
assert!(revealed > 0, "reveal shows the freshly-minted items: {rv}");
|
||||||
|
|
||||||
|
// Repeat reveal is idempotent (no re-grant, no extra debit).
|
||||||
|
let bal2 = client.balance().unwrap();
|
||||||
|
let _ = server.handle_with_ip("GET", "/ut/game/fifa17/purchased/items", &[], b"", None);
|
||||||
|
assert_eq!(
|
||||||
|
client.balance().unwrap(),
|
||||||
|
bal2,
|
||||||
|
"repeat reveal does not mutate coins"
|
||||||
|
);
|
||||||
|
|
||||||
|
// NEVER any Python proxy across the whole /purchased/items sequence.
|
||||||
|
assert_eq!(
|
||||||
|
mock.calls.load(std::sync::atomic::Ordering::SeqCst),
|
||||||
|
calls0,
|
||||||
|
"retail /purchased/items sequence never reached the Python proxy"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn retail_purchased_items_buy_debits_core_through_dispatch() {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"openfut-econ-pi-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let db_url = format!("sqlite://{}/econ.db", dir.display());
|
||||||
|
let (h, base) = start_core_seeded(&db_url, true).await;
|
||||||
|
let (b, d) = (base.clone(), dir.clone());
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
std::thread::spawn(move || retail_purchased_items_flow(&b, &d))
|
||||||
|
.join()
|
||||||
|
.expect("purchased/items flow thread")
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("purchased/items phase");
|
||||||
|
h.abort();
|
||||||
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user