feat(seed): curated FIFA17 dev content pack + opt-in game-scoped ownership seed
This commit is contained in:
@@ -0,0 +1,546 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "fifa17_101490",
|
||||||
|
"name": "Conor Casey",
|
||||||
|
"overall": 64,
|
||||||
|
"position": "ST",
|
||||||
|
"nation": "United States",
|
||||||
|
"league": "MLS",
|
||||||
|
"club": "Columbus Crew SC",
|
||||||
|
"pace": 43,
|
||||||
|
"shooting": 65,
|
||||||
|
"passing": 52,
|
||||||
|
"dribbling": 60,
|
||||||
|
"defending": 33,
|
||||||
|
"physical": 72,
|
||||||
|
"rarity": "bronze",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_101880",
|
||||||
|
"name": "Rob Green",
|
||||||
|
"overall": 74,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "England",
|
||||||
|
"league": "EFL Championship",
|
||||||
|
"club": "Leeds United",
|
||||||
|
"pace": 78,
|
||||||
|
"shooting": 70,
|
||||||
|
"passing": 62,
|
||||||
|
"dribbling": 77,
|
||||||
|
"defending": 47,
|
||||||
|
"physical": 71,
|
||||||
|
"rarity": "silver",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_102356",
|
||||||
|
"name": "Markus Feulner",
|
||||||
|
"overall": 74,
|
||||||
|
"position": "CM",
|
||||||
|
"nation": "Germany",
|
||||||
|
"league": "Bundesliga",
|
||||||
|
"club": "Augsburg",
|
||||||
|
"pace": 58,
|
||||||
|
"shooting": 70,
|
||||||
|
"passing": 75,
|
||||||
|
"dribbling": 71,
|
||||||
|
"defending": 66,
|
||||||
|
"physical": 71,
|
||||||
|
"rarity": "silver",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_102593",
|
||||||
|
"name": "Craig Woodman",
|
||||||
|
"overall": 64,
|
||||||
|
"position": "LB",
|
||||||
|
"nation": "England",
|
||||||
|
"league": "EFL League Two",
|
||||||
|
"club": "Exeter City",
|
||||||
|
"pace": 66,
|
||||||
|
"shooting": 45,
|
||||||
|
"passing": 58,
|
||||||
|
"dribbling": 60,
|
||||||
|
"defending": 62,
|
||||||
|
"physical": 65,
|
||||||
|
"rarity": "bronze",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_105046",
|
||||||
|
"name": "Anders Østli",
|
||||||
|
"overall": 64,
|
||||||
|
"position": "CB",
|
||||||
|
"nation": "Norway",
|
||||||
|
"league": "Tippeligaen",
|
||||||
|
"club": "Sarpsborg 08 FF",
|
||||||
|
"pace": 54,
|
||||||
|
"shooting": 46,
|
||||||
|
"passing": 54,
|
||||||
|
"dribbling": 52,
|
||||||
|
"defending": 62,
|
||||||
|
"physical": 75,
|
||||||
|
"rarity": "bronze",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_107298",
|
||||||
|
"name": "Yohann Pelé",
|
||||||
|
"overall": 74,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "France",
|
||||||
|
"league": "Ligue 1",
|
||||||
|
"club": "O. de Marseille",
|
||||||
|
"pace": 75,
|
||||||
|
"shooting": 74,
|
||||||
|
"passing": 72,
|
||||||
|
"dribbling": 70,
|
||||||
|
"defending": 49,
|
||||||
|
"physical": 76,
|
||||||
|
"rarity": "silver",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_107713",
|
||||||
|
"name": "Tom Starke",
|
||||||
|
"overall": 74,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "Germany",
|
||||||
|
"league": "Bundesliga",
|
||||||
|
"club": "Bayern",
|
||||||
|
"pace": 76,
|
||||||
|
"shooting": 73,
|
||||||
|
"passing": 59,
|
||||||
|
"dribbling": 72,
|
||||||
|
"defending": 39,
|
||||||
|
"physical": 76,
|
||||||
|
"rarity": "silver",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_110020",
|
||||||
|
"name": "Sergio Pelegrín",
|
||||||
|
"overall": 74,
|
||||||
|
"position": "CB",
|
||||||
|
"nation": "Spain",
|
||||||
|
"league": "LaLiga 1 I 2 I 3",
|
||||||
|
"club": "Elche CF",
|
||||||
|
"pace": 45,
|
||||||
|
"shooting": 32,
|
||||||
|
"passing": 52,
|
||||||
|
"dribbling": 49,
|
||||||
|
"defending": 75,
|
||||||
|
"physical": 76,
|
||||||
|
"rarity": "silver",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_110026",
|
||||||
|
"name": "Cani",
|
||||||
|
"overall": 74,
|
||||||
|
"position": "LM",
|
||||||
|
"nation": "Spain",
|
||||||
|
"league": "LaLiga 1 I 2 I 3",
|
||||||
|
"club": "Real Zaragoza",
|
||||||
|
"pace": 67,
|
||||||
|
"shooting": 72,
|
||||||
|
"passing": 73,
|
||||||
|
"dribbling": 78,
|
||||||
|
"defending": 45,
|
||||||
|
"physical": 61,
|
||||||
|
"rarity": "silver",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_11811",
|
||||||
|
"name": "Paul Green",
|
||||||
|
"overall": 64,
|
||||||
|
"position": "CM",
|
||||||
|
"nation": "Republic of Ireland",
|
||||||
|
"league": "EFL League One",
|
||||||
|
"club": "Oldham Athletic",
|
||||||
|
"pace": 65,
|
||||||
|
"shooting": 58,
|
||||||
|
"passing": 62,
|
||||||
|
"dribbling": 63,
|
||||||
|
"defending": 62,
|
||||||
|
"physical": 68,
|
||||||
|
"rarity": "bronze",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_139720",
|
||||||
|
"name": "Vincent Kompany",
|
||||||
|
"overall": 86,
|
||||||
|
"position": "CB",
|
||||||
|
"nation": "Belgium",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester City",
|
||||||
|
"pace": 69,
|
||||||
|
"shooting": 54,
|
||||||
|
"passing": 62,
|
||||||
|
"dribbling": 65,
|
||||||
|
"defending": 86,
|
||||||
|
"physical": 81,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_146562",
|
||||||
|
"name": "Santi Cazorla",
|
||||||
|
"overall": 86,
|
||||||
|
"position": "CAM",
|
||||||
|
"nation": "Spain",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Arsenal",
|
||||||
|
"pace": 71,
|
||||||
|
"shooting": 78,
|
||||||
|
"passing": 85,
|
||||||
|
"dribbling": 86,
|
||||||
|
"defending": 57,
|
||||||
|
"physical": 64,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_153079",
|
||||||
|
"name": "Sergio Agüero",
|
||||||
|
"overall": 89,
|
||||||
|
"position": "ST",
|
||||||
|
"nation": "Argentina",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester City",
|
||||||
|
"pace": 89,
|
||||||
|
"shooting": 88,
|
||||||
|
"passing": 75,
|
||||||
|
"dribbling": 89,
|
||||||
|
"defending": 23,
|
||||||
|
"physical": 70,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_158023",
|
||||||
|
"name": "Lionel Messi",
|
||||||
|
"overall": 93,
|
||||||
|
"position": "RW",
|
||||||
|
"nation": "Argentina",
|
||||||
|
"league": "LaLiga Santander",
|
||||||
|
"club": "FC Barcelona",
|
||||||
|
"pace": 89,
|
||||||
|
"shooting": 90,
|
||||||
|
"passing": 86,
|
||||||
|
"dribbling": 96,
|
||||||
|
"defending": 26,
|
||||||
|
"physical": 61,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_162895",
|
||||||
|
"name": "Cesc Fàbregas",
|
||||||
|
"overall": 86,
|
||||||
|
"position": "CM",
|
||||||
|
"nation": "Spain",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Chelsea",
|
||||||
|
"pace": 63,
|
||||||
|
"shooting": 77,
|
||||||
|
"passing": 89,
|
||||||
|
"dribbling": 81,
|
||||||
|
"defending": 61,
|
||||||
|
"physical": 64,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_163705",
|
||||||
|
"name": "Steve Mandanda",
|
||||||
|
"overall": 85,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "France",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Crystal Palace",
|
||||||
|
"pace": 86,
|
||||||
|
"shooting": 80,
|
||||||
|
"passing": 79,
|
||||||
|
"dribbling": 85,
|
||||||
|
"defending": 49,
|
||||||
|
"physical": 81,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_165229",
|
||||||
|
"name": "Laurent Koscielny",
|
||||||
|
"overall": 85,
|
||||||
|
"position": "CB",
|
||||||
|
"nation": "France",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Arsenal",
|
||||||
|
"pace": 78,
|
||||||
|
"shooting": 40,
|
||||||
|
"passing": 62,
|
||||||
|
"dribbling": 65,
|
||||||
|
"defending": 85,
|
||||||
|
"physical": 78,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_167948",
|
||||||
|
"name": "Hugo Lloris",
|
||||||
|
"overall": 88,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "France",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Spurs",
|
||||||
|
"pace": 87,
|
||||||
|
"shooting": 87,
|
||||||
|
"passing": 68,
|
||||||
|
"dribbling": 90,
|
||||||
|
"defending": 64,
|
||||||
|
"physical": 82,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_168542",
|
||||||
|
"name": "David Silva",
|
||||||
|
"overall": 87,
|
||||||
|
"position": "CAM",
|
||||||
|
"nation": "Spain",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester City",
|
||||||
|
"pace": 68,
|
||||||
|
"shooting": 72,
|
||||||
|
"passing": 87,
|
||||||
|
"dribbling": 87,
|
||||||
|
"defending": 32,
|
||||||
|
"physical": 58,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_176580",
|
||||||
|
"name": "Luis Suárez",
|
||||||
|
"overall": 92,
|
||||||
|
"position": "ST",
|
||||||
|
"nation": "Uruguay",
|
||||||
|
"league": "LaLiga Santander",
|
||||||
|
"club": "FC Barcelona",
|
||||||
|
"pace": 82,
|
||||||
|
"shooting": 90,
|
||||||
|
"passing": 79,
|
||||||
|
"dribbling": 87,
|
||||||
|
"defending": 42,
|
||||||
|
"physical": 79,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_176635",
|
||||||
|
"name": "Mesut Özil",
|
||||||
|
"overall": 89,
|
||||||
|
"position": "CAM",
|
||||||
|
"nation": "Germany",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Arsenal",
|
||||||
|
"pace": 72,
|
||||||
|
"shooting": 74,
|
||||||
|
"passing": 86,
|
||||||
|
"dribbling": 86,
|
||||||
|
"defending": 24,
|
||||||
|
"physical": 58,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_177388",
|
||||||
|
"name": "Dimitri Payet",
|
||||||
|
"overall": 86,
|
||||||
|
"position": "LM",
|
||||||
|
"nation": "France",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "West Ham",
|
||||||
|
"pace": 77,
|
||||||
|
"shooting": 78,
|
||||||
|
"passing": 87,
|
||||||
|
"dribbling": 87,
|
||||||
|
"defending": 42,
|
||||||
|
"physical": 70,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_183277",
|
||||||
|
"name": "Eden Hazard",
|
||||||
|
"overall": 88,
|
||||||
|
"position": "LM",
|
||||||
|
"nation": "Belgium",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Chelsea",
|
||||||
|
"pace": 90,
|
||||||
|
"shooting": 81,
|
||||||
|
"passing": 82,
|
||||||
|
"dribbling": 91,
|
||||||
|
"defending": 32,
|
||||||
|
"physical": 64,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_184941",
|
||||||
|
"name": "Alexis Sánchez",
|
||||||
|
"overall": 87,
|
||||||
|
"position": "LW",
|
||||||
|
"nation": "Chile",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Arsenal",
|
||||||
|
"pace": 86,
|
||||||
|
"shooting": 82,
|
||||||
|
"passing": 79,
|
||||||
|
"dribbling": 88,
|
||||||
|
"defending": 39,
|
||||||
|
"physical": 74,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_190871",
|
||||||
|
"name": "Neymar",
|
||||||
|
"overall": 92,
|
||||||
|
"position": "LW",
|
||||||
|
"nation": "Brazil",
|
||||||
|
"league": "LaLiga Santander",
|
||||||
|
"club": "FC Barcelona",
|
||||||
|
"pace": 91,
|
||||||
|
"shooting": 84,
|
||||||
|
"passing": 78,
|
||||||
|
"dribbling": 95,
|
||||||
|
"defending": 30,
|
||||||
|
"physical": 56,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_192119",
|
||||||
|
"name": "Thibaut Courtois",
|
||||||
|
"overall": 89,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "Belgium",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Chelsea",
|
||||||
|
"pace": 84,
|
||||||
|
"shooting": 91,
|
||||||
|
"passing": 69,
|
||||||
|
"dribbling": 89,
|
||||||
|
"defending": 48,
|
||||||
|
"physical": 86,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_192985",
|
||||||
|
"name": "Kevin De Bruyne",
|
||||||
|
"overall": 88,
|
||||||
|
"position": "CAM",
|
||||||
|
"nation": "Belgium",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester City",
|
||||||
|
"pace": 77,
|
||||||
|
"shooting": 83,
|
||||||
|
"passing": 86,
|
||||||
|
"dribbling": 84,
|
||||||
|
"defending": 40,
|
||||||
|
"physical": 75,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_193080",
|
||||||
|
"name": "David De Gea",
|
||||||
|
"overall": 90,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "Spain",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester Utd",
|
||||||
|
"pace": 88,
|
||||||
|
"shooting": 85,
|
||||||
|
"passing": 87,
|
||||||
|
"dribbling": 90,
|
||||||
|
"defending": 56,
|
||||||
|
"physical": 85,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_195864",
|
||||||
|
"name": "Paul Pogba",
|
||||||
|
"overall": 88,
|
||||||
|
"position": "CM",
|
||||||
|
"nation": "France",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester Utd",
|
||||||
|
"pace": 77,
|
||||||
|
"shooting": 80,
|
||||||
|
"passing": 83,
|
||||||
|
"dribbling": 87,
|
||||||
|
"defending": 72,
|
||||||
|
"physical": 87,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_20801",
|
||||||
|
"name": "Cristiano Ronaldo",
|
||||||
|
"overall": 94,
|
||||||
|
"position": "LW",
|
||||||
|
"nation": "Portugal",
|
||||||
|
"league": "LaLiga Santander",
|
||||||
|
"club": "Real Madrid",
|
||||||
|
"pace": 92,
|
||||||
|
"shooting": 92,
|
||||||
|
"passing": 81,
|
||||||
|
"dribbling": 91,
|
||||||
|
"defending": 33,
|
||||||
|
"physical": 80,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_41236",
|
||||||
|
"name": "Zlatan Ibrahimović",
|
||||||
|
"overall": 90,
|
||||||
|
"position": "ST",
|
||||||
|
"nation": "Sweden",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Manchester Utd",
|
||||||
|
"pace": 72,
|
||||||
|
"shooting": 90,
|
||||||
|
"passing": 81,
|
||||||
|
"dribbling": 85,
|
||||||
|
"defending": 31,
|
||||||
|
"physical": 86,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fifa17_48940",
|
||||||
|
"name": "Petr Čech",
|
||||||
|
"overall": 88,
|
||||||
|
"position": "GK",
|
||||||
|
"nation": "Czech Republic",
|
||||||
|
"league": "Premier League",
|
||||||
|
"club": "Arsenal",
|
||||||
|
"pace": 83,
|
||||||
|
"shooting": 90,
|
||||||
|
"passing": 77,
|
||||||
|
"dribbling": 85,
|
||||||
|
"defending": 48,
|
||||||
|
"physical": 85,
|
||||||
|
"rarity": "gold",
|
||||||
|
"image_path": null
|
||||||
|
}
|
||||||
|
]
|
||||||
+5
-1
@@ -42,7 +42,11 @@ pub struct AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
||||||
let card_db = Arc::new(CardDb::load(&cfg.data_dir)?);
|
let mut card_db = CardDb::load(&cfg.data_dir)?;
|
||||||
|
for game in &cfg.dev_content_games {
|
||||||
|
card_db.load_game_dev(&cfg.data_dir, game)?;
|
||||||
|
}
|
||||||
|
let card_db = Arc::new(card_db);
|
||||||
let pack_defs = Arc::new(load_pack_definitions(&cfg.data_dir)?);
|
let pack_defs = Arc::new(load_pack_definitions(&cfg.data_dir)?);
|
||||||
let obj_defs = Arc::new(load_objective_definitions(&cfg.data_dir)?);
|
let obj_defs = Arc::new(load_objective_definitions(&cfg.data_dir)?);
|
||||||
let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?);
|
let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ pub struct Config {
|
|||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub data_dir: String,
|
pub data_dir: String,
|
||||||
pub max_connections: u32,
|
pub max_connections: u32,
|
||||||
|
/// Games whose opt-in development content pack (`data/games/<game>/dev/`) is
|
||||||
|
/// loaded IN ADDITION to the default `data/cards` catalog. Empty by default —
|
||||||
|
/// default/test content is never affected unless a game is named here.
|
||||||
|
pub dev_content_games: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -19,6 +23,16 @@ impl Config {
|
|||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
.unwrap_or(5),
|
.unwrap_or(5),
|
||||||
|
dev_content_games: std::env::var("OPENFUT_DEV_CONTENT_GAMES")
|
||||||
|
.ok()
|
||||||
|
.map(|v| {
|
||||||
|
v.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub async fn build_app(pool: db::Pool, data_dir: &str) -> Result<Router> {
|
|||||||
database_url: "sqlite::memory:".into(),
|
database_url: "sqlite::memory:".into(),
|
||||||
data_dir: data_dir.to_string(),
|
data_dir: data_dir.to_string(),
|
||||||
max_connections: 1,
|
max_connections: 1,
|
||||||
|
dev_content_games: Vec::new(),
|
||||||
};
|
};
|
||||||
app::build(pool, cfg).await
|
app::build(pool, cfg).await
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -16,6 +16,20 @@ async fn main() -> Result<()> {
|
|||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cfg = config::Config::from_env()?;
|
let cfg = config::Config::from_env()?;
|
||||||
|
|
||||||
|
// Opt-in dev subcommand: `openfut-core seed-dev` seeds the FIFA 17 dev
|
||||||
|
// profile/club from the dev content pack, prints a coverage report, and
|
||||||
|
// exits. Normal server startup NEVER seeds dev inventory.
|
||||||
|
if std::env::args().nth(1).as_deref() == Some("seed-dev") {
|
||||||
|
let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?;
|
||||||
|
db::run_migrations(&pool).await?;
|
||||||
|
let mut card_db = openfut_core::services::card_db::CardDb::load(&cfg.data_dir)?;
|
||||||
|
card_db.load_game_dev(&cfg.data_dir, seed::FIFA17_GAME)?;
|
||||||
|
let report = seed::seed_fifa17_dev(&pool, &card_db).await?;
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
info!("OpenFUT Core starting on {}", cfg.listen_addr);
|
info!("OpenFUT Core starting on {}", cfg.listen_addr);
|
||||||
|
|
||||||
let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?;
|
let pool = db::init_pool(&cfg.database_url, cfg.max_connections).await?;
|
||||||
|
|||||||
+186
-1
@@ -1,6 +1,23 @@
|
|||||||
use crate::{db::Pool, error::AppResult, models::pack::PackDefinition, services::pack as pack_svc};
|
use crate::{
|
||||||
|
db::Pool,
|
||||||
|
error::AppResult,
|
||||||
|
models::{card::Quality, club::Club, pack::PackDefinition},
|
||||||
|
services::{card_db::CardDb, club as club_svc, pack as pack_svc, profile as profile_svc},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
/// The game whose dev content + inventory this seeds.
|
||||||
|
pub const FIFA17_GAME: &str = "fifa17";
|
||||||
|
/// Deterministic owned-instance id prefix, so re-running the seed is idempotent
|
||||||
|
/// (INSERT OR IGNORE on a stable id) rather than minting duplicate ownership.
|
||||||
|
const DEV_OWNED_PREFIX: &str = "fdev-";
|
||||||
|
/// Fixed grant timestamp — the seed is deterministic, not wall-clock dependent.
|
||||||
|
const DEV_ACQUIRED_AT: &str = "2026-08-11T00:00:00Z";
|
||||||
|
/// The client's My Squad page size (evidence: request `count=11`).
|
||||||
|
const MY_SQUAD_PAGE: usize = 11;
|
||||||
|
|
||||||
/// Seeds the market with NPC listings if empty.
|
/// Seeds the market with NPC listings if empty.
|
||||||
pub async fn maybe_seed(_pool: &Pool) -> AppResult<()> {
|
pub async fn maybe_seed(_pool: &Pool) -> AppResult<()> {
|
||||||
// Any one-time startup seeds go here.
|
// Any one-time startup seeds go here.
|
||||||
@@ -29,3 +46,171 @@ pub async fn grant_starter_pack(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────── FIFA 17 dev seed ─────────────────────────────
|
||||||
|
|
||||||
|
/// Coverage of the seeded FIFA 17 development inventory. Game-independent: it
|
||||||
|
/// counts quality tiers, positions and distinct entities, and whether the Gold
|
||||||
|
/// filter spans more than one page — everything the retail `/club` UI must
|
||||||
|
/// exercise. It carries NO FIFA wire ids (those are the adapter/host's runtime
|
||||||
|
/// concern; the seed never allocates them).
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DevSeedReport {
|
||||||
|
pub game_id: String,
|
||||||
|
/// True if the fifa17 club already owned dev cards (no new grants made).
|
||||||
|
pub already_seeded: bool,
|
||||||
|
pub definitions_available: usize,
|
||||||
|
pub owned_total: usize,
|
||||||
|
pub unique_definitions: usize,
|
||||||
|
pub gold: usize,
|
||||||
|
pub silver: usize,
|
||||||
|
pub bronze: usize,
|
||||||
|
pub positions: BTreeMap<String, usize>,
|
||||||
|
pub distinct_nations: usize,
|
||||||
|
pub distinct_leagues: usize,
|
||||||
|
pub distinct_clubs: usize,
|
||||||
|
pub max_same_club: usize,
|
||||||
|
/// Gold owned items exceed one page → the client must request a 2nd page.
|
||||||
|
pub gold_over_one_page: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opt-in development seed: create (if absent) a `game_id=fifa17` profile + club
|
||||||
|
/// and grant Core-owned instances of every dev-pack `CardDefinition` (ids
|
||||||
|
/// `fifa17_*`), plus one deliberate duplicate of a single definition (to exercise
|
||||||
|
/// two-copies-of-one-card identity later).
|
||||||
|
///
|
||||||
|
/// **Ownership only — no FIFA wire ids.** The FIFA 17 integer item id is minted
|
||||||
|
/// lazily by `Fifa17IdentityResolver` at request time, never here. This keeps the
|
||||||
|
/// boundary clean: Core owns "this profile owns this card"; the adapter owns
|
||||||
|
/// "this owned item is wire id N".
|
||||||
|
///
|
||||||
|
/// Idempotent: owned ids are deterministic (`fdev-<card_id>`), inserted with
|
||||||
|
/// `INSERT OR IGNORE`, so re-running grants nothing new. The default profile
|
||||||
|
/// (`fifa23`/no-header) and any existing synthetic inventory are never touched.
|
||||||
|
pub async fn seed_fifa17_dev(pool: &Pool, card_db: &CardDb) -> AppResult<DevSeedReport> {
|
||||||
|
// The dev definitions are exactly the game-namespaced ids in the catalog.
|
||||||
|
let mut defs: Vec<&crate::models::card::CardDefinition> = card_db
|
||||||
|
.cards
|
||||||
|
.values()
|
||||||
|
.filter(|c| c.id.starts_with("fifa17_"))
|
||||||
|
.collect();
|
||||||
|
defs.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
|
||||||
|
// Ensure the fifa17-scoped profile + club exist (single-profile-per-game).
|
||||||
|
let profile = match profile_svc::get_active_profile(pool, FIFA17_GAME).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => profile_svc::create_profile(pool, "OpenFUT Dev (FIFA17)", FIFA17_GAME).await?,
|
||||||
|
};
|
||||||
|
let club = match club_svc::get_club_by_profile(pool, &profile.id).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => {
|
||||||
|
let c = Club::new(&profile.id, "OpenFUT Dev FC", 100_000);
|
||||||
|
club_svc::create_club(pool, &c).await?;
|
||||||
|
c
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let prior: i64 = sqlx::query_scalar(
|
||||||
|
"SELECT COUNT(*) FROM owned_cards WHERE club_id = ? AND card_id LIKE 'fifa17_%'",
|
||||||
|
)
|
||||||
|
.bind(&club.id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
let already_seeded = prior > 0;
|
||||||
|
|
||||||
|
// Grant one instance per definition; INSERT OR IGNORE keeps reruns idempotent.
|
||||||
|
for def in &defs {
|
||||||
|
grant_owned(
|
||||||
|
pool,
|
||||||
|
&format!("{DEV_OWNED_PREFIX}{}", def.id),
|
||||||
|
&club.id,
|
||||||
|
&def.id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
// One deliberate duplicate of the first (lexicographic) definition → two
|
||||||
|
// owned copies of one card sharing a definition but distinct owned ids.
|
||||||
|
if let Some(first) = defs.first() {
|
||||||
|
grant_owned(
|
||||||
|
pool,
|
||||||
|
&format!("{DEV_OWNED_PREFIX}{}-b", first.id),
|
||||||
|
&club.id,
|
||||||
|
&first.id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let report = dev_coverage(pool, card_db, &club.id, already_seeded, defs.len()).await?;
|
||||||
|
info!(
|
||||||
|
"seeded fifa17 dev inventory: {} owned ({} gold) over club {}",
|
||||||
|
report.owned_total, report.gold, club.id
|
||||||
|
);
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn grant_owned(pool: &Pool, owned_id: &str, club_id: &str, card_id: &str) -> AppResult<()> {
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT OR IGNORE INTO owned_cards \
|
||||||
|
(id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at) \
|
||||||
|
VALUES (?, ?, ?, 0, NULL, ?)",
|
||||||
|
)
|
||||||
|
.bind(owned_id)
|
||||||
|
.bind(club_id)
|
||||||
|
.bind(card_id)
|
||||||
|
.bind(DEV_ACQUIRED_AT)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the coverage report from the club's owned dev cards joined to `card_db`.
|
||||||
|
async fn dev_coverage(
|
||||||
|
pool: &Pool,
|
||||||
|
card_db: &CardDb,
|
||||||
|
club_id: &str,
|
||||||
|
already_seeded: bool,
|
||||||
|
definitions_available: usize,
|
||||||
|
) -> AppResult<DevSeedReport> {
|
||||||
|
let card_ids: Vec<String> = sqlx::query_scalar(
|
||||||
|
"SELECT card_id FROM owned_cards WHERE club_id = ? AND card_id LIKE 'fifa17_%'",
|
||||||
|
)
|
||||||
|
.bind(club_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
let (mut gold, mut silver, mut bronze) = (0usize, 0usize, 0usize);
|
||||||
|
let mut positions: BTreeMap<String, usize> = BTreeMap::new();
|
||||||
|
let mut nations = std::collections::BTreeSet::new();
|
||||||
|
let mut leagues = std::collections::BTreeSet::new();
|
||||||
|
let mut club_counts: BTreeMap<String, usize> = BTreeMap::new();
|
||||||
|
let mut unique = std::collections::BTreeSet::new();
|
||||||
|
for card_id in &card_ids {
|
||||||
|
unique.insert(card_id.clone());
|
||||||
|
if let Some(def) = card_db.get(card_id) {
|
||||||
|
match Quality::from_overall(def.overall) {
|
||||||
|
Quality::Gold => gold += 1,
|
||||||
|
Quality::Silver => silver += 1,
|
||||||
|
Quality::Bronze => bronze += 1,
|
||||||
|
}
|
||||||
|
*positions.entry(def.position.clone()).or_default() += 1;
|
||||||
|
nations.insert(def.nation.clone());
|
||||||
|
leagues.insert(def.league.clone());
|
||||||
|
*club_counts.entry(def.club.clone()).or_default() += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(DevSeedReport {
|
||||||
|
game_id: FIFA17_GAME.to_string(),
|
||||||
|
already_seeded,
|
||||||
|
definitions_available,
|
||||||
|
owned_total: card_ids.len(),
|
||||||
|
unique_definitions: unique.len(),
|
||||||
|
gold,
|
||||||
|
silver,
|
||||||
|
bronze,
|
||||||
|
positions,
|
||||||
|
distinct_nations: nations.len(),
|
||||||
|
distinct_leagues: leagues.len(),
|
||||||
|
distinct_clubs: club_counts.len(),
|
||||||
|
max_same_club: club_counts.values().copied().max().unwrap_or(0),
|
||||||
|
gold_over_one_page: gold > MY_SQUAD_PAGE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ impl CardDb {
|
|||||||
Ok(Self { cards })
|
Ok(Self { cards })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Merge a game's **opt-in development content pack** from
|
||||||
|
/// `{data_dir}/games/{game}/dev/cards.json` (a single `CardDefinition[]`).
|
||||||
|
/// This is NOT read by [`CardDb::load`]; it is loaded only when a game is
|
||||||
|
/// explicitly named in `Config::dev_content_games`, so default content stays
|
||||||
|
/// untouched. Returns the number of definitions merged. A missing file is an
|
||||||
|
/// error (opt-in means the pack is expected to exist).
|
||||||
|
pub fn load_game_dev(&mut self, data_dir: &str, game: &str) -> Result<usize> {
|
||||||
|
let path = Path::new(data_dir)
|
||||||
|
.join("games")
|
||||||
|
.join(game)
|
||||||
|
.join("dev")
|
||||||
|
.join("cards.json");
|
||||||
|
let content = std::fs::read_to_string(&path)
|
||||||
|
.with_context(|| format!("reading dev content pack {path:?}"))?;
|
||||||
|
let batch: Vec<CardDefinition> =
|
||||||
|
serde_json::from_str(&content).with_context(|| format!("parsing {path:?}"))?;
|
||||||
|
let n = batch.len();
|
||||||
|
for card in batch {
|
||||||
|
self.cards.insert(card.id.clone(), card);
|
||||||
|
}
|
||||||
|
tracing::info!("Loaded {} dev card definitions for game '{}'", n, game);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: &str) -> Option<&CardDefinition> {
|
pub fn get(&self, id: &str) -> Option<&CardDefinition> {
|
||||||
self.cards.get(id)
|
self.cards.get(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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