style: rustfmt squad host + adapter files
Formatting-only. Runs the project formatter over the files authored/edited this session (host lib+tests, adapter item/squad_ext/squad_projection/mod + projection test). The intentionally-preserved dirty catalog.rs and pre-existing /club-era host drift beyond these files are out of scope.
This commit is contained in:
@@ -87,7 +87,11 @@ fn discard_value(rating: u8) -> i64 {
|
|||||||
/// identical by construction. `id` is the owned instance's resolved FIFA
|
/// identical by construction. `id` is the owned instance's resolved FIFA
|
||||||
/// identity — pass the resolver's answer for *this* owned copy so two copies of
|
/// identity — pass the resolver's answer for *this* owned copy so two copies of
|
||||||
/// one definition stay distinct on the wire.
|
/// one definition stay distinct on the wire.
|
||||||
pub fn shape_item(item: &CoreOwnedItem, id: Fifa17Identity, ent: &impl ReverseEntityResolver) -> Value {
|
pub fn shape_item(
|
||||||
|
item: &CoreOwnedItem,
|
||||||
|
id: Fifa17Identity,
|
||||||
|
ent: &impl ReverseEntityResolver,
|
||||||
|
) -> Value {
|
||||||
let asset = id.asset_id;
|
let asset = id.asset_id;
|
||||||
let league_id = ent.league_id(&item.league).unwrap_or(0);
|
let league_id = ent.league_id(&item.league).unwrap_or(0);
|
||||||
let team_id = ent.team_id(&item.club).unwrap_or(0);
|
let team_id = ent.team_id(&item.club).unwrap_or(0);
|
||||||
@@ -155,13 +159,19 @@ mod tests {
|
|||||||
let ent = entities();
|
let ent = entities();
|
||||||
let it = shape_item(
|
let it = shape_item(
|
||||||
&item("oc1", "card_ch_1", 86, "CDM"),
|
&item("oc1", "card_ch_1", 86, "CDM"),
|
||||||
Fifa17Identity { item_id: 100000001, asset_id: 20801 },
|
Fifa17Identity {
|
||||||
|
item_id: 100000001,
|
||||||
|
asset_id: 20801,
|
||||||
|
},
|
||||||
&ent,
|
&ent,
|
||||||
);
|
);
|
||||||
assert_eq!(it["id"], 100000001, "wire instance id");
|
assert_eq!(it["id"], 100000001, "wire instance id");
|
||||||
assert_eq!(it["resourceId"], 20801);
|
assert_eq!(it["resourceId"], 20801);
|
||||||
assert_eq!(it["assetId"], 20801);
|
assert_eq!(it["assetId"], 20801);
|
||||||
assert_eq!(it["definitionId"], 20801, "version byte 0 => resourceId==assetId==definitionId");
|
assert_eq!(
|
||||||
|
it["definitionId"], 20801,
|
||||||
|
"version byte 0 => resourceId==assetId==definitionId"
|
||||||
|
);
|
||||||
assert_eq!(it["rating"], 86);
|
assert_eq!(it["rating"], 86);
|
||||||
assert_eq!(it["preferredPosition"], "CDM");
|
assert_eq!(it["preferredPosition"], "CDM");
|
||||||
assert_eq!(it["leagueId"], 13);
|
assert_eq!(it["leagueId"], 13);
|
||||||
@@ -180,16 +190,28 @@ mod tests {
|
|||||||
let ent = entities();
|
let ent = entities();
|
||||||
let a = shape_item(
|
let a = shape_item(
|
||||||
&item("oc-a", "fifa17_101490", 84, "ST"),
|
&item("oc-a", "fifa17_101490", 84, "ST"),
|
||||||
Fifa17Identity { item_id: 100000030, asset_id: 101490 },
|
Fifa17Identity {
|
||||||
|
item_id: 100000030,
|
||||||
|
asset_id: 101490,
|
||||||
|
},
|
||||||
&ent,
|
&ent,
|
||||||
);
|
);
|
||||||
let b = shape_item(
|
let b = shape_item(
|
||||||
&item("oc-b", "fifa17_101490", 84, "ST"),
|
&item("oc-b", "fifa17_101490", 84, "ST"),
|
||||||
Fifa17Identity { item_id: 100000031, asset_id: 101490 },
|
Fifa17Identity {
|
||||||
|
item_id: 100000031,
|
||||||
|
asset_id: 101490,
|
||||||
|
},
|
||||||
&ent,
|
&ent,
|
||||||
);
|
);
|
||||||
assert_eq!(a["resourceId"], b["resourceId"], "same definition => same asset");
|
assert_eq!(
|
||||||
assert_ne!(a["id"], b["id"], "distinct owned copies keep distinct wire ids");
|
a["resourceId"], b["resourceId"],
|
||||||
|
"same definition => same asset"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
a["id"], b["id"],
|
||||||
|
"distinct owned copies keep distinct wire ids"
|
||||||
|
);
|
||||||
assert_eq!(a["id"], 100000030);
|
assert_eq!(a["id"], 100000030);
|
||||||
assert_eq!(b["id"], 100000031);
|
assert_eq!(b["id"], 100000031);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
//! socket — a Rust UTAS host wires it to Core later.
|
//! socket — a Rust UTAS host wires it to Core later.
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub mod club_response;
|
pub mod club_response;
|
||||||
pub mod item;
|
|
||||||
pub mod entities;
|
pub mod entities;
|
||||||
|
pub mod item;
|
||||||
pub mod owned_query;
|
pub mod owned_query;
|
||||||
pub mod squad;
|
pub mod squad;
|
||||||
pub mod squad_ext;
|
pub mod squad_ext;
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ pub struct WireItemRef {
|
|||||||
|
|
||||||
impl From<&SquadEntityRef> for WireItemRef {
|
impl From<&SquadEntityRef> for WireItemRef {
|
||||||
fn from(r: &SquadEntityRef) -> Self {
|
fn from(r: &SquadEntityRef) -> Self {
|
||||||
WireItemRef { id: r.id, dream: r.dream }
|
WireItemRef {
|
||||||
|
id: r.id,
|
||||||
|
dream: r.dream,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +138,10 @@ impl Fifa17SquadExtensionV1 {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|k| KicktakerRef {
|
.map(|k| KicktakerRef {
|
||||||
index: k.index,
|
index: k.index,
|
||||||
item: WireItemRef { id: k.id, dream: k.dream },
|
item: WireItemRef {
|
||||||
|
id: k.id,
|
||||||
|
dream: k.dream,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
client_reported: ClientReportedSquadEval {
|
client_reported: ClientReportedSquadEval {
|
||||||
@@ -213,16 +219,23 @@ pub fn build_squad_write(
|
|||||||
) -> Result<SquadWriteBuild, SquadBuildError> {
|
) -> Result<SquadWriteBuild, SquadBuildError> {
|
||||||
let canonical = crate::fut::squad::to_proposed(put, resolver);
|
let canonical = crate::fut::squad::to_proposed(put, resolver);
|
||||||
if !canonical.unresolved_wire_ids.is_empty() {
|
if !canonical.unresolved_wire_ids.is_empty() {
|
||||||
return Err(SquadBuildError::UnresolvedWireIds(canonical.unresolved_wire_ids.clone()));
|
return Err(SquadBuildError::UnresolvedWireIds(
|
||||||
|
canonical.unresolved_wire_ids.clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for slot in &canonical.slots {
|
for slot in &canonical.slots {
|
||||||
if !seen.insert(slot.owned_card_id.as_str()) {
|
if !seen.insert(slot.owned_card_id.as_str()) {
|
||||||
return Err(SquadBuildError::DuplicateOwnedItem(slot.owned_card_id.clone()));
|
return Err(SquadBuildError::DuplicateOwnedItem(
|
||||||
|
slot.owned_card_id.clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let extension = Fifa17SquadExtensionV1::from_put(put, &canonical);
|
let extension = Fifa17SquadExtensionV1::from_put(put, &canonical);
|
||||||
Ok(SquadWriteBuild { canonical, extension })
|
Ok(SquadWriteBuild {
|
||||||
|
canonical,
|
||||||
|
extension,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -266,7 +279,8 @@ mod tests {
|
|||||||
let ext = built().extension;
|
let ext = built().extension;
|
||||||
assert_eq!(ext.custom, put.custom, "opaque custom carried verbatim");
|
assert_eq!(ext.custom, put.custom, "opaque custom carried verbatim");
|
||||||
// survives a serialize/parse cycle unchanged.
|
// survives a serialize/parse cycle unchanged.
|
||||||
let back = Fifa17SquadExtensionV1::from_payload(EXT_SCHEMA_VERSION, &ext.to_payload()).unwrap();
|
let back =
|
||||||
|
Fifa17SquadExtensionV1::from_payload(EXT_SCHEMA_VERSION, &ext.to_payload()).unwrap();
|
||||||
assert_eq!(back.custom, put.custom);
|
assert_eq!(back.custom, put.custom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,14 +299,24 @@ mod tests {
|
|||||||
let ext = built().extension;
|
let ext = built().extension;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ext.client_reported,
|
ext.client_reported,
|
||||||
ClientReportedSquadEval { chemistry: Some(52), rating: Some(90), star_rating: Some(90) }
|
ClientReportedSquadEval {
|
||||||
|
chemistry: Some(52),
|
||||||
|
rating: Some(90),
|
||||||
|
star_rating: Some(90)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manager_and_kicktakers_preserved_opaquely() {
|
fn manager_and_kicktakers_preserved_opaquely() {
|
||||||
let ext = built().extension;
|
let ext = built().extension;
|
||||||
assert_eq!(ext.manager, vec![WireItemRef { id: 100000427, dream: false }]);
|
assert_eq!(
|
||||||
|
ext.manager,
|
||||||
|
vec![WireItemRef {
|
||||||
|
id: 100000427,
|
||||||
|
dream: false
|
||||||
|
}]
|
||||||
|
);
|
||||||
assert_eq!(ext.kicktakers.len(), 5);
|
assert_eq!(ext.kicktakers.len(), 5);
|
||||||
// All five reference the same wire id in this capture; carried verbatim,
|
// All five reference the same wire id in this capture; carried verbatim,
|
||||||
// NEVER normalized to the captain even though they coincide here.
|
// NEVER normalized to the captain even though they coincide here.
|
||||||
@@ -341,7 +365,10 @@ mod tests {
|
|||||||
m.insert(100000003, "oc-dup".to_string());
|
m.insert(100000003, "oc-dup".to_string());
|
||||||
m.insert(100000004, "oc-dup".to_string()); // collide onto same owned id
|
m.insert(100000004, "oc-dup".to_string()); // collide onto same owned id
|
||||||
let err = build_squad_write(&put, &MapResolver(m)).unwrap_err();
|
let err = build_squad_write(&put, &MapResolver(m)).unwrap_err();
|
||||||
assert_eq!(err, SquadBuildError::DuplicateOwnedItem("oc-dup".to_string()));
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
SquadBuildError::DuplicateOwnedItem("oc-dup".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -350,6 +377,10 @@ mod tests {
|
|||||||
assert_eq!(build.canonical.slots.len(), 11, "whole squad, not a diff");
|
assert_eq!(build.canonical.slots.len(), 11, "whole squad, not a diff");
|
||||||
assert_eq!(build.canonical.formation.as_deref(), Some("f442"));
|
assert_eq!(build.canonical.formation.as_deref(), Some("f442"));
|
||||||
// No FIFA wire integer survives into the canonical slots.
|
// No FIFA wire integer survives into the canonical slots.
|
||||||
assert!(build.canonical.slots.iter().all(|s| s.owned_card_id.starts_with("oc-")));
|
assert!(build
|
||||||
|
.canonical
|
||||||
|
.slots
|
||||||
|
.iter()
|
||||||
|
.all(|s| s.owned_card_id.starts_with("oc-")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,10 @@ impl std::fmt::Display for SquadProjectError {
|
|||||||
write!(f, "projection input missing owned item {id}")
|
write!(f, "projection input missing owned item {id}")
|
||||||
}
|
}
|
||||||
SquadProjectError::NoFifaIdentity(id) => {
|
SquadProjectError::NoFifaIdentity(id) => {
|
||||||
write!(f, "owned item {id} has no real FIFA asset identity (cannot render)")
|
write!(
|
||||||
|
f,
|
||||||
|
"owned item {id} has no real FIFA asset identity (cannot render)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,10 +142,9 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
|||||||
for index in 0..FIFA17_SQUAD_SLOTS {
|
for index in 0..FIFA17_SQUAD_SLOTS {
|
||||||
match by_index.get(&index) {
|
match by_index.get(&index) {
|
||||||
Some(slot) => {
|
Some(slot) => {
|
||||||
let item = input
|
let item = input.owned.get(&slot.owned_card_id).ok_or_else(|| {
|
||||||
.owned
|
SquadProjectError::MissingOwnedItem(slot.owned_card_id.clone())
|
||||||
.get(&slot.owned_card_id)
|
})?;
|
||||||
.ok_or_else(|| SquadProjectError::MissingOwnedItem(slot.owned_card_id.clone()))?;
|
|
||||||
let id = ident
|
let id = ident
|
||||||
.resolve(item)
|
.resolve(item)
|
||||||
.ok_or_else(|| SquadProjectError::NoFifaIdentity(slot.owned_card_id.clone()))?;
|
.ok_or_else(|| SquadProjectError::NoFifaIdentity(slot.owned_card_id.clone()))?;
|
||||||
@@ -150,7 +152,11 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
|
|||||||
captain_wire = id.item_id as i64;
|
captain_wire = id.item_id as i64;
|
||||||
}
|
}
|
||||||
// kit follows the player: look it up by owned id, never by index.
|
// kit follows the player: look it up by owned id, never by index.
|
||||||
let kit = ext.kit_numbers.get(&slot.owned_card_id).copied().unwrap_or(0);
|
let kit = ext
|
||||||
|
.kit_numbers
|
||||||
|
.get(&slot.owned_card_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0);
|
||||||
players.push(json!({
|
players.push(json!({
|
||||||
"index": index,
|
"index": index,
|
||||||
"itemData": shape_item(item, id, ent),
|
"itemData": shape_item(item, id, ent),
|
||||||
@@ -278,14 +284,24 @@ mod tests {
|
|||||||
owned.insert("oc1".to_string(), owned_item("oc1", "card_x"));
|
owned.insert("oc1".to_string(), owned_item("oc1", "card_x"));
|
||||||
let ident = TableIdentity(HashMap::from([(
|
let ident = TableIdentity(HashMap::from([(
|
||||||
"oc1".to_string(),
|
"oc1".to_string(),
|
||||||
Fifa17Identity { item_id: 100000042, asset_id: 20801 },
|
Fifa17Identity {
|
||||||
|
item_id: 100000042,
|
||||||
|
asset_id: 20801,
|
||||||
|
},
|
||||||
)]));
|
)]));
|
||||||
let input = one_slot_input(&owned, SquadExtInput::Fresh(fresh_ext()));
|
let input = one_slot_input(&owned, SquadExtInput::Fresh(fresh_ext()));
|
||||||
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
|
||||||
panic!("expected Projected");
|
panic!("expected Projected");
|
||||||
};
|
};
|
||||||
assert_eq!(v["players"].as_array().unwrap().len(), 23, "fixed 23-slot array");
|
assert_eq!(
|
||||||
assert_eq!(v["players"][0]["itemData"]["id"], 100000042, "wire id, not resourceId");
|
v["players"].as_array().unwrap().len(),
|
||||||
|
23,
|
||||||
|
"fixed 23-slot array"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
v["players"][0]["itemData"]["id"], 100000042,
|
||||||
|
"wire id, not resourceId"
|
||||||
|
);
|
||||||
assert_eq!(v["players"][0]["itemData"]["resourceId"], 20801);
|
assert_eq!(v["players"][0]["itemData"]["resourceId"], 20801);
|
||||||
assert_eq!(v["players"][0]["kitNumber"], 9, "kit from ext by owned id");
|
assert_eq!(v["players"][0]["kitNumber"], 9, "kit from ext by owned id");
|
||||||
assert_eq!(v["players"][1]["itemData"]["id"], 0, "empty slot");
|
assert_eq!(v["players"][1]["itemData"]["id"], 0, "empty slot");
|
||||||
@@ -301,7 +317,10 @@ mod tests {
|
|||||||
// A stale extension IS carried (host may log it) but must not be applied.
|
// A stale extension IS carried (host may log it) but must not be applied.
|
||||||
let input = one_slot_input(&owned, SquadExtInput::Stale(fresh_ext()));
|
let input = one_slot_input(&owned, SquadExtInput::Stale(fresh_ext()));
|
||||||
let ident = TableIdentity(HashMap::new());
|
let ident = TableIdentity(HashMap::new());
|
||||||
assert_eq!(project_squad(&input, &ident, &ent()).unwrap(), SquadProjection::Stale);
|
assert_eq!(
|
||||||
|
project_squad(&input, &ident, &ent()).unwrap(),
|
||||||
|
SquadProjection::Stale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -309,7 +328,10 @@ mod tests {
|
|||||||
let owned = HashMap::new();
|
let owned = HashMap::new();
|
||||||
let input = one_slot_input(&owned, SquadExtInput::Missing);
|
let input = one_slot_input(&owned, SquadExtInput::Missing);
|
||||||
let ident = TableIdentity(HashMap::new());
|
let ident = TableIdentity(HashMap::new());
|
||||||
assert_eq!(project_squad(&input, &ident, &ent()).unwrap(), SquadProjection::Missing);
|
assert_eq!(
|
||||||
|
project_squad(&input, &ident, &ent()).unwrap(),
|
||||||
|
SquadProjection::Missing
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -332,21 +354,46 @@ mod tests {
|
|||||||
owned.insert("oc-a".to_string(), owned_item("oc-a", "fifa17_101490"));
|
owned.insert("oc-a".to_string(), owned_item("oc-a", "fifa17_101490"));
|
||||||
owned.insert("oc-b".to_string(), owned_item("oc-b", "fifa17_101490"));
|
owned.insert("oc-b".to_string(), owned_item("oc-b", "fifa17_101490"));
|
||||||
let ident = TableIdentity(HashMap::from([
|
let ident = TableIdentity(HashMap::from([
|
||||||
("oc-a".to_string(), Fifa17Identity { item_id: 100000030, asset_id: 101490 }),
|
(
|
||||||
("oc-b".to_string(), Fifa17Identity { item_id: 100000031, asset_id: 101490 }),
|
"oc-a".to_string(),
|
||||||
|
Fifa17Identity {
|
||||||
|
item_id: 100000030,
|
||||||
|
asset_id: 101490,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"oc-b".to_string(),
|
||||||
|
Fifa17Identity {
|
||||||
|
item_id: 100000031,
|
||||||
|
asset_id: 101490,
|
||||||
|
},
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
let mut kit = std::collections::BTreeMap::new();
|
let mut kit = std::collections::BTreeMap::new();
|
||||||
kit.insert("oc-a".to_string(), 7);
|
kit.insert("oc-a".to_string(), 7);
|
||||||
kit.insert("oc-b".to_string(), 19);
|
kit.insert("oc-b".to_string(), 19);
|
||||||
let ext = Fifa17SquadExtensionV1 { kit_numbers: kit, ..fresh_ext() };
|
let ext = Fifa17SquadExtensionV1 {
|
||||||
|
kit_numbers: kit,
|
||||||
|
..fresh_ext()
|
||||||
|
};
|
||||||
let owned_ref = &owned;
|
let owned_ref = &owned;
|
||||||
let input = SquadProjectionInput {
|
let input = SquadProjectionInput {
|
||||||
fifa_squad_id: 0,
|
fifa_squad_id: 0,
|
||||||
name: "OpenFUT".into(),
|
name: "OpenFUT".into(),
|
||||||
formation: "f442".into(),
|
formation: "f442".into(),
|
||||||
slots: vec![
|
slots: vec![
|
||||||
ProjectionSlot { owned_card_id: "oc-a".into(), index: 0, is_captain: false, is_on_bench: false },
|
ProjectionSlot {
|
||||||
ProjectionSlot { owned_card_id: "oc-b".into(), index: 1, is_captain: false, is_on_bench: false },
|
owned_card_id: "oc-a".into(),
|
||||||
|
index: 0,
|
||||||
|
is_captain: false,
|
||||||
|
is_on_bench: false,
|
||||||
|
},
|
||||||
|
ProjectionSlot {
|
||||||
|
owned_card_id: "oc-b".into(),
|
||||||
|
index: 1,
|
||||||
|
is_captain: false,
|
||||||
|
is_on_bench: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
ext: SquadExtInput::Fresh(ext),
|
ext: SquadExtInput::Fresh(ext),
|
||||||
owned: owned_ref,
|
owned: owned_ref,
|
||||||
@@ -356,9 +403,18 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let a = &v["players"][0]["itemData"];
|
let a = &v["players"][0]["itemData"];
|
||||||
let b = &v["players"][1]["itemData"];
|
let b = &v["players"][1]["itemData"];
|
||||||
assert_eq!(a["resourceId"], b["resourceId"], "same definition => same asset");
|
assert_eq!(
|
||||||
assert_ne!(a["id"], b["id"], "distinct owned copies keep distinct wire ids");
|
a["resourceId"], b["resourceId"],
|
||||||
|
"same definition => same asset"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
a["id"], b["id"],
|
||||||
|
"distinct owned copies keep distinct wire ids"
|
||||||
|
);
|
||||||
assert_eq!(v["players"][0]["kitNumber"], 7);
|
assert_eq!(v["players"][0]["kitNumber"], 7);
|
||||||
assert_eq!(v["players"][1]["kitNumber"], 19, "kit stays with the instance");
|
assert_eq!(
|
||||||
|
v["players"][1]["kitNumber"], 19,
|
||||||
|
"kit stays with the instance"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,15 @@ impl ItemIdentityResolver for TableIdentity {
|
|||||||
/// projector round-trip asserts item *identity* (wire id + asset), not entity ids.
|
/// projector round-trip asserts item *identity* (wire id + asset), not entity ids.
|
||||||
struct NoEntities;
|
struct NoEntities;
|
||||||
impl openfut_adapter_fifa17::fut::entities::ReverseEntityResolver for NoEntities {
|
impl openfut_adapter_fifa17::fut::entities::ReverseEntityResolver for NoEntities {
|
||||||
fn league_id(&self, _: &str) -> Option<u32> { None }
|
fn league_id(&self, _: &str) -> Option<u32> {
|
||||||
fn team_id(&self, _: &str) -> Option<u32> { None }
|
None
|
||||||
fn nation_id(&self, _: &str) -> Option<u32> { None }
|
}
|
||||||
|
fn team_id(&self, _: &str) -> Option<u32> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn nation_id(&self, _: &str) -> Option<u32> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the owned-item map + identity table from the persisted read oracle, so
|
/// Build the owned-item map + identity table from the persisted read oracle, so
|
||||||
@@ -99,7 +105,13 @@ fn oracle_tables() -> (HashMap<String, CoreOwnedItem>, TableIdentity) {
|
|||||||
attributes: [attrs[0], attrs[1], attrs[2], attrs[3], attrs[4], attrs[5]],
|
attributes: [attrs[0], attrs[1], attrs[2], attrs[3], attrs[4], attrs[5]],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ident.insert(oc, Fifa17Identity { item_id: wire as u32, asset_id: asset });
|
ident.insert(
|
||||||
|
oc,
|
||||||
|
Fifa17Identity {
|
||||||
|
item_id: wire as u32,
|
||||||
|
asset_id: asset,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
(owned, TableIdentity(ident))
|
(owned, TableIdentity(ident))
|
||||||
}
|
}
|
||||||
@@ -111,8 +123,10 @@ fn project_put(
|
|||||||
owned: &HashMap<String, CoreOwnedItem>,
|
owned: &HashMap<String, CoreOwnedItem>,
|
||||||
ident: &TableIdentity,
|
ident: &TableIdentity,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let SquadWriteBuild { canonical, extension } =
|
let SquadWriteBuild {
|
||||||
build_squad_write(put, &OcResolver).expect("build must succeed for a full valid squad");
|
canonical,
|
||||||
|
extension,
|
||||||
|
} = build_squad_write(put, &OcResolver).expect("build must succeed for a full valid squad");
|
||||||
let slots: Vec<ProjectionSlot> = canonical
|
let slots: Vec<ProjectionSlot> = canonical
|
||||||
.slots
|
.slots
|
||||||
.iter()
|
.iter()
|
||||||
@@ -148,7 +162,10 @@ fn occupied(v: &Value) -> HashMap<i64, (i64, i64)> {
|
|||||||
.map(|p| {
|
.map(|p| {
|
||||||
(
|
(
|
||||||
p["index"].as_i64().unwrap(),
|
p["index"].as_i64().unwrap(),
|
||||||
(p["itemData"]["id"].as_i64().unwrap(), p["kitNumber"].as_i64().unwrap()),
|
(
|
||||||
|
p["itemData"]["id"].as_i64().unwrap(),
|
||||||
|
p["kitNumber"].as_i64().unwrap(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -163,11 +180,18 @@ fn baseline_projects_the_known_squad_round_trip() {
|
|||||||
// Fixed 23-slot array; 11 occupied at 0..=10.
|
// Fixed 23-slot array; 11 occupied at 0..=10.
|
||||||
assert_eq!(projected["players"].as_array().unwrap().len(), 23);
|
assert_eq!(projected["players"].as_array().unwrap().len(), 23);
|
||||||
let put_v: Value = serde_json::from_str(PUT_BASELINE).unwrap();
|
let put_v: Value = serde_json::from_str(PUT_BASELINE).unwrap();
|
||||||
assert_eq!(occupied(&projected), occupied(&put_v), "wire id + kit per index round-trip");
|
assert_eq!(
|
||||||
|
occupied(&projected),
|
||||||
|
occupied(&put_v),
|
||||||
|
"wire id + kit per index round-trip"
|
||||||
|
);
|
||||||
|
|
||||||
// CANONICAL: formation verbatim, captain follows the semantic player.
|
// CANONICAL: formation verbatim, captain follows the semantic player.
|
||||||
assert_eq!(projected["formation"], "f442");
|
assert_eq!(projected["formation"], "f442");
|
||||||
assert_eq!(projected["captain"], 100000001, "captain is the player's WIRE id");
|
assert_eq!(
|
||||||
|
projected["captain"], 100000001,
|
||||||
|
"captain is the player's WIRE id"
|
||||||
|
);
|
||||||
// EXTENSION: custom byte-identical, manager + squadType preserved.
|
// EXTENSION: custom byte-identical, manager + squadType preserved.
|
||||||
assert_eq!(projected["custom"], put_v["custom"]);
|
assert_eq!(projected["custom"], put_v["custom"]);
|
||||||
assert_eq!(projected["manager"], put_v["manager"]);
|
assert_eq!(projected["manager"], put_v["manager"]);
|
||||||
@@ -184,12 +208,20 @@ fn swap_moves_two_players_with_their_kits_and_round_trips() {
|
|||||||
// build(swap) must reproduce the swap wire exactly, and the affected players'
|
// build(swap) must reproduce the swap wire exactly, and the affected players'
|
||||||
// kit numbers must have travelled with them (kit follows the player).
|
// kit numbers must have travelled with them (kit follows the player).
|
||||||
let (owned, ident) = oracle_tables();
|
let (owned, ident) = oracle_tables();
|
||||||
let projected = project_put(&parse_squad_put(PUT_SWAP.as_bytes()).unwrap(), &owned, &ident);
|
let projected = project_put(
|
||||||
|
&parse_squad_put(PUT_SWAP.as_bytes()).unwrap(),
|
||||||
|
&owned,
|
||||||
|
&ident,
|
||||||
|
);
|
||||||
let swap_v: Value = serde_json::from_str(PUT_SWAP).unwrap();
|
let swap_v: Value = serde_json::from_str(PUT_SWAP).unwrap();
|
||||||
let base_v: Value = serde_json::from_str(PUT_BASELINE).unwrap();
|
let base_v: Value = serde_json::from_str(PUT_BASELINE).unwrap();
|
||||||
|
|
||||||
// CANONICAL: the projected occupancy per index matches the swap PUT exactly.
|
// CANONICAL: the projected occupancy per index matches the swap PUT exactly.
|
||||||
assert_eq!(occupied(&projected), occupied(&swap_v), "player+kit per index round-trip");
|
assert_eq!(
|
||||||
|
occupied(&projected),
|
||||||
|
occupied(&swap_v),
|
||||||
|
"player+kit per index round-trip"
|
||||||
|
);
|
||||||
|
|
||||||
// The swap is real: at least two indices carry a different player than baseline.
|
// The swap is real: at least two indices carry a different player than baseline.
|
||||||
let (proj_occ, base_occ) = (occupied(&projected), occupied(&base_v));
|
let (proj_occ, base_occ) = (occupied(&projected), occupied(&base_v));
|
||||||
@@ -198,7 +230,10 @@ fn swap_moves_two_players_with_their_kits_and_round_trips() {
|
|||||||
.filter(|(idx, pair)| base_occ.get(idx).map(|b| b.0) != Some(pair.0))
|
.filter(|(idx, pair)| base_occ.get(idx).map(|b| b.0) != Some(pair.0))
|
||||||
.map(|(idx, _)| *idx)
|
.map(|(idx, _)| *idx)
|
||||||
.collect();
|
.collect();
|
||||||
assert!(moved.len() >= 2, "a swap changes at least two slots, got {moved:?}");
|
assert!(
|
||||||
|
moved.len() >= 2,
|
||||||
|
"a swap changes at least two slots, got {moved:?}"
|
||||||
|
);
|
||||||
|
|
||||||
// kit follows the PLAYER: for every player, its kit in baseline == its kit
|
// kit follows the PLAYER: for every player, its kit in baseline == its kit
|
||||||
// in the swap projection, regardless of which slot it now occupies.
|
// in the swap projection, regardless of which slot it now occupies.
|
||||||
@@ -211,8 +246,14 @@ fn swap_moves_two_players_with_their_kits_and_round_trips() {
|
|||||||
"each player kept its kit number through the swap"
|
"each player kept its kit number through the swap"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(projected["captain"], 100000001, "captain follows the semantic player");
|
assert_eq!(
|
||||||
assert_eq!(projected["custom"], swap_v["custom"], "opaque custom unchanged by the swap");
|
projected["captain"], 100000001,
|
||||||
|
"captain follows the semantic player"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
projected["custom"], swap_v["custom"],
|
||||||
|
"opaque custom unchanged by the swap"
|
||||||
|
);
|
||||||
// SHADOW: the client-reported chemistry from THIS PUT (58) is round-tripped
|
// SHADOW: the client-reported chemistry from THIS PUT (58) is round-tripped
|
||||||
// as-is — never reconciled to a server recompute.
|
// as-is — never reconciled to a server recompute.
|
||||||
assert_eq!(projected["chemistry"], 58);
|
assert_eq!(projected["chemistry"], 58);
|
||||||
@@ -225,7 +266,9 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
|||||||
// evidence, then project and require the read back — the strongest fidelity
|
// evidence, then project and require the read back — the strongest fidelity
|
||||||
// check across all four ownership classes.
|
// check across all four ownership classes.
|
||||||
use openfut_adapter_fifa17::fut::squad::ClientReportedSquadEval;
|
use openfut_adapter_fifa17::fut::squad::ClientReportedSquadEval;
|
||||||
use openfut_adapter_fifa17::fut::squad_ext::{Fifa17SquadExtensionV1, KicktakerRef, WireItemRef};
|
use openfut_adapter_fifa17::fut::squad_ext::{
|
||||||
|
Fifa17SquadExtensionV1, KicktakerRef, WireItemRef,
|
||||||
|
};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
let oracle: Value = serde_json::from_str(READ_ORACLE).unwrap();
|
let oracle: Value = serde_json::from_str(READ_ORACLE).unwrap();
|
||||||
@@ -250,7 +293,8 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let manager: Vec<WireItemRef> = serde_json::from_value(oracle["manager"].clone()).unwrap();
|
let manager: Vec<WireItemRef> = serde_json::from_value(oracle["manager"].clone()).unwrap();
|
||||||
let kicktakers: Vec<KicktakerRef> = serde_json::from_value(oracle["kicktakers"].clone()).unwrap();
|
let kicktakers: Vec<KicktakerRef> =
|
||||||
|
serde_json::from_value(oracle["kicktakers"].clone()).unwrap();
|
||||||
let ext = Fifa17SquadExtensionV1 {
|
let ext = Fifa17SquadExtensionV1 {
|
||||||
custom: oracle["custom"].as_str().map(str::to_string),
|
custom: oracle["custom"].as_str().map(str::to_string),
|
||||||
squad_type: oracle["squadType"].as_str().map(str::to_string),
|
squad_type: oracle["squadType"].as_str().map(str::to_string),
|
||||||
@@ -277,14 +321,32 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// CANONICAL + DERIVED: identity and placement per slot match the read.
|
// CANONICAL + DERIVED: identity and placement per slot match the read.
|
||||||
assert_eq!(occupied(&projected), occupied(&oracle), "player+kit per index");
|
assert_eq!(
|
||||||
|
occupied(&projected),
|
||||||
|
occupied(&oracle),
|
||||||
|
"player+kit per index"
|
||||||
|
);
|
||||||
assert_eq!(projected["captain"], oracle["captain"]);
|
assert_eq!(projected["captain"], oracle["captain"]);
|
||||||
assert_eq!(projected["formation"], oracle["formation"]);
|
assert_eq!(projected["formation"], oracle["formation"]);
|
||||||
for (pp, op) in projected["players"].as_array().unwrap().iter().zip(oracle["players"].as_array().unwrap()) {
|
for (pp, op) in projected["players"]
|
||||||
assert_eq!(pp["itemData"]["id"], op["itemData"]["id"], "wire id per slot");
|
.as_array()
|
||||||
assert_eq!(pp["itemData"]["resourceId"], op["itemData"]["resourceId"], "asset id per slot");
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.zip(oracle["players"].as_array().unwrap())
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
pp["itemData"]["id"], op["itemData"]["id"],
|
||||||
|
"wire id per slot"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pp["itemData"]["resourceId"], op["itemData"]["resourceId"],
|
||||||
|
"asset id per slot"
|
||||||
|
);
|
||||||
assert_eq!(pp["itemData"]["rating"], op["itemData"]["rating"]);
|
assert_eq!(pp["itemData"]["rating"], op["itemData"]["rating"]);
|
||||||
assert_eq!(pp["itemData"]["preferredPosition"], op["itemData"]["preferredPosition"]);
|
assert_eq!(
|
||||||
|
pp["itemData"]["preferredPosition"],
|
||||||
|
op["itemData"]["preferredPosition"]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// EXTENSION + SHADOW: sourced from the read, so they round-trip identically.
|
// EXTENSION + SHADOW: sourced from the read, so they round-trip identically.
|
||||||
assert_eq!(projected["custom"], oracle["custom"]);
|
assert_eq!(projected["custom"], oracle["custom"]);
|
||||||
@@ -299,8 +361,16 @@ fn persisted_read_round_trips_via_reconstructed_canonical_and_extension() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn formation_change_reindexes_without_deriving_slots_and_kit_follows_player() {
|
fn formation_change_reindexes_without_deriving_slots_and_kit_follows_player() {
|
||||||
let (owned, ident) = oracle_tables();
|
let (owned, ident) = oracle_tables();
|
||||||
let swap = project_put(&parse_squad_put(PUT_SWAP.as_bytes()).unwrap(), &owned, &ident);
|
let swap = project_put(
|
||||||
let f433 = project_put(&parse_squad_put(PUT_F433.as_bytes()).unwrap(), &owned, &ident);
|
&parse_squad_put(PUT_SWAP.as_bytes()).unwrap(),
|
||||||
|
&owned,
|
||||||
|
&ident,
|
||||||
|
);
|
||||||
|
let f433 = project_put(
|
||||||
|
&parse_squad_put(PUT_F433.as_bytes()).unwrap(),
|
||||||
|
&owned,
|
||||||
|
&ident,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(f433["formation"], "f433");
|
assert_eq!(f433["formation"], "f433");
|
||||||
assert_eq!(swap["formation"], "f442");
|
assert_eq!(swap["formation"], "f442");
|
||||||
@@ -311,29 +381,50 @@ fn formation_change_reindexes_without_deriving_slots_and_kit_follows_player() {
|
|||||||
ids.sort();
|
ids.sort();
|
||||||
ids
|
ids
|
||||||
};
|
};
|
||||||
assert_eq!(set(&swap), set(&f433), "same 11 players survive the formation change");
|
assert_eq!(
|
||||||
|
set(&swap),
|
||||||
|
set(&f433),
|
||||||
|
"same 11 players survive the formation change"
|
||||||
|
);
|
||||||
|
|
||||||
// The captain (wire 100000001) moved from index 8 (f442) to index 10 (f433) —
|
// The captain (wire 100000001) moved from index 8 (f442) to index 10 (f433) —
|
||||||
// proof indices are round-tripped, not derived from the formation.
|
// proof indices are round-tripped, not derived from the formation.
|
||||||
let idx_of = |v: &Value, wire: i64| -> i64 {
|
let idx_of = |v: &Value, wire: i64| -> i64 {
|
||||||
occupied(v).into_iter().find(|(_, (id, _))| *id == wire).unwrap().0
|
occupied(v)
|
||||||
|
.into_iter()
|
||||||
|
.find(|(_, (id, _))| *id == wire)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
};
|
};
|
||||||
assert_eq!(idx_of(&swap, 100000001), 8);
|
assert_eq!(idx_of(&swap, 100000001), 8);
|
||||||
assert_eq!(idx_of(&f433, 100000001), 10);
|
assert_eq!(idx_of(&f433, 100000001), 10);
|
||||||
|
|
||||||
// kit follows the PLAYER, not the slot: captain keeps kit 8 across the reindex.
|
// kit follows the PLAYER, not the slot: captain keeps kit 8 across the reindex.
|
||||||
let kit_of = |v: &Value, wire: i64| -> i64 {
|
let kit_of = |v: &Value, wire: i64| -> i64 {
|
||||||
occupied(v).into_iter().find(|(_, (id, _))| *id == wire).unwrap().1 .1
|
occupied(v)
|
||||||
|
.into_iter()
|
||||||
|
.find(|(_, (id, _))| *id == wire)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.1
|
||||||
};
|
};
|
||||||
assert_eq!(kit_of(&swap, 100000001), 8);
|
assert_eq!(kit_of(&swap, 100000001), 8);
|
||||||
assert_eq!(kit_of(&f433, 100000001), 8, "kit stayed with the player despite the reindex");
|
assert_eq!(
|
||||||
|
kit_of(&f433, 100000001),
|
||||||
|
8,
|
||||||
|
"kit stayed with the player despite the reindex"
|
||||||
|
);
|
||||||
assert_eq!(f433["captain"], 100000001, "captain still the same player");
|
assert_eq!(f433["captain"], 100000001, "captain still the same player");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_projector_serves_every_endpoint_no_divergence() {
|
fn one_projector_serves_every_endpoint_no_divergence() {
|
||||||
let (owned, ident) = oracle_tables();
|
let (owned, ident) = oracle_tables();
|
||||||
let projected = project_put(&parse_squad_put(PUT_SWAP.as_bytes()).unwrap(), &owned, &ident);
|
let projected = project_put(
|
||||||
|
&parse_squad_put(PUT_SWAP.as_bytes()).unwrap(),
|
||||||
|
&owned,
|
||||||
|
&ident,
|
||||||
|
);
|
||||||
|
|
||||||
// userMassInfo.squad = the projected object + session envelope.
|
// userMassInfo.squad = the projected object + session envelope.
|
||||||
let ummi = user_mass_info_squad(projected.clone(), 33068179);
|
let ummi = user_mass_info_squad(projected.clone(), 33068179);
|
||||||
@@ -346,8 +437,18 @@ fn one_projector_serves_every_endpoint_no_divergence() {
|
|||||||
// squad/list = a summary SUBSET of the SAME object, not a second projection.
|
// squad/list = a summary SUBSET of the SAME object, not a second projection.
|
||||||
let list = squad_list(&projected);
|
let list = squad_list(&projected);
|
||||||
let entry = &list["squad"][0];
|
let entry = &list["squad"][0];
|
||||||
for k in ["id", "squadName", "formation", "squadType", "rating", "chemistry"] {
|
for k in [
|
||||||
assert_eq!(entry[k], projected[k], "summary field {k} derived from the one projection");
|
"id",
|
||||||
|
"squadName",
|
||||||
|
"formation",
|
||||||
|
"squadType",
|
||||||
|
"rating",
|
||||||
|
"chemistry",
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
entry[k], projected[k],
|
||||||
|
"summary field {k} derived from the one projection"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// The summary carries only those six keys — no divergent squad shape.
|
// The summary carries only those six keys — no divergent squad shape.
|
||||||
assert_eq!(entry.as_object().unwrap().len(), 6);
|
assert_eq!(entry.as_object().unwrap().len(), 6);
|
||||||
|
|||||||
+165
-39
@@ -164,8 +164,14 @@ pub struct CoreSquadSlot {
|
|||||||
/// when Stale — the host decides policy).
|
/// when Stale — the host decides policy).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CoreExtState {
|
pub enum CoreExtState {
|
||||||
Fresh { schema_version: i64, payload: String },
|
Fresh {
|
||||||
Stale { schema_version: i64, payload: String },
|
schema_version: i64,
|
||||||
|
payload: String,
|
||||||
|
},
|
||||||
|
Stale {
|
||||||
|
schema_version: i64,
|
||||||
|
payload: String,
|
||||||
|
},
|
||||||
Missing,
|
Missing,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +305,11 @@ impl CoreAccess for HttpCoreClient {
|
|||||||
}
|
}
|
||||||
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
let v: Value = resp.json().map_err(|e| CoreError::Parse(e.to_string()))?;
|
||||||
Ok(CoreReplaceResult {
|
Ok(CoreReplaceResult {
|
||||||
squad_id: v.get("squad_id").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
squad_id: v
|
||||||
|
.get("squad_id")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
canonical_fingerprint: v
|
canonical_fingerprint: v
|
||||||
.get("canonical_fingerprint")
|
.get("canonical_fingerprint")
|
||||||
.and_then(|x| x.as_str())
|
.and_then(|x| x.as_str())
|
||||||
@@ -343,8 +353,14 @@ pub fn replace_request_body(req: &CoreReplaceRequest) -> Value {
|
|||||||
|
|
||||||
/// Parse Core's `GET /squad/ext` response into a [`CoreSquadRead`].
|
/// Parse Core's `GET /squad/ext` response into a [`CoreSquadRead`].
|
||||||
pub fn parse_core_squad_read(v: &Value) -> Result<CoreSquadRead, CoreError> {
|
pub fn parse_core_squad_read(v: &Value) -> Result<CoreSquadRead, CoreError> {
|
||||||
let squad = v.get("squad").ok_or_else(|| CoreError::Parse("missing `squad`".into()))?;
|
let squad = v
|
||||||
let name = squad.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string();
|
.get("squad")
|
||||||
|
.ok_or_else(|| CoreError::Parse("missing `squad`".into()))?;
|
||||||
|
let name = squad
|
||||||
|
.get("name")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
let formation = squad
|
let formation = squad
|
||||||
.get("formation")
|
.get("formation")
|
||||||
.and_then(|x| x.as_str())
|
.and_then(|x| x.as_str())
|
||||||
@@ -360,25 +376,56 @@ pub fn parse_core_squad_read(v: &Value) -> Result<CoreSquadRead, CoreError> {
|
|||||||
Some(CoreSquadSlot {
|
Some(CoreSquadSlot {
|
||||||
owned_card_id: p.get("owned_card_id")?.as_str()?.to_string(),
|
owned_card_id: p.get("owned_card_id")?.as_str()?.to_string(),
|
||||||
index: p.get("position_index")?.as_i64()?,
|
index: p.get("position_index")?.as_i64()?,
|
||||||
is_captain: p.get("is_captain").and_then(|x| x.as_bool()).unwrap_or(false),
|
is_captain: p
|
||||||
is_on_bench: p.get("is_on_bench").and_then(|x| x.as_bool()).unwrap_or(false),
|
.get("is_captain")
|
||||||
|
.and_then(|x| x.as_bool())
|
||||||
|
.unwrap_or(false),
|
||||||
|
is_on_bench: p
|
||||||
|
.get("is_on_bench")
|
||||||
|
.and_then(|x| x.as_bool())
|
||||||
|
.unwrap_or(false),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let ext_v = v.get("extension").ok_or_else(|| CoreError::Parse("missing `extension`".into()))?;
|
let ext_v = v
|
||||||
|
.get("extension")
|
||||||
|
.ok_or_else(|| CoreError::Parse("missing `extension`".into()))?;
|
||||||
let ext = match ext_v.get("state").and_then(|x| x.as_str()) {
|
let ext = match ext_v.get("state").and_then(|x| x.as_str()) {
|
||||||
Some("fresh") => CoreExtState::Fresh {
|
Some("fresh") => CoreExtState::Fresh {
|
||||||
schema_version: ext_v.get("schema_version").and_then(|x| x.as_i64()).unwrap_or(0),
|
schema_version: ext_v
|
||||||
payload: ext_v.get("payload").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
.get("schema_version")
|
||||||
|
.and_then(|x| x.as_i64())
|
||||||
|
.unwrap_or(0),
|
||||||
|
payload: ext_v
|
||||||
|
.get("payload")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
},
|
},
|
||||||
Some("stale") => CoreExtState::Stale {
|
Some("stale") => CoreExtState::Stale {
|
||||||
schema_version: ext_v.get("schema_version").and_then(|x| x.as_i64()).unwrap_or(0),
|
schema_version: ext_v
|
||||||
payload: ext_v.get("payload").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
.get("schema_version")
|
||||||
|
.and_then(|x| x.as_i64())
|
||||||
|
.unwrap_or(0),
|
||||||
|
payload: ext_v
|
||||||
|
.get("payload")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
},
|
},
|
||||||
Some("missing") => CoreExtState::Missing,
|
Some("missing") => CoreExtState::Missing,
|
||||||
other => return Err(CoreError::Parse(format!("unknown extension state {other:?}"))),
|
other => {
|
||||||
|
return Err(CoreError::Parse(format!(
|
||||||
|
"unknown extension state {other:?}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(CoreSquadRead { name, formation, slots, ext })
|
Ok(CoreSquadRead {
|
||||||
|
name,
|
||||||
|
formation,
|
||||||
|
slots,
|
||||||
|
ext,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse Core's `/collection` response `{ "collection": [...], "total": n }` into
|
/// Parse Core's `/collection` response `{ "collection": [...], "total": n }` into
|
||||||
@@ -655,7 +702,10 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
|||||||
Err(e) => return HostProjection::Error(e.to_string()),
|
Err(e) => return HostProjection::Error(e.to_string()),
|
||||||
};
|
};
|
||||||
let ext = match &read.ext {
|
let ext = match &read.ext {
|
||||||
CoreExtState::Fresh { schema_version, payload } => {
|
CoreExtState::Fresh {
|
||||||
|
schema_version,
|
||||||
|
payload,
|
||||||
|
} => {
|
||||||
match Fifa17SquadExtensionV1::from_payload(*schema_version, payload) {
|
match Fifa17SquadExtensionV1::from_payload(*schema_version, payload) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
// Fresh but the payload does not parse as our schema: corruption,
|
// Fresh but the payload does not parse as our schema: corruption,
|
||||||
@@ -670,8 +720,10 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
|
|||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => return HostProjection::Error(e.to_string()),
|
Err(e) => return HostProjection::Error(e.to_string()),
|
||||||
};
|
};
|
||||||
let owned_by_id: std::collections::HashMap<String, CoreOwnedItem> =
|
let owned_by_id: std::collections::HashMap<String, CoreOwnedItem> = owned
|
||||||
owned.into_iter().map(|i| (i.owned_card_id.clone(), i)).collect();
|
.into_iter()
|
||||||
|
.map(|i| (i.owned_card_id.clone(), i))
|
||||||
|
.collect();
|
||||||
let slots: Vec<ProjectionSlot> = read
|
let slots: Vec<ProjectionSlot> = read
|
||||||
.slots
|
.slots
|
||||||
.iter()
|
.iter()
|
||||||
@@ -718,7 +770,10 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
return (
|
return (
|
||||||
error_response(400, "parse_error"),
|
error_response(400, "parse_error"),
|
||||||
SquadLog { outcome: "parse_error", detail: e.to_string() },
|
SquadLog {
|
||||||
|
outcome: "parse_error",
|
||||||
|
detail: e.to_string(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -729,13 +784,19 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
|||||||
Err(SquadBuildError::UnresolvedWireIds(ids)) => {
|
Err(SquadBuildError::UnresolvedWireIds(ids)) => {
|
||||||
return (
|
return (
|
||||||
error_response(400, "unresolved_wire_ids"),
|
error_response(400, "unresolved_wire_ids"),
|
||||||
SquadLog { outcome: "unresolved_wire_ids", detail: format!("{ids:?}") },
|
SquadLog {
|
||||||
|
outcome: "unresolved_wire_ids",
|
||||||
|
detail: format!("{ids:?}"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Err(SquadBuildError::DuplicateOwnedItem(id)) => {
|
Err(SquadBuildError::DuplicateOwnedItem(id)) => {
|
||||||
return (
|
return (
|
||||||
error_response(400, "duplicate_owned_item"),
|
error_response(400, "duplicate_owned_item"),
|
||||||
SquadLog { outcome: "duplicate_owned_item", detail: id },
|
SquadLog {
|
||||||
|
outcome: "duplicate_owned_item",
|
||||||
|
detail: id,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -747,7 +808,10 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
return (
|
return (
|
||||||
error_response(502, "core_error"),
|
error_response(502, "core_error"),
|
||||||
SquadLog { outcome: "core_error", detail: e.to_string() },
|
SquadLog {
|
||||||
|
outcome: "core_error",
|
||||||
|
detail: e.to_string(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -755,7 +819,10 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
|||||||
if !owned_set.contains(&slot.owned_card_id) {
|
if !owned_set.contains(&slot.owned_card_id) {
|
||||||
return (
|
return (
|
||||||
error_response(403, "not_owned"),
|
error_response(403, "not_owned"),
|
||||||
SquadLog { outcome: "unauthorized_item", detail: slot.owned_card_id.clone() },
|
SquadLog {
|
||||||
|
outcome: "unauthorized_item",
|
||||||
|
detail: slot.owned_card_id.clone(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,11 +853,17 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
|
|||||||
match deps.core.replace_squad(&req) {
|
match deps.core.replace_squad(&req) {
|
||||||
Ok(_) => (
|
Ok(_) => (
|
||||||
json_response(&save_ack(put.id)),
|
json_response(&save_ack(put.id)),
|
||||||
SquadLog { outcome: "ok", detail: String::new() },
|
SquadLog {
|
||||||
|
outcome: "ok",
|
||||||
|
detail: String::new(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Err(e) => (
|
Err(e) => (
|
||||||
error_response(502, "core_error"),
|
error_response(502, "core_error"),
|
||||||
SquadLog { outcome: "core_error", detail: e.to_string() },
|
SquadLog {
|
||||||
|
outcome: "core_error",
|
||||||
|
detail: e.to_string(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,19 +875,31 @@ pub fn handle_squad_list(deps: &SquadDeps<'_>) -> (WireResponse, SquadLog) {
|
|||||||
match project_active_squad(deps) {
|
match project_active_squad(deps) {
|
||||||
HostProjection::Squad(v) => (
|
HostProjection::Squad(v) => (
|
||||||
json_response(&squad_list(&v)),
|
json_response(&squad_list(&v)),
|
||||||
SquadLog { outcome: "ok", detail: String::new() },
|
SquadLog {
|
||||||
|
outcome: "ok",
|
||||||
|
detail: String::new(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
HostProjection::Stale => (
|
HostProjection::Stale => (
|
||||||
json_response(&json!({ "squad": [] })),
|
json_response(&json!({ "squad": [] })),
|
||||||
SquadLog { outcome: "stale_integrity", detail: "stale extension not applied".into() },
|
SquadLog {
|
||||||
|
outcome: "stale_integrity",
|
||||||
|
detail: "stale extension not applied".into(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
HostProjection::Missing => (
|
HostProjection::Missing => (
|
||||||
json_response(&json!({ "squad": [] })),
|
json_response(&json!({ "squad": [] })),
|
||||||
SquadLog { outcome: "missing_integrity", detail: "no extension stored".into() },
|
SquadLog {
|
||||||
|
outcome: "missing_integrity",
|
||||||
|
detail: "no extension stored".into(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
HostProjection::Error(e) => (
|
HostProjection::Error(e) => (
|
||||||
json_response(&json!({ "squad": [] })),
|
json_response(&json!({ "squad": [] })),
|
||||||
SquadLog { outcome: "core_error", detail: e },
|
SquadLog {
|
||||||
|
outcome: "core_error",
|
||||||
|
detail: e,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -841,8 +926,10 @@ fn set_json_body(resp: &mut WireResponse, body: Vec<u8>) {
|
|||||||
&& !k.eq_ignore_ascii_case("content-type")
|
&& !k.eq_ignore_ascii_case("content-type")
|
||||||
&& !k.eq_ignore_ascii_case("transfer-encoding")
|
&& !k.eq_ignore_ascii_case("transfer-encoding")
|
||||||
});
|
});
|
||||||
resp.headers.push(("Content-Type".to_string(), "application/json".to_string()));
|
resp.headers
|
||||||
resp.headers.push(("Content-Length".to_string(), body.len().to_string()));
|
.push(("Content-Type".to_string(), "application/json".to_string()));
|
||||||
|
resp.headers
|
||||||
|
.push(("Content-Length".to_string(), body.len().to_string()));
|
||||||
resp.body = body;
|
resp.body = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,44 +949,83 @@ pub fn handle_user_mass_info(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
return (
|
return (
|
||||||
error_response(502, "upstream_unavailable"),
|
error_response(502, "upstream_unavailable"),
|
||||||
SquadLog { outcome: "python_unreachable", detail: e.to_string() },
|
SquadLog {
|
||||||
|
outcome: "python_unreachable",
|
||||||
|
detail: e.to_string(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Only a successful JSON object carrying `.squad` is overlaid; anything else
|
// Only a successful JSON object carrying `.squad` is overlaid; anything else
|
||||||
// is returned verbatim (we never invent a squad into an unrelated response).
|
// is returned verbatim (we never invent a squad into an unrelated response).
|
||||||
if !(200..300).contains(&resp.status) {
|
if !(200..300).contains(&resp.status) {
|
||||||
return (resp, SquadLog { outcome: "python_non_2xx_passthrough", detail: String::new() });
|
return (
|
||||||
|
resp,
|
||||||
|
SquadLog {
|
||||||
|
outcome: "python_non_2xx_passthrough",
|
||||||
|
detail: String::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut root: Value = match serde_json::from_slice::<Value>(&resp.body) {
|
let mut root: Value = match serde_json::from_slice::<Value>(&resp.body) {
|
||||||
Ok(v) if v.is_object() => v,
|
Ok(v) if v.is_object() => v,
|
||||||
_ => return (resp, SquadLog { outcome: "python_body_unusable_passthrough", detail: String::new() }),
|
_ => {
|
||||||
|
return (
|
||||||
|
resp,
|
||||||
|
SquadLog {
|
||||||
|
outcome: "python_body_unusable_passthrough",
|
||||||
|
detail: String::new(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if root.get("squad").is_none() {
|
if root.get("squad").is_none() {
|
||||||
return (resp, SquadLog { outcome: "python_no_squad_passthrough", detail: String::new() });
|
return (
|
||||||
|
resp,
|
||||||
|
SquadLog {
|
||||||
|
outcome: "python_no_squad_passthrough",
|
||||||
|
detail: String::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Preserve the client's persona from Python's own response.
|
// Preserve the client's persona from Python's own response.
|
||||||
let persona = root["squad"]
|
let persona = root["squad"]
|
||||||
.get("personaId")
|
.get("personaId")
|
||||||
.and_then(|x| x.as_i64())
|
.and_then(|x| x.as_i64())
|
||||||
.or_else(|| root.get("userInfo").and_then(|u| u.get("personaId")).and_then(|x| x.as_i64()))
|
.or_else(|| {
|
||||||
|
root.get("userInfo")
|
||||||
|
.and_then(|u| u.get("personaId"))
|
||||||
|
.and_then(|x| x.as_i64())
|
||||||
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let (squad_val, log) = match project_active_squad(deps) {
|
let (squad_val, log) = match project_active_squad(deps) {
|
||||||
HostProjection::Squad(v) => (
|
HostProjection::Squad(v) => (
|
||||||
user_mass_info_squad(v, persona),
|
user_mass_info_squad(v, persona),
|
||||||
SquadLog { outcome: "ok", detail: String::new() },
|
SquadLog {
|
||||||
|
outcome: "ok",
|
||||||
|
detail: String::new(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
HostProjection::Stale => (
|
HostProjection::Stale => (
|
||||||
empty_squad_overlay(persona),
|
empty_squad_overlay(persona),
|
||||||
SquadLog { outcome: "stale_integrity", detail: "stale extension not applied".into() },
|
SquadLog {
|
||||||
|
outcome: "stale_integrity",
|
||||||
|
detail: "stale extension not applied".into(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
HostProjection::Missing => (
|
HostProjection::Missing => (
|
||||||
empty_squad_overlay(persona),
|
empty_squad_overlay(persona),
|
||||||
SquadLog { outcome: "missing_integrity", detail: "no extension stored".into() },
|
SquadLog {
|
||||||
|
outcome: "missing_integrity",
|
||||||
|
detail: "no extension stored".into(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
HostProjection::Error(e) => (
|
HostProjection::Error(e) => (
|
||||||
empty_squad_overlay(persona),
|
empty_squad_overlay(persona),
|
||||||
SquadLog { outcome: "core_error", detail: e },
|
SquadLog {
|
||||||
|
outcome: "core_error",
|
||||||
|
detail: e,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
root["squad"] = squad_val;
|
root["squad"] = squad_val;
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ use openfut_adapter_fifa17::fut::club_response::{CoreOwnedItem, ItemIdentityReso
|
|||||||
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
||||||
use openfut_identity::JsonIdentityStore;
|
use openfut_identity::JsonIdentityStore;
|
||||||
use openfut_utas_host::{
|
use openfut_utas_host::{
|
||||||
classify, handle_put_squad, handle_squad_list, handle_user_mass_info, read_request,
|
classify, handle_put_squad, handle_squad_list, handle_user_mass_info, read_request, CoreAccess,
|
||||||
CoreAccess, CoreError, CoreExtState, CorePage, CoreReplaceRequest,
|
CoreError, CoreExtState, CorePage, CoreReplaceRequest, CoreReplaceResult, CoreSquadRead,
|
||||||
CoreReplaceResult, CoreSquadRead, CoreSquadSlot, Fifa17IdentityResolver, HttpCoreClient,
|
CoreSquadSlot, Fifa17IdentityResolver, HttpCoreClient, PassClient, Route, Server, SquadDeps,
|
||||||
PassClient, Route, Server, SquadDeps,
|
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -53,13 +52,23 @@ struct FakeCore {
|
|||||||
|
|
||||||
impl FakeCore {
|
impl FakeCore {
|
||||||
fn new(items: Vec<CoreOwnedItem>, total: i64) -> Self {
|
fn new(items: Vec<CoreOwnedItem>, total: i64) -> Self {
|
||||||
FakeCore { items, total, ..Default::default() }
|
FakeCore {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn forbidden() -> Self {
|
fn forbidden() -> Self {
|
||||||
FakeCore { panic_if_called: true, ..Default::default() }
|
FakeCore {
|
||||||
|
panic_if_called: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn erroring() -> Self {
|
fn erroring() -> Self {
|
||||||
FakeCore { return_err: true, ..Default::default() }
|
FakeCore {
|
||||||
|
return_err: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// Preset the stored squad read (for read-path tests without a prior PUT).
|
/// Preset the stored squad read (for read-path tests without a prior PUT).
|
||||||
fn with_squad(self, read: CoreSquadRead) -> Self {
|
fn with_squad(self, read: CoreSquadRead) -> Self {
|
||||||
@@ -85,7 +94,10 @@ impl FakeCore {
|
|||||||
|
|
||||||
impl CoreAccess for FakeCore {
|
impl CoreAccess for FakeCore {
|
||||||
fn query_owned(&self, params: &[(&str, String)]) -> Result<CorePage, CoreError> {
|
fn query_owned(&self, params: &[(&str, String)]) -> Result<CorePage, CoreError> {
|
||||||
assert!(!self.panic_if_called, "Core must NOT be called on this path");
|
assert!(
|
||||||
|
!self.panic_if_called,
|
||||||
|
"Core must NOT be called on this path"
|
||||||
|
);
|
||||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||||
if self.return_err {
|
if self.return_err {
|
||||||
return Err(CoreError::Status(500));
|
return Err(CoreError::Status(500));
|
||||||
@@ -94,11 +106,17 @@ impl CoreAccess for FakeCore {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (k.to_string(), v.clone()))
|
.map(|(k, v)| (k.to_string(), v.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(CorePage { items: self.items.clone(), total: self.total })
|
Ok(CorePage {
|
||||||
|
items: self.items.clone(),
|
||||||
|
total: self.total,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_squad_ext(&self, _namespace: &str) -> Result<CoreSquadRead, CoreError> {
|
fn read_squad_ext(&self, _namespace: &str) -> Result<CoreSquadRead, CoreError> {
|
||||||
assert!(!self.panic_if_called, "Core must NOT be called on this path");
|
assert!(
|
||||||
|
!self.panic_if_called,
|
||||||
|
"Core must NOT be called on this path"
|
||||||
|
);
|
||||||
self.read_calls.fetch_add(1, Ordering::SeqCst);
|
self.read_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
if self.return_err {
|
if self.return_err {
|
||||||
return Err(CoreError::Status(500));
|
return Err(CoreError::Status(500));
|
||||||
@@ -107,7 +125,10 @@ impl CoreAccess for FakeCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn replace_squad(&self, req: &CoreReplaceRequest) -> Result<CoreReplaceResult, CoreError> {
|
fn replace_squad(&self, req: &CoreReplaceRequest) -> Result<CoreReplaceResult, CoreError> {
|
||||||
assert!(!self.panic_if_called, "Core must NOT be called on this path");
|
assert!(
|
||||||
|
!self.panic_if_called,
|
||||||
|
"Core must NOT be called on this path"
|
||||||
|
);
|
||||||
self.replace_calls.fetch_add(1, Ordering::SeqCst);
|
self.replace_calls.fetch_add(1, Ordering::SeqCst);
|
||||||
if self.return_err {
|
if self.return_err {
|
||||||
return Err(CoreError::Status(500));
|
return Err(CoreError::Status(500));
|
||||||
@@ -117,7 +138,14 @@ impl CoreAccess for FakeCore {
|
|||||||
slots: req
|
slots: req
|
||||||
.slots
|
.slots
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| (s.owned_card_id.clone(), s.index, s.is_captain, s.is_on_bench))
|
.map(|s| {
|
||||||
|
(
|
||||||
|
s.owned_card_id.clone(),
|
||||||
|
s.index,
|
||||||
|
s.is_captain,
|
||||||
|
s.is_on_bench,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
ext_payload: req.ext_payload.clone(),
|
ext_payload: req.ext_payload.clone(),
|
||||||
chemistry: req.client_reported.chemistry,
|
chemistry: req.client_reported.chemistry,
|
||||||
@@ -634,7 +662,6 @@ fn club_end_to_end_through_real_resolver_and_sends_game_header() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Squad host: routing, PUT pipeline, authorization, coupled read-after-write ─
|
// ── Squad host: routing, PUT pipeline, authorization, coupled read-after-write ─
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -691,10 +718,26 @@ fn put_body(formation: &str, captain: i64, players: &[(i64, i64, i64)], custom:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gk() -> CoreOwnedItem {
|
fn gk() -> CoreOwnedItem {
|
||||||
item("oc-a", "card_a", 87, "GK", "Argentina", "Premier League", "Chelsea")
|
item(
|
||||||
|
"oc-a",
|
||||||
|
"card_a",
|
||||||
|
87,
|
||||||
|
"GK",
|
||||||
|
"Argentina",
|
||||||
|
"Premier League",
|
||||||
|
"Chelsea",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
fn st() -> CoreOwnedItem {
|
fn st() -> CoreOwnedItem {
|
||||||
item("oc-b", "card_b", 90, "ST", "Argentina", "Premier League", "Chelsea")
|
item(
|
||||||
|
"oc-b",
|
||||||
|
"card_b",
|
||||||
|
90,
|
||||||
|
"ST",
|
||||||
|
"Argentina",
|
||||||
|
"Premier League",
|
||||||
|
"Chelsea",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const ASSETS: &[(&str, u32)] = &[("card_a", 20801), ("card_b", 158023)];
|
const ASSETS: &[(&str, u32)] = &[("card_a", 20801), ("card_b", 158023)];
|
||||||
|
|
||||||
@@ -714,7 +757,8 @@ fn spawn_mock_python_json(body: Value) -> (String, Recorded) {
|
|||||||
};
|
};
|
||||||
let mut r = BufReader::new(s.try_clone().unwrap());
|
let mut r = BufReader::new(s.try_clone().unwrap());
|
||||||
if let Ok(Some(req)) = read_request(&mut r) {
|
if let Ok(Some(req)) = read_request(&mut r) {
|
||||||
rec2.lock().push((req.method.clone(), req.target.clone(), req.body.clone()));
|
rec2.lock()
|
||||||
|
.push((req.method.clone(), req.target.clone(), req.body.clone()));
|
||||||
let head = format!(
|
let head = format!(
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
text.len()
|
text.len()
|
||||||
@@ -731,16 +775,40 @@ fn spawn_mock_python_json(body: Value) -> (String, Recorded) {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classify_squad_and_usermassinfo_routes() {
|
fn classify_squad_and_usermassinfo_routes() {
|
||||||
assert_eq!(classify("PUT", "/ut/game/fifa17/squad/0"), Route::SquadReplace);
|
assert_eq!(
|
||||||
assert_eq!(classify("PUT", "/ut/game/fifa17/squad/3"), Route::SquadReplace);
|
classify("PUT", "/ut/game/fifa17/squad/0"),
|
||||||
assert_eq!(classify("GET", "/ut/game/fifa17/squad/list"), Route::SquadList);
|
Route::SquadReplace
|
||||||
assert_eq!(classify("GET", "/ut/game/fifa17/userMassInfo"), Route::UserMassInfo);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify("PUT", "/ut/game/fifa17/squad/3"),
|
||||||
|
Route::SquadReplace
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify("GET", "/ut/game/fifa17/squad/list"),
|
||||||
|
Route::SquadList
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify("GET", "/ut/game/fifa17/userMassInfo"),
|
||||||
|
Route::UserMassInfo
|
||||||
|
);
|
||||||
// /squad/active stays with Python (not numeric); GET squad/<n> is NOT a Rust
|
// /squad/active stays with Python (not numeric); GET squad/<n> is NOT a Rust
|
||||||
// read route; a squad PUT is never a GET.
|
// read route; a squad PUT is never a GET.
|
||||||
assert_eq!(classify("PUT", "/ut/game/fifa17/squad/active"), Route::Passthrough);
|
assert_eq!(
|
||||||
assert_eq!(classify("GET", "/ut/game/fifa17/squad/active"), Route::Passthrough);
|
classify("PUT", "/ut/game/fifa17/squad/active"),
|
||||||
assert_eq!(classify("GET", "/ut/game/fifa17/squad/0"), Route::Passthrough);
|
Route::Passthrough
|
||||||
assert_eq!(classify("PUT", "/ut/game/fifa17/squad/list"), Route::Passthrough);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify("GET", "/ut/game/fifa17/squad/active"),
|
||||||
|
Route::Passthrough
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify("GET", "/ut/game/fifa17/squad/0"),
|
||||||
|
Route::Passthrough
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify("PUT", "/ut/game/fifa17/squad/list"),
|
||||||
|
Route::Passthrough
|
||||||
|
);
|
||||||
assert_eq!(classify("GET", "/ut/game/fifa17/club"), Route::Club);
|
assert_eq!(classify("GET", "/ut/game/fifa17/club"), Route::Club);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,8 +820,17 @@ fn put_full_replacement_commits_canonical_and_extension_and_acks_id0() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-b"], 9)], "[1,2,3]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-b"], 9)],
|
||||||
|
"[1,2,3]",
|
||||||
|
);
|
||||||
|
|
||||||
let (resp, log) = handle_put_squad(&body, &deps);
|
let (resp, log) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(resp.status, 200, "{log:?}");
|
assert_eq!(resp.status, 200, "{log:?}");
|
||||||
@@ -782,8 +859,17 @@ fn put_rejects_wire_id_owned_by_another_profile_core_unchanged() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(vec![gk()], 1); // only oc-a owned
|
let core = FakeCore::new(vec![gk()], 1); // only oc-a owned
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-b"], 9)], "[]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-b"], 9)],
|
||||||
|
"[]",
|
||||||
|
);
|
||||||
|
|
||||||
let (resp, log) = handle_put_squad(&body, &deps);
|
let (resp, log) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(resp.status, 403, "resolvable != authorized");
|
assert_eq!(resp.status, 403, "resolvable != authorized");
|
||||||
@@ -798,9 +884,18 @@ fn put_rejects_unknown_wire_id() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
// 999_999_999 was never allocated → unresolvable.
|
// 999_999_999 was never allocated → unresolvable.
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, 999_999_999, 9)], "[]");
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, 999_999_999, 9)],
|
||||||
|
"[]",
|
||||||
|
);
|
||||||
let (resp, log) = handle_put_squad(&body, &deps);
|
let (resp, log) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(resp.status, 400);
|
assert_eq!(resp.status, 400);
|
||||||
assert_eq!(log.outcome, "unresolved_wire_ids");
|
assert_eq!(log.outcome, "unresolved_wire_ids");
|
||||||
@@ -813,8 +908,17 @@ fn put_rejects_duplicate_owned_item() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-a"], 9)], "[]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-a"], 9)],
|
||||||
|
"[]",
|
||||||
|
);
|
||||||
let (resp, log) = handle_put_squad(&body, &deps);
|
let (resp, log) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(resp.status, 400);
|
assert_eq!(resp.status, 400);
|
||||||
assert_eq!(log.outcome, "duplicate_owned_item");
|
assert_eq!(log.outcome, "duplicate_owned_item");
|
||||||
@@ -828,7 +932,11 @@ fn put_core_failure_returns_error_never_python() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::erroring();
|
let core = FakeCore::erroring();
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1)], "[]");
|
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1)], "[]");
|
||||||
let (resp, log) = handle_put_squad(&body, &deps);
|
let (resp, log) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(resp.status, 502);
|
assert_eq!(resp.status, 502);
|
||||||
@@ -841,8 +949,17 @@ fn repeated_identical_put_is_idempotent() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-b"], 9)], "[1,2,3]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-b"], 9)],
|
||||||
|
"[1,2,3]",
|
||||||
|
);
|
||||||
let (r1, _) = handle_put_squad(&body, &deps);
|
let (r1, _) = handle_put_squad(&body, &deps);
|
||||||
let (r2, _) = handle_put_squad(&body, &deps);
|
let (r2, _) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(r1.body, br#"{"id":0}"#);
|
assert_eq!(r1.body, br#"{"id":0}"#);
|
||||||
@@ -851,7 +968,10 @@ fn repeated_identical_put_is_idempotent() {
|
|||||||
assert_eq!(recs.len(), 2);
|
assert_eq!(recs.len(), 2);
|
||||||
assert_eq!(recs[0].slots, recs[1].slots, "canonical converges");
|
assert_eq!(recs[0].slots, recs[1].slots, "canonical converges");
|
||||||
assert_eq!(recs[0].formation, recs[1].formation);
|
assert_eq!(recs[0].formation, recs[1].formation);
|
||||||
assert_eq!(recs[0].ext_payload, recs[1].ext_payload, "extension converges");
|
assert_eq!(
|
||||||
|
recs[0].ext_payload, recs[1].ext_payload,
|
||||||
|
"extension converges"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Coupled read-after-write ─────────────────────────────────────────────────
|
// ── Coupled read-after-write ─────────────────────────────────────────────────
|
||||||
@@ -864,8 +984,17 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-b"], 9)], "[1,2,3]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-b"], 9)],
|
||||||
|
"[1,2,3]",
|
||||||
|
);
|
||||||
let (put, _) = handle_put_squad(&body, &deps);
|
let (put, _) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(put.status, 200);
|
assert_eq!(put.status, 200);
|
||||||
|
|
||||||
@@ -888,8 +1017,14 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() {
|
|||||||
});
|
});
|
||||||
let (py_url, _rec) = spawn_mock_python_json(py_body);
|
let (py_url, _rec) = spawn_mock_python_json(py_body);
|
||||||
let pass = PassClient::new(&py_url);
|
let pass = PassClient::new(&py_url);
|
||||||
let (umi_resp, umi_log) =
|
let (umi_resp, umi_log) = handle_user_mass_info(
|
||||||
handle_user_mass_info("GET", "/ut/game/fifa17/userMassInfo", &[], b"", &deps, &pass);
|
"GET",
|
||||||
|
"/ut/game/fifa17/userMassInfo",
|
||||||
|
&[],
|
||||||
|
b"",
|
||||||
|
&deps,
|
||||||
|
&pass,
|
||||||
|
);
|
||||||
assert_eq!(umi_log.outcome, "ok");
|
assert_eq!(umi_log.outcome, "ok");
|
||||||
let umi: Value = serde_json::from_slice(&umi_resp.body).unwrap();
|
let umi: Value = serde_json::from_slice(&umi_resp.body).unwrap();
|
||||||
|
|
||||||
@@ -903,7 +1038,10 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() {
|
|||||||
let sq = &umi["squad"];
|
let sq = &umi["squad"];
|
||||||
assert_eq!(sq["personaId"], 42, "persona preserved from Python");
|
assert_eq!(sq["personaId"], 42, "persona preserved from Python");
|
||||||
assert_eq!(sq["formation"], "f442");
|
assert_eq!(sq["formation"], "f442");
|
||||||
assert_eq!(sq["captain"], w["oc-a"], "captain is the wire id, not resourceId");
|
assert_eq!(
|
||||||
|
sq["captain"], w["oc-a"],
|
||||||
|
"captain is the wire id, not resourceId"
|
||||||
|
);
|
||||||
let occ: Vec<&Value> = sq["players"]
|
let occ: Vec<&Value> = sq["players"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -919,8 +1057,14 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() {
|
|||||||
let p1 = occ.iter().find(|p| p["index"] == 1).unwrap();
|
let p1 = occ.iter().find(|p| p["index"] == 1).unwrap();
|
||||||
assert_eq!(p1["itemData"]["resourceId"], 158023);
|
assert_eq!(p1["itemData"]["resourceId"], 158023);
|
||||||
assert_eq!(p1["kitNumber"], 9);
|
assert_eq!(p1["kitNumber"], 9);
|
||||||
assert!(umi["squad"]["players"].as_array().unwrap().iter().all(|p| p["itemData"]["id"] != 777),
|
assert!(
|
||||||
"no Python squad content survives");
|
umi["squad"]["players"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.all(|p| p["itemData"]["id"] != 777),
|
||||||
|
"no Python squad content survives"
|
||||||
|
);
|
||||||
|
|
||||||
// Content-Length matches the rewritten body.
|
// Content-Length matches the rewritten body.
|
||||||
let cl = umi_resp
|
let cl = umi_resp
|
||||||
@@ -928,7 +1072,11 @@ fn coupled_read_after_write_list_and_usermassinfo_agree() {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|(k, _)| k.eq_ignore_ascii_case("content-length"))
|
.find(|(k, _)| k.eq_ignore_ascii_case("content-length"))
|
||||||
.map(|(_, v)| v.parse::<usize>().unwrap());
|
.map(|(_, v)| v.parse::<usize>().unwrap());
|
||||||
assert_eq!(cl, Some(umi_resp.body.len()), "Content-Length fixed after overlay");
|
assert_eq!(
|
||||||
|
cl,
|
||||||
|
Some(umi_resp.body.len()),
|
||||||
|
"Content-Length fixed after overlay"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -937,15 +1085,32 @@ fn read_path_is_bounded_no_per_slot_lookup() {
|
|||||||
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-b"], 9)], "[]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-b"], 9)],
|
||||||
|
"[]",
|
||||||
|
);
|
||||||
handle_put_squad(&body, &deps);
|
handle_put_squad(&body, &deps);
|
||||||
let reads_before = core.read_calls();
|
let reads_before = core.read_calls();
|
||||||
let owned_before = core.calls();
|
let owned_before = core.calls();
|
||||||
let (_r, log) = handle_squad_list(&deps);
|
let (_r, log) = handle_squad_list(&deps);
|
||||||
assert_eq!(log.outcome, "ok");
|
assert_eq!(log.outcome, "ok");
|
||||||
assert_eq!(core.read_calls() - reads_before, 1, "exactly one squad read");
|
assert_eq!(
|
||||||
assert_eq!(core.calls() - owned_before, 1, "exactly one batch owned fetch (no per-slot)");
|
core.read_calls() - reads_before,
|
||||||
|
1,
|
||||||
|
"exactly one squad read"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
core.calls() - owned_before,
|
||||||
|
1,
|
||||||
|
"exactly one batch owned fetch (no per-slot)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Stale / Missing integrity ────────────────────────────────────────────────
|
// ── Stale / Missing integrity ────────────────────────────────────────────────
|
||||||
@@ -968,52 +1133,84 @@ fn read_with_ext(ext: CoreExtState) -> CoreSquadRead {
|
|||||||
fn stale_extension_is_not_applied_on_reads() {
|
fn stale_extension_is_not_applied_on_reads() {
|
||||||
let items = vec![gk()];
|
let items = vec![gk()];
|
||||||
let (resolver, _w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, _w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core = FakeCore::new(items.clone(), 1)
|
let core = FakeCore::new(items.clone(), 1).with_squad(read_with_ext(CoreExtState::Stale {
|
||||||
.with_squad(read_with_ext(CoreExtState::Stale { schema_version: 1, payload: "{}".into() }));
|
schema_version: 1,
|
||||||
|
payload: "{}".into(),
|
||||||
|
}));
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
let (resp, log) = handle_squad_list(&deps);
|
let (resp, log) = handle_squad_list(&deps);
|
||||||
assert_eq!(log.outcome, "stale_integrity");
|
assert_eq!(log.outcome, "stale_integrity");
|
||||||
let v: Value = serde_json::from_slice(&resp.body).unwrap();
|
let v: Value = serde_json::from_slice(&resp.body).unwrap();
|
||||||
assert_eq!(v["squad"].as_array().unwrap().len(), 0, "stale never projected");
|
assert_eq!(
|
||||||
|
v["squad"].as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"stale never projected"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_extension_is_explicit_on_reads() {
|
fn missing_extension_is_explicit_on_reads() {
|
||||||
let items = vec![gk()];
|
let items = vec![gk()];
|
||||||
let (resolver, _w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, _w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core =
|
let core = FakeCore::new(items.clone(), 1).with_squad(read_with_ext(CoreExtState::Missing));
|
||||||
FakeCore::new(items.clone(), 1).with_squad(read_with_ext(CoreExtState::Missing));
|
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
let (resp, log) = handle_squad_list(&deps);
|
let (resp, log) = handle_squad_list(&deps);
|
||||||
assert_eq!(log.outcome, "missing_integrity");
|
assert_eq!(log.outcome, "missing_integrity");
|
||||||
let v: Value = serde_json::from_slice(&resp.body).unwrap();
|
let v: Value = serde_json::from_slice(&resp.body).unwrap();
|
||||||
assert_eq!(v["squad"].as_array().unwrap().len(), 0, "missing never fabricated");
|
assert_eq!(
|
||||||
|
v["squad"].as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"missing never fabricated"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn usermassinfo_never_serves_python_squad_on_integrity_failure() {
|
fn usermassinfo_never_serves_python_squad_on_integrity_failure() {
|
||||||
let items = vec![gk()];
|
let items = vec![gk()];
|
||||||
let (resolver, _w) = resolver_with_wires(&items, ASSETS);
|
let (resolver, _w) = resolver_with_wires(&items, ASSETS);
|
||||||
let core =
|
let core = FakeCore::new(items.clone(), 1).with_squad(read_with_ext(CoreExtState::Missing));
|
||||||
FakeCore::new(items.clone(), 1).with_squad(read_with_ext(CoreExtState::Missing));
|
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
let py_body = json!({
|
let py_body = json!({
|
||||||
"userInfo": {"personaId": 7},
|
"userInfo": {"personaId": 7},
|
||||||
"squad": {"id": 0, "players": [{"index": 0, "itemData": {"id": 777}, "kitNumber": 5}]},
|
"squad": {"id": 0, "players": [{"index": 0, "itemData": {"id": 777}, "kitNumber": 5}]},
|
||||||
});
|
});
|
||||||
let (py_url, _rec) = spawn_mock_python_json(py_body);
|
let (py_url, _rec) = spawn_mock_python_json(py_body);
|
||||||
let pass = PassClient::new(&py_url);
|
let pass = PassClient::new(&py_url);
|
||||||
let (resp, log) =
|
let (resp, log) = handle_user_mass_info(
|
||||||
handle_user_mass_info("GET", "/ut/game/fifa17/userMassInfo", &[], b"", &deps, &pass);
|
"GET",
|
||||||
|
"/ut/game/fifa17/userMassInfo",
|
||||||
|
&[],
|
||||||
|
b"",
|
||||||
|
&deps,
|
||||||
|
&pass,
|
||||||
|
);
|
||||||
assert_eq!(log.outcome, "missing_integrity");
|
assert_eq!(log.outcome, "missing_integrity");
|
||||||
let v: Value = serde_json::from_slice(&resp.body).unwrap();
|
let v: Value = serde_json::from_slice(&resp.body).unwrap();
|
||||||
assert_eq!(v["userInfo"]["personaId"], 7, "unrelated fields still Python");
|
assert_eq!(
|
||||||
|
v["userInfo"]["personaId"], 7,
|
||||||
|
"unrelated fields still Python"
|
||||||
|
);
|
||||||
// Rust owns squad: Python's squad (id 777) must NOT survive.
|
// Rust owns squad: Python's squad (id 777) must NOT survive.
|
||||||
assert!(v["squad"]["players"].as_array().unwrap().is_empty());
|
assert!(v["squad"]["players"].as_array().unwrap().is_empty());
|
||||||
assert_eq!(v["squad"]["personaId"], 7, "persona preserved, squad emptied");
|
assert_eq!(
|
||||||
|
v["squad"]["personaId"], 7,
|
||||||
|
"persona preserved, squad emptied"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1023,7 +1220,10 @@ fn unrelated_route_still_reaches_python() {
|
|||||||
let server = build_server(core, &py_url, None);
|
let server = build_server(core, &py_url, None);
|
||||||
let resp = server.handle("POST", "/ut/game/fifa17/packs/purchase", &[], b"{}");
|
let resp = server.handle("POST", "/ut/game/fifa17/packs/purchase", &[], b"{}");
|
||||||
assert_eq!(resp.status, 200);
|
assert_eq!(resp.status, 200);
|
||||||
assert!(resp.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("x-from-python")));
|
assert!(resp
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.any(|(k, _)| k.eq_ignore_ascii_case("x-from-python")));
|
||||||
assert_eq!(rec.lock().len(), 1, "reached Python");
|
assert_eq!(rec.lock().len(), 1, "reached Python");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,23 +1231,54 @@ fn unrelated_route_still_reaches_python() {
|
|||||||
fn duplicate_definition_instances_stay_distinct_through_host() {
|
fn duplicate_definition_instances_stay_distinct_through_host() {
|
||||||
// Two owned copies of ONE definition (card_a): distinct owned ids and wire
|
// Two owned copies of ONE definition (card_a): distinct owned ids and wire
|
||||||
// ids, one shared resourceId — must not collapse anywhere in the host.
|
// ids, one shared resourceId — must not collapse anywhere in the host.
|
||||||
let a = item("oc-a", "card_a", 87, "GK", "Argentina", "Premier League", "Chelsea");
|
let a = item(
|
||||||
let c = item("oc-c", "card_a", 87, "ST", "Argentina", "Premier League", "Chelsea");
|
"oc-a",
|
||||||
|
"card_a",
|
||||||
|
87,
|
||||||
|
"GK",
|
||||||
|
"Argentina",
|
||||||
|
"Premier League",
|
||||||
|
"Chelsea",
|
||||||
|
);
|
||||||
|
let c = item(
|
||||||
|
"oc-c",
|
||||||
|
"card_a",
|
||||||
|
87,
|
||||||
|
"ST",
|
||||||
|
"Argentina",
|
||||||
|
"Premier League",
|
||||||
|
"Chelsea",
|
||||||
|
);
|
||||||
let items = vec![a, c];
|
let items = vec![a, c];
|
||||||
let (resolver, w) = resolver_with_wires(&items, &[("card_a", 20801)]);
|
let (resolver, w) = resolver_with_wires(&items, &[("card_a", 20801)]);
|
||||||
assert_ne!(w["oc-a"], w["oc-c"], "two copies get distinct wire ids");
|
assert_ne!(w["oc-a"], w["oc-c"], "two copies get distinct wire ids");
|
||||||
let core = FakeCore::new(items.clone(), 2);
|
let core = FakeCore::new(items.clone(), 2);
|
||||||
let ent = entities();
|
let ent = entities();
|
||||||
let deps = SquadDeps { core: &core, resolver: &resolver, entities: &ent };
|
let deps = SquadDeps {
|
||||||
let body = put_body("f442", w["oc-a"], &[(0, w["oc-a"], 1), (1, w["oc-c"], 7)], "[]");
|
core: &core,
|
||||||
|
resolver: &resolver,
|
||||||
|
entities: &ent,
|
||||||
|
};
|
||||||
|
let body = put_body(
|
||||||
|
"f442",
|
||||||
|
w["oc-a"],
|
||||||
|
&[(0, w["oc-a"], 1), (1, w["oc-c"], 7)],
|
||||||
|
"[]",
|
||||||
|
);
|
||||||
let (put, _) = handle_put_squad(&body, &deps);
|
let (put, _) = handle_put_squad(&body, &deps);
|
||||||
assert_eq!(put.status, 200);
|
assert_eq!(put.status, 200);
|
||||||
|
|
||||||
let py = json!({"userInfo": {"personaId": 1}, "squad": {"players": []}});
|
let py = json!({"userInfo": {"personaId": 1}, "squad": {"players": []}});
|
||||||
let (url, _r) = spawn_mock_python_json(py);
|
let (url, _r) = spawn_mock_python_json(py);
|
||||||
let pass = PassClient::new(&url);
|
let pass = PassClient::new(&url);
|
||||||
let (umi, _) =
|
let (umi, _) = handle_user_mass_info(
|
||||||
handle_user_mass_info("GET", "/ut/game/fifa17/userMassInfo", &[], b"", &deps, &pass);
|
"GET",
|
||||||
|
"/ut/game/fifa17/userMassInfo",
|
||||||
|
&[],
|
||||||
|
b"",
|
||||||
|
&deps,
|
||||||
|
&pass,
|
||||||
|
);
|
||||||
let v: Value = serde_json::from_slice(&umi.body).unwrap();
|
let v: Value = serde_json::from_slice(&umi.body).unwrap();
|
||||||
let occ: Vec<&Value> = v["squad"]["players"]
|
let occ: Vec<&Value> = v["squad"]["players"]
|
||||||
.as_array()
|
.as_array()
|
||||||
@@ -1063,4 +1294,4 @@ fn duplicate_definition_instances_stay_distinct_through_host() {
|
|||||||
for p in &occ {
|
for p in &occ {
|
||||||
assert_eq!(p["itemData"]["resourceId"], 20801, "shared asset id");
|
assert_eq!(p["itemData"]["resourceId"], 20801, "shared asset id");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user