feat(content): production content-pack loader + referenced-definition preflight
Production real-profile content is loaded via an explicit path, NOT the dev-only OPENFUT_DEV_CONTENT_GAMES gate: - Config.content_packs from env OPENFUT_CONTENT_PACKS (comma-sep file paths). - CardDb::load_pack(path): merge an explicit CardDefinition[] production pack. - app::build loads dev packs then production packs. Preflight (app::build, always on): every owned_cards.card_id MUST resolve to a loaded CardDefinition. A real profile with owned players but even ONE missing definition fails LOUDLY instead of silently serving an empty /collection; an empty owned_cards table (fresh DB / tests) passes. 2 preflight integration tests (missing def fails, loaded def passes). clippy -D warnings clean; full suite 151 tests green.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
//! Content preflight: a real profile with owned players but a missing
|
||||
//! CardDefinition must fail startup LOUDLY, never serve a silent empty club.
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use openfut_core::services::card_db::CardDb;
|
||||
use tower::ServiceExt;
|
||||
|
||||
async fn fresh_pool() -> sqlx::SqlitePool {
|
||||
let p = sqlx::sqlite::SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
.expect("in-memory sqlite");
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&p)
|
||||
.await
|
||||
.expect("migrations");
|
||||
p
|
||||
}
|
||||
|
||||
async fn create_profile(app: &axum::Router) {
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/auth/local")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(r#"{"username":"CAGE"}"#))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
StatusCode::OK,
|
||||
"auth/local should create a profile+club"
|
||||
);
|
||||
}
|
||||
|
||||
async fn insert_owned(pool: &sqlx::SqlitePool, id: &str, club_id: &str, card_id: &str) {
|
||||
sqlx::query(
|
||||
"INSERT INTO owned_cards (id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at) \
|
||||
VALUES (?, ?, ?, 0, NULL, ?)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(club_id)
|
||||
.bind(card_id)
|
||||
.bind("2026-01-01T00:00:00Z")
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preflight_fails_on_owned_card_missing_definition() {
|
||||
let pool = fresh_pool().await;
|
||||
// first build is fine: no owned cards yet.
|
||||
let app = openfut_core::build_app(pool.clone(), "data").await.unwrap();
|
||||
create_profile(&app).await;
|
||||
let club: String = sqlx::query_scalar("SELECT id FROM clubs LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_owned(&pool, "oc-bogus", &club, "fifa17_definitely_missing_999999").await;
|
||||
|
||||
// second build must now fail preflight: one owned card references a def that
|
||||
// is not loaded — must not silently serve an empty collection.
|
||||
let err = openfut_core::build_app(pool.clone(), "data")
|
||||
.await
|
||||
.expect_err("preflight must fail on a missing definition");
|
||||
let msg = format!("{err:#}");
|
||||
assert!(
|
||||
msg.contains("content preflight failed"),
|
||||
"unexpected error: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preflight_passes_when_owned_card_definition_is_loaded() {
|
||||
let pool = fresh_pool().await;
|
||||
// pick a definition that IS in the default data/cards catalog.
|
||||
let valid_id = CardDb::load("data")
|
||||
.unwrap()
|
||||
.all()
|
||||
.first()
|
||||
.map(|c| c.id.clone())
|
||||
.expect("data/cards must be non-empty");
|
||||
|
||||
let app = openfut_core::build_app(pool.clone(), "data").await.unwrap();
|
||||
create_profile(&app).await;
|
||||
let club: String = sqlx::query_scalar("SELECT id FROM clubs LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_owned(&pool, "oc-valid", &club, &valid_id).await;
|
||||
|
||||
let _app = openfut_core::build_app(pool.clone(), "data")
|
||||
.await
|
||||
.expect("preflight passes when the owned card's definition is loaded");
|
||||
}
|
||||
Reference in New Issue
Block a user