feat(seed): curated FIFA17 dev content pack + opt-in game-scoped ownership seed
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
//! 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::<i64>().is_err(),
|
||||
"nation must be a name, got {:?}",
|
||||
c.nation
|
||||
);
|
||||
assert!(
|
||||
c.league.parse::<i64>().is_err(),
|
||||
"league must be a name: {:?}",
|
||||
c.league
|
||||
);
|
||||
assert!(
|
||||
c.club.parse::<i64>().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");
|
||||
}
|
||||
Reference in New Issue
Block a user