test(host): lock the whole ownable taxonomy end to end

A club holding every ownable class, served through the REAL catalog resolver, so
each family travels the production classification path rather than a stub.

Asserts each family reaches its own `?type=` arm, that the staff arm carries the
manager too, and that `teamid` appears only where a caption resolves
TeamName_Abbr15 (badge yes, stadium no).

The cardtype-9 families are asserted WITHHELD with their catalog entries
RESOLVABLE, so an empty ball list is provably a decision about the family and
not an accident of a missing asset id — the two failure modes are otherwise
indistinguishable from the response.

Mutation-checked: reverting `is_cardtype7_club_item` to Kit-only fails this test
on the badge arm, so it guards the behaviour rather than merely describing it.
This commit is contained in:
funman300
2026-08-21 20:58:21 +00:00
parent beb505b0fa
commit 67d896615c
+126
View File
@@ -2441,3 +2441,129 @@ fn incomplete_consumable_definitions_are_dropped_not_drawn_wrong() {
"neither definition can be drawn honestly"
);
}
/// A resolver over a catalog that states each card's KIND, so club families flow
/// through exactly the production classification path (catalog kind -> shaper),
/// not a test-only stub.
fn club_resolver(
cards: &[(&str, u32, &str, i64, u32, Option<i64>)],
) -> Arc<Fifa17IdentityResolver> {
let entries: Vec<String> = cards
.iter()
.map(|(id, asset, kind, subtype, art, team)| {
let team = team
.map(|t| format!(",\"team_id\":{t}"))
.unwrap_or_default();
format!(
"\"{id}\":{{\"asset_id\":{asset},\"kind\":\"{kind}\",\"subtype\":{subtype},\
\"card_asset_id\":{art}{team}}}"
)
})
.collect();
let doc = format!(
"{{\"schema_version\":1,\"game\":\"fifa17\",\"cards\":{{{}}}}}",
entries.join(",")
);
let catalog = Fifa17CardCatalog::from_json_str(&doc).unwrap();
let store = JsonIdentityStore::open(unique_store_path()).unwrap();
Arc::new(Fifa17IdentityResolver::new(catalog, Arc::new(store)))
}
/// A club holding EVERY ownable class, served end to end through the real
/// catalog resolver.
///
/// This is the whole-taxonomy lock. Each family must reach the client on its own
/// `?type=` arm, and the two cardtype-9 families must be WITHHELD rather than
/// guessed — and withheld deliberately, which is why their identities resolve
/// here: an empty result must not be an accident of a missing asset id.
#[test]
fn every_ownable_class_projects_on_its_own_arm() {
let cards: Vec<(&str, u32, &str, i64, u32, Option<i64>)> = vec![
("c_player", 20801, "player", 0, 0, None),
("c_manager", 1_000_509, "manager", 4, 0, None),
("c_coach", 3_000_083, "staff", 8, 0, None),
("c_kit", 6_300_006, "kit", 9, 35, Some(21)),
("c_badge", 6_000_005, "badge", 11, 39, Some(21)),
("c_stadium", 6_200_000, "stadium", 10, 36, None),
("c_ball", 8_120_194, "ball", 30, 37, None),
("c_logo", 8_010_015, "misc", 31, 40, None),
];
let owned: Vec<CoreOwnedItem> = cards
.iter()
.map(|(id, ..)| {
item(
&format!("oc_{id}"),
id,
84,
"ST",
"Spain",
"La Liga",
"Barcelona",
)
})
.collect();
let core = Arc::new(FakeCore::new(owned, cards.len() as i64));
let resolver = club_resolver(&cards);
let ents = entities();
let hidden = std::collections::HashSet::new();
let active_kits = CoreKitAssignments::default();
let deps = ClubDeps {
core: core.as_ref(),
entities: &ents,
assets: resolver.as_ref(),
hidden: &hidden,
active_kits: &active_kits,
};
let served = |arm: &str| -> Vec<Value> {
let (resp, _log) = handle_club(&format!("type={arm}&count=50"), &deps);
let v: Value = serde_json::from_slice(&resp.body).unwrap();
v["itemData"].as_array().cloned().unwrap_or_default()
};
// Projected families, each on its own arm.
for (arm, subtype) in [("player", 0i64), ("kit", 9), ("badge", 11), ("stadium", 10)] {
let got = served(arm);
assert_eq!(got.len(), 1, "type={arm} must serve exactly its own item");
if subtype != 0 {
assert_eq!(got[0]["cardsubtypeid"], subtype, "type={arm}");
}
}
// The staff arm is the whole family: coach AND manager.
assert_eq!(
served("staff").len(),
2,
"manager is inside the staff family"
);
assert_eq!(
served("manager").len(),
2,
"the manager arm asks for the family too"
);
// teamid only where the caption resolves TeamName_Abbr15_<teamid>.
assert_eq!(served("badge")[0]["teamid"], 21);
assert!(
served("stadium")[0].get("teamid").is_none(),
"a stadium caption reads assetId, never teamid"
);
// Cardtype 9: withheld on purpose. Their catalog entries resolve, so an empty
// result here is a decision about the family and not a missing identity.
for arm in ["ball", "misc"] {
assert!(
served(arm).is_empty(),
"type={arm} is cardtype 9: no DB name resolver, so it stays withheld"
);
}
// A consumable never rides in THIS envelope even when owned: it has its own
// route and a stack-wrapper element, and the client silently discards a bare
// consumable item here.
assert!(
served("consumables").is_empty()
|| served("consumables")
.iter()
.all(|i| i["cardsubtypeid"] != 0)
);
}