//! FIFA 17 development content pack + ownership seed (Commit 5). //! //! Proves: the dev pack is opt-in and isolated from default content; the seed //! creates a `game_id=fifa17` profile/club and grants real Core `OwnedCard`s //! (never FIFA wire ids); it is idempotent and leaves the default profile alone; //! and the seeded inventory can exercise the retail `/club` filter + pagination. use openfut_core::db::Pool; use openfut_core::services::card_db::CardDb; async fn pool() -> Pool { let pool = sqlx::sqlite::SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .expect("in-memory sqlite"); sqlx::migrate!("./migrations") .run(&pool) .await .expect("migrations"); pool } fn dev_card_db() -> CardDb { let mut db = CardDb::load("data").expect("default cards"); db.load_game_dev("data", "fifa17").expect("dev pack"); db } // ── Content isolation ──────────────────────────────────────────────────────── #[test] fn default_load_never_contains_dev_pack() { // The default global loader reads only data/cards — the dev pack under // data/games/fifa17/dev must be invisible unless explicitly requested. let default = CardDb::load("data").expect("default cards"); let leaked: Vec<_> = default .cards .keys() .filter(|k| k.starts_with("fifa17_")) .collect(); assert!( leaked.is_empty(), "default content must not include FIFA17 dev cards: {leaked:?}" ); assert!( !default.cards.is_empty(), "default synthetic catalogue still loads" ); } #[test] fn opt_in_load_adds_dev_pack_only() { let default_n = CardDb::load("data").unwrap().cards.len(); let db = dev_card_db(); let dev: Vec<_> = db .cards .keys() .filter(|k| k.starts_with("fifa17_")) .collect(); assert_eq!(dev.len(), 32, "the curated dev pack is 32 definitions"); assert_eq!( db.cards.len(), default_n + 32, "dev pack is additive; default content unchanged" ); } #[test] fn dev_definitions_carry_semantic_names_not_raw_ids() { let db = dev_card_db(); for c in db.cards.values().filter(|c| c.id.starts_with("fifa17_")) { // Semantic Core fields are names, never raw FIFA numeric entity ids. assert!( c.nation.parse::().is_err(), "nation must be a name, got {:?}", c.nation ); assert!( c.league.parse::().is_err(), "league must be a name: {:?}", c.league ); assert!( c.club.parse::().is_err(), "club must be a name: {:?}", c.club ); assert!(!c.name.is_empty(), "every dev card has a player name"); } } // ── Ownership seed ───────────────────────────────────────────────────────── #[tokio::test] async fn seed_grants_game_scoped_inventory_with_filter_coverage() { let pool = pool().await; let db = dev_card_db(); let r = openfut_core::seed::seed_fifa17_dev(&pool, &db) .await .unwrap(); assert_eq!(r.game_id, "fifa17"); assert!(!r.already_seeded); assert_eq!(r.definitions_available, 32); assert_eq!(r.owned_total, 33, "32 defs + 1 deliberate duplicate"); assert_eq!(r.unique_definitions, 32); assert!(r.gold_over_one_page, "gold spans >1 page (22 > 11)"); assert!(r.gold > 11, "enough gold for pagination"); assert!(r.silver >= 1 && r.bronze >= 1, "quality spread"); assert!(r.positions.contains_key("GK"), "GK present"); assert!(r.positions.contains_key("ST"), "ST present"); assert!(r.distinct_leagues >= 2, "multiple leagues"); assert!(r.distinct_nations >= 2, "multiple nations"); assert!(r.max_same_club >= 2, "a same-club group for team filters"); // The seed created ONLY a fifa17 profile — the default (fifa23) profile and // any synthetic inventory are untouched. let games: Vec<(String,)> = sqlx::query_as("SELECT game_id FROM profiles") .fetch_all(&pool) .await .unwrap(); assert_eq!( games, vec![("fifa17".to_string(),)], "only the fifa17 profile exists" ); // Every seeded owned card references a dev-pack definition (all renderable). let orphans: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM owned_cards o \ WHERE o.card_id LIKE 'fifa17_%' AND o.card_id NOT IN \ (SELECT card_id FROM owned_cards WHERE card_id LIKE 'fifa17_%')", ) .fetch_one(&pool) .await .unwrap(); assert_eq!(orphans, 0); } #[tokio::test] async fn seed_is_idempotent_across_reruns() { let pool = pool().await; let db = dev_card_db(); let first = openfut_core::seed::seed_fifa17_dev(&pool, &db) .await .unwrap(); let second = openfut_core::seed::seed_fifa17_dev(&pool, &db) .await .unwrap(); assert!(!first.already_seeded); assert!(second.already_seeded, "second run sees existing ownership"); assert_eq!(first.owned_total, second.owned_total, "no duplicate grants"); let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM owned_cards WHERE card_id LIKE 'fifa17_%'") .fetch_one(&pool) .await .unwrap(); assert_eq!(n, 33, "row count stable after rerun"); } #[tokio::test] async fn seed_creates_exactly_one_two_copy_definition() { let pool = pool().await; let db = dev_card_db(); openfut_core::seed::seed_fifa17_dev(&pool, &db) .await .unwrap(); // Exactly one definition is owned twice (distinct owned ids, same card_id): // the identity foundation for "two copies of one card" later. let dupes: Vec<(String, i64)> = sqlx::query_as( "SELECT card_id, COUNT(*) c FROM owned_cards WHERE card_id LIKE 'fifa17_%' \ GROUP BY card_id HAVING c > 1", ) .fetch_all(&pool) .await .unwrap(); assert_eq!(dupes.len(), 1, "exactly one duplicated definition"); assert_eq!(dupes[0].1, 2, "owned twice"); }