test(fifa17): harden SBC retail acceptance

This commit is contained in:
funman300
2026-08-18 19:01:33 +00:00
parent f9740f640d
commit e8d1c1ddac
8 changed files with 650 additions and 25 deletions
+237 -1
View File
@@ -12,7 +12,7 @@
//! touches the production Core DB, production ports/containers, or `.105`.
use openfut_adapter_fifa17::fut::catalog::Fifa17CardCatalog;
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
use openfut_adapter_fifa17::fut::entities::{Fifa17Entities, ReverseEntityResolver};
use openfut_adapter_fifa17::fut::store_session::StoreMode;
use openfut_identity::JsonIdentityStore;
use openfut_utas_host::async_bridge::AsyncBridge;
@@ -769,6 +769,7 @@ fn from_config(base: &str, dir: &std::path::Path) -> openfut_utas_host::config::
.join("active_account.json")
.to_string_lossy()
.into_owned(),
sbc_post_commit_fault: openfut_utas_host::config::SbcPostCommitFault::Off,
}
}
@@ -908,6 +909,212 @@ async fn from_config_constructs_and_serves_economy() {
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn sbc_survives_complete_core_and_host_restart() {
let dir = std::env::temp_dir().join(format!(
"openfut-sbc-restart-{}-{}",
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://{}/sbc.db", dir.display());
let (h1, base1) = start_core_seeded(&db_url, true).await;
let first_base = base1.clone();
let first_dir = dir.clone();
let (mut cfg, selected_core_ids, selected_wire_ids) = tokio::task::spawn_blocking(move || {
let cfg = from_config(&first_base, &first_dir);
let server = Server::from_config(&cfg).expect("first complete host");
let club = server.handle("GET", "/ut/game/fifa17/club?count=200", &[], b"");
assert_eq!(club.status, 200);
let club: Value = serde_json::from_slice(&club.body).unwrap();
let items = club["itemData"].as_array().expect("club itemData");
let entities =
Fifa17Entities::from_tables_dir(std::path::Path::new(&cfg.tables_dir)).unwrap();
let argentina = i64::from(entities.nation_id("Argentina").unwrap());
let brazil = i64::from(entities.nation_id("Brazil").unwrap());
let mut selected = Vec::new();
for nation in [argentina, brazil] {
selected.push(
items
.iter()
.find(|item| {
item["rating"].as_i64().unwrap_or_default() >= 70
&& item["nation"].as_i64() == Some(nation)
})
.expect("required hybrid nation")
.clone(),
);
}
for item in items {
if selected.len() == 11 {
break;
}
if item["rating"].as_i64().unwrap_or_default() >= 70
&& !selected.iter().any(|existing| existing["id"] == item["id"])
{
selected.push(item.clone());
}
}
assert_eq!(selected.len(), 11, "deterministic Hybrid Nations squad");
let selected_wire_ids: Vec<i64> = selected
.iter()
.map(|item| item["id"].as_i64().expect("wire id"))
.collect();
let catalog = Fifa17CardCatalog::from_file(std::path::Path::new(&cfg.catalog_path))
.expect("catalog reload");
let store = JsonIdentityStore::open(&cfg.identity_store_path).expect("identity reload");
let resolver = Fifa17IdentityResolver::new(catalog, Arc::new(store));
let selected_core_ids: Vec<String> = selected_wire_ids
.iter()
.map(|wire| {
resolver
.owned_id_for_wire(*wire)
.expect("wire mapping persisted")
})
.collect();
let squad = json!({
"squad": selected_wire_ids
.iter()
.enumerate()
.map(|(index, id)| json!({
"index": index,
"itemData": { "id": id },
"kitNumber": 0
}))
.collect::<Vec<_>>()
});
let squad_bytes = serde_json::to_vec(&squad).unwrap();
assert_eq!(
server
.handle(
"PUT",
"/ut/game/fifa17/sbs/challenge/201/squad",
&[],
&squad_bytes,
)
.status,
200
);
let submitted = server.handle("PUT", "/ut/game/fifa17/sbs/challenge/201", &[], br#"{}"#);
assert_eq!(submitted.status, 200);
let submitted: Value = serde_json::from_slice(&submitted.body).unwrap();
assert_eq!(submitted["challengeId"], 201);
assert_eq!(submitted["credits"], 102_750);
assert_eq!(submitted["recoveredPacks"], 1);
(cfg, selected_core_ids, selected_wire_ids)
})
.await
.expect("initial complete-host phase");
h1.abort();
let _ = h1.await;
let (h2, base2) = start_core_seeded(&db_url, false).await;
cfg.core_url = base2.clone();
let selected_core_ids_check = selected_core_ids.clone();
let selected_wire_ids_check = selected_wire_ids.clone();
tokio::task::spawn_blocking(move || {
let server = Server::from_config(&cfg).expect("restarted complete host");
let client = HttpCoreClient::new(&base2, "fifa17");
assert_eq!(client.balance().unwrap(), 102_750);
assert_eq!(client.entitlements().unwrap().len(), 1);
assert_eq!(
client
.sbc_completion_counts()
.unwrap()
.get("sbc_hybrid_nations")
.copied(),
Some(1)
);
let owned = client.all_owned().unwrap();
assert!(selected_core_ids_check
.iter()
.all(|id| { owned.iter().all(|item| item.owned_card_id != *id) }));
let club = server.handle("GET", "/ut/game/fifa17/club?count=200", &[], b"");
let club: Value = serde_json::from_slice(&club.body).unwrap();
assert!(selected_wire_ids_check.iter().all(|id| {
club["itemData"]
.as_array()
.unwrap()
.iter()
.all(|item| item["id"].as_i64() != Some(*id))
}));
let saved = server.handle("GET", "/ut/game/fifa17/sbs/challenge/201/squad", &[], b"");
let saved: Value = serde_json::from_slice(&saved.body).unwrap();
assert!(saved["squad"].as_array().unwrap().is_empty());
let purchased = server.handle("GET", "/ut/v2/game/fifa17/purchased/items", &[], b"");
let purchased: Value = serde_json::from_slice(&purchased.body).unwrap();
assert!(purchased["itemData"].as_array().unwrap().is_empty());
for path in ["/ut/game/fifa17/tradePile", "/ut/game/fifa17/watchList"] {
let pile = server.handle("GET", path, &[], b"");
let pile: Value = serde_json::from_slice(&pile.body).unwrap();
assert!(pile["auctionInfo"]
.as_array()
.unwrap()
.iter()
.all(|auction| {
selected_wire_ids_check
.iter()
.all(|id| auction["itemData"]["id"].as_i64() != Some(*id))
}));
}
let active = server.handle("GET", "/ut/game/fifa17/squad/active", &[], b"");
let active: Value = serde_json::from_slice(&active.body).unwrap();
assert!(selected_wire_ids_check.iter().all(|id| {
active["players"].as_array().is_none_or(|players| {
players
.iter()
.all(|slot| slot["itemData"]["id"].as_i64() != Some(*id))
})
}));
let replay_body = json!({
"squad": selected_wire_ids_check
.iter()
.map(|id| json!({ "itemData": { "id": id } }))
.collect::<Vec<_>>()
});
assert_eq!(
server
.handle(
"PUT",
"/ut/game/fifa17/sbs/challenge/201",
&[],
&serde_json::to_vec(&replay_body).unwrap(),
)
.status,
404,
"host rejects consumed wire ids before a duplicate effect"
);
assert!(matches!(
client.submit_sbc("sbc_hybrid_nations", &selected_core_ids_check),
Err(openfut_utas_host::CoreError::Status(409))
));
assert_eq!(client.balance().unwrap(), 102_750);
assert_eq!(client.entitlements().unwrap().len(), 1);
let catalog = Fifa17CardCatalog::from_file(std::path::Path::new(&cfg.catalog_path))
.expect("restart catalog");
let store = JsonIdentityStore::open(&cfg.identity_store_path).expect("restart identity");
let resolver = Fifa17IdentityResolver::new(catalog, Arc::new(store));
for (wire, core_id) in selected_wire_ids_check.iter().zip(&selected_core_ids_check) {
assert_eq!(
resolver.owned_id_for_wire(*wire).as_deref(),
Some(core_id.as_str())
);
}
})
.await
.expect("restart verification");
h2.abort();
let _ = h2.await;
std::fs::remove_dir_all(&dir).ok();
}
// ─────────────── Post-barrier authority proofs (NEVER BOTH / no fallback / ────
// stale reader), through the REAL handle_with_ip dispatch ──────
@@ -1040,6 +1247,35 @@ fn pure_economy_routes() -> Vec<(&'static str, String, Vec<u8>)> {
"/ut/game/fifa17/tradePile/counts".into(),
b"".to_vec(),
),
// ── FIFA 17 SBC family. Reads, tag acknowledgement, durable squad
// saves, challenge start, and submission are all Rust-owned. ──
("GET", "/ut/game/fifa17/sbs/sets".into(), b"".to_vec()),
(
"GET",
"/ut/game/fifa17/sbs/setId/1/challenges".into(),
b"".to_vec(),
),
(
"GET",
"/ut/game/fifa17/sbs/challenge/101/squad".into(),
b"".to_vec(),
),
("PUT", "/ut/game/fifa17/sbs/sets/tag".into(), b"{}".to_vec()),
(
"PUT",
"/ut/game/fifa17/sbs/challenge/101/squad".into(),
br#"{"squad":[]}"#.to_vec(),
),
(
"POST",
"/ut/game/fifa17/sbs/challenge/101".into(),
b"".to_vec(),
),
(
"PUT",
"/ut/game/fifa17/sbs/challenge/101".into(),
br#"{"squad":[]}"#.to_vec(),
),
]
}