diff --git a/openfut-utas-host/tests/host_test.rs b/openfut-utas-host/tests/host_test.rs index 4d8ccb9..7012298 100644 --- a/openfut-utas-host/tests/host_test.rs +++ b/openfut-utas-host/tests/host_test.rs @@ -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)], +) -> Arc { + let entries: Vec = 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)> = 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 = 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 { + 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_. + 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) + ); +}