Files
OpenFUT-Core/tests/integration_test.rs
T
funman300 4ae1081aa9 feat: Phase 3 — polish & settings complete
Card pool:
- TOTW cards (5): overall 88-92, rarity "totw"
- Hero cards (5): overall 85-88, rarity "hero"
- Icon cards (5): overall 93-95, rarity "icon" (permanent, non-loan)

Packs:
- TOTW Pack (30,000 coins): 5 TOTW cards guaranteed
- Icon Pack (50,000 coins): 3 icon cards guaranteed
- Hero Pack (20,000 coins): 5 hero cards guaranteed

Objectives:
- Milestone objectives (6): win 10/50, score 100/500 goals, 10 SBCs, 25 packs

Formations:
- GET /formations returns 12 valid formation strings

Multiple named squads (#20):
- POST /squad with no squad_id always creates a new squad
- POST /squad with squad_id updates that specific squad
- GET /squads: list all squads for the club (metadata only)
- GET /squads/🆔 squad with players + chemistry
- DELETE /squads/🆔 remove a squad

Per-position goal stats (#41):
- Migration 0003: position_goals table
- POST /matches/result accepts optional goal_positions: ["ST","CAM",...]
- GET /statistics now includes position_goals map

Settings (#44-46):
- GET /settings: { difficulty, preferred_formation } with defaults
- PUT /settings: upsert any key-value combination

Draft mode skeleton (#38):
- GET /draft/squad?difficulty=... returns a randomly generated 11-player squad

Integration tests:
- 9 new tests covering: formations, pack buy/open, SBC submit, settings R/W,
  multiple squads, draft endpoint, goal position tracking, win streak

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 15:46:20 -07:00

385 lines
12 KiB
Rust

use axum::{
body::Body,
http::{Request, StatusCode},
};
use serde_json::Value;
use tower::ServiceExt;
// We test by spinning up the full app against an in-memory SQLite database.
async fn build_test_app() -> axum::Router {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.connect("sqlite::memory:")
.await
.expect("in-memory sqlite");
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("migrations");
let data_dir = "data";
openfut_core::build_app(pool, data_dir)
.await
.expect("app build")
}
async fn auth(app: &axum::Router, username: &str) -> Value {
let payload = serde_json::json!({ "username": username });
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/auth/local")
.header("content-type", "application/json")
.body(Body::from(payload.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
}
async fn json_get(app: &axum::Router, uri: &str) -> (StatusCode, Value) {
let resp = app
.clone()
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
.unwrap();
let status = resp.status();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
(status, serde_json::from_slice(&body).unwrap())
}
async fn json_post(app: &axum::Router, uri: &str, payload: Value) -> (StatusCode, Value) {
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(payload.to_string()))
.unwrap(),
)
.await
.unwrap();
let status = resp.status();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
(status, serde_json::from_slice(&body).unwrap())
}
// ── Existing tests ───────────────────────────────────────────────────────────
#[tokio::test]
async fn test_health_endpoint() {
let app = build_test_app().await;
let (status, json) = json_get(&app, "/health").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["status"], "ok");
}
#[tokio::test]
async fn test_formations_endpoint() {
let app = build_test_app().await;
let (status, json) = json_get(&app, "/formations").await;
assert_eq!(status, StatusCode::OK);
assert!(json["formations"].is_array());
assert!(json["formations"].as_array().unwrap().len() >= 8);
}
#[tokio::test]
async fn test_auth_local_creates_profile_and_club() {
let app = build_test_app().await;
let json = auth(&app, "TestPlayer").await;
assert_eq!(json["profile"]["username"], "TestPlayer");
assert_eq!(json["club"]["name"], "OpenFUT FC");
assert_eq!(json["club"]["coins"], 5000);
}
#[tokio::test]
async fn test_profile_not_found_before_auth() {
let app = build_test_app().await;
let (status, _) = json_get(&app, "/profile").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_match_result_awards_coins() {
let app = build_test_app().await;
auth(&app, "MatchPlayer").await;
let payload = serde_json::json!({
"squad_id": "dummy-squad",
"opponent_name": "AI Club",
"goals_for": 3,
"goals_against": 1,
"mode": "squad_battles"
});
let (status, json) = json_post(&app, "/matches/result", payload).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["match_record"]["outcome"], "win");
assert!(json["coins_awarded"].as_i64().unwrap() > 0);
}
#[tokio::test]
async fn test_sbc_list_returns_definitions() {
let app = build_test_app().await;
let (status, json) = json_get(&app, "/sbc").await;
assert_eq!(status, StatusCode::OK);
assert!(json["sbcs"].is_array());
}
// ── Phase 3 new tests ────────────────────────────────────────────────────────
#[tokio::test]
async fn test_match_with_goal_positions_tracks_stats() {
let app = build_test_app().await;
auth(&app, "GoalTracker").await;
let payload = serde_json::json!({
"squad_id": "dummy-squad",
"opponent_name": "AI Club",
"goals_for": 3,
"goals_against": 0,
"mode": "squad_battles",
"goal_positions": ["ST", "ST", "CAM"]
});
let (status, _) = json_post(&app, "/matches/result", payload).await;
assert_eq!(status, StatusCode::OK);
let (status, stats) = json_get(&app, "/statistics").await;
assert_eq!(status, StatusCode::OK);
assert!(stats["position_goals"].is_object());
assert_eq!(stats["position_goals"]["ST"], 2);
assert_eq!(stats["position_goals"]["CAM"], 1);
}
#[tokio::test]
async fn test_buy_and_open_pack() {
let app = build_test_app().await;
auth(&app, "PackOpener").await;
// Starter gold_pack is granted on registration
let (status, packs_json) = json_get(&app, "/packs").await;
assert_eq!(status, StatusCode::OK);
let packs = packs_json["packs"].as_array().unwrap();
assert!(!packs.is_empty(), "starter pack should be in inventory");
let pack_id = packs[0]["pack_id"].as_str().unwrap();
let (status, open_json) = json_post(
&app,
&format!("/packs/open/{pack_id}"),
serde_json::json!({}),
)
.await;
assert_eq!(status, StatusCode::OK, "open failed: {open_json}");
assert!(!open_json["cards"].as_array().unwrap().is_empty());
let (status, coll) = json_get(&app, "/collection").await;
assert_eq!(status, StatusCode::OK);
assert!(!coll["collection"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn test_buy_bronze_pack_deducts_coins() {
let app = build_test_app().await;
auth(&app, "CoinSpender").await;
let (status, buy_json) = json_post(
&app,
"/packs/buy",
serde_json::json!({ "pack_definition_id": "bronze_pack" }),
)
.await;
assert_eq!(status, StatusCode::OK, "buy failed: {buy_json}");
let (_, club_json) = json_get(&app, "/club").await;
assert_eq!(club_json["coins"], 5000 - 400);
}
#[tokio::test]
async fn test_sbc_submit_with_bronze_cards() {
let app = build_test_app().await;
auth(&app, "SBCPlayer").await;
// Buy and open a bronze pack
let (s, _) = json_post(
&app,
"/packs/buy",
serde_json::json!({ "pack_definition_id": "bronze_pack" }),
)
.await;
assert_eq!(s, StatusCode::OK);
let (_, packs_json) = json_get(&app, "/packs").await;
let bronze_pack = packs_json["packs"]
.as_array()
.unwrap()
.iter()
.find(|p| p["definition_id"] == "bronze_pack")
.expect("bronze pack in inventory");
let pack_id = bronze_pack["pack_id"].as_str().unwrap();
let (s, open_json) = json_post(
&app,
&format!("/packs/open/{pack_id}"),
serde_json::json!({}),
)
.await;
assert_eq!(s, StatusCode::OK, "{open_json}");
// Collect bronze card IDs (overall ≤ 64)
let (_, coll) = json_get(&app, "/collection").await;
let bronze_ids: Vec<&str> = coll["collection"]
.as_array()
.unwrap()
.iter()
.filter(|c| c["card"]["overall"].as_i64().unwrap_or(99) <= 64)
.take(11)
.map(|c| c["owned_card_id"].as_str().unwrap())
.collect();
if bronze_ids.len() < 11 {
// Card pool variance — not enough bronze to fill SBC; skip gracefully
return;
}
let (s, result) = json_post(
&app,
"/sbc/submit",
serde_json::json!({
"sbc_id": "sbc_bronze_upgrade",
"owned_card_ids": bronze_ids
}),
)
.await;
assert_eq!(s, StatusCode::OK, "SBC submit: {result}");
assert_eq!(result["passed"], true);
// bronze_upgrade rewards a silver pack (coins = 0)
assert!(result["reward"].is_object());
}
#[tokio::test]
async fn test_settings_read_write() {
let app = build_test_app().await;
auth(&app, "SettingsUser").await;
let (status, defaults) = json_get(&app, "/settings").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(defaults["difficulty"], "beginner");
assert_eq!(defaults["preferred_formation"], "4-4-2");
let resp = app
.clone()
.oneshot(
Request::builder()
.method("PUT")
.uri("/settings")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"difficulty": "world_class",
"preferred_formation": "4-3-3"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let updated: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(updated["difficulty"], "world_class");
assert_eq!(updated["preferred_formation"], "4-3-3");
let (s, persisted) = json_get(&app, "/settings").await;
assert_eq!(s, StatusCode::OK);
assert_eq!(persisted["difficulty"], "world_class");
}
#[tokio::test]
async fn test_multiple_squads() {
let app = build_test_app().await;
auth(&app, "SquadManager").await;
let (s1, j1) = json_post(
&app,
"/squad",
serde_json::json!({ "name": "Attack Squad", "formation": "4-3-3", "players": [] }),
)
.await;
assert_eq!(s1, StatusCode::OK, "{j1}");
let squad1_id = j1["squad"]["id"].as_str().unwrap().to_string();
let (s2, j2) = json_post(
&app,
"/squad",
serde_json::json!({ "name": "Defense Squad", "formation": "5-4-1", "players": [] }),
)
.await;
assert_eq!(s2, StatusCode::OK, "{j2}");
let squad2_id = j2["squad"]["id"].as_str().unwrap().to_string();
assert_ne!(squad1_id, squad2_id);
let (status, squads_json) = json_get(&app, "/squads").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(squads_json["squads"].as_array().unwrap().len(), 2);
let (status, sq1) = json_get(&app, &format!("/squads/{squad1_id}")).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(sq1["squad"]["name"], "Attack Squad");
}
#[tokio::test]
async fn test_draft_squad_endpoint() {
let app = build_test_app().await;
auth(&app, "DraftPlayer").await;
let (status, json) = json_get(&app, "/draft/squad?difficulty=beginner").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["mode"], "draft");
assert_eq!(json["difficulty"], "beginner");
assert!(json["cards"].is_array());
}
#[tokio::test]
async fn test_win_streak_tracking() {
let app = build_test_app().await;
auth(&app, "StreakPlayer").await;
for _ in 0..3 {
let (s, _) = json_post(
&app,
"/matches/result",
serde_json::json!({
"squad_id": "dummy",
"opponent_name": "Bot",
"goals_for": 2,
"goals_against": 0,
"mode": "squad_battles"
}),
)
.await;
assert_eq!(s, StatusCode::OK);
}
let (s, stats) = json_get(&app, "/statistics").await;
assert_eq!(s, StatusCode::OK);
assert_eq!(stats["win_streak"], 3);
assert_eq!(stats["best_win_streak"], 3);
}