Phase 11: FUT Champions mode and Division Rivals weekly rewards
CI / Build, lint & test (push) Failing after 1m33s
CI / Build, lint & test (push) Failing after 1m33s
FUT Champions (Weekend League): - POST /fut-champs/start — open a new 30-match week (one active session at a time) - GET /fut-champs — current session status with matches_remaining - POST /fut-champs/:id/result — record a match; auto-completes and computes tier at 30 - POST /fut-champs/:id/claim — claim coins + pack reward (idempotent guard) - GET /fut-champs/history — past sessions newest-first - 11 reward tiers: Elite (27+ wins, 50k coins + icon pack) down to Bronze 1 (0 wins, 250 coins) Division Rivals: - POST /rivals/claim-weekly — claim weekly reward scaled by current division - Week counter is monotonic (offline-safe, no real-time calendar dependency) - Division 1-3 get a bonus pack alongside coins Migration 0008 adds fut_champs_sessions table and two columns to seasons. 12 new integration tests — all 67 pass, clippy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1168,3 +1168,244 @@ async fn test_upgrades_on_nonexistent_card_return_404() {
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// ── Phase 11: FUT Champions / Division Rivals ─────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_no_active_session_initially() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "ChampsCheck").await;
|
||||
|
||||
let (s, json) = json_get(&app, "/fut-champs").await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
assert!(json["session"].is_null(), "no session should exist on fresh profile");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_start_session() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "ChampsStarter").await;
|
||||
|
||||
let (s, json) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["session"]["week_number"], 1);
|
||||
assert_eq!(json["session"]["matches_played"], 0);
|
||||
assert_eq!(json["session"]["status"], "active");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_cannot_start_two_sessions() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "DoubleStart").await;
|
||||
|
||||
json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let (s, json) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
assert_eq!(s, StatusCode::CONFLICT, "{json}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_record_match_win() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "ChampsWinner").await;
|
||||
|
||||
let (_, start) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let session_id = start["session"]["id"].as_str().unwrap();
|
||||
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
serde_json::json!({ "goals_for": 3, "goals_against": 1 }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["outcome"], "win");
|
||||
assert_eq!(json["session"]["wins"], 1);
|
||||
assert_eq!(json["session"]["matches_played"], 1);
|
||||
assert_eq!(json["matches_remaining"], 29);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_record_match_draw_and_loss() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "ChampsDrawLoss").await;
|
||||
|
||||
let (_, start) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let session_id = start["session"]["id"].as_str().unwrap();
|
||||
|
||||
let (_, draw) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
serde_json::json!({ "goals_for": 1, "goals_against": 1 }),
|
||||
).await;
|
||||
assert_eq!(draw["outcome"], "draw");
|
||||
assert_eq!(draw["session"]["draws"], 1);
|
||||
|
||||
let (_, loss) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
serde_json::json!({ "goals_for": 0, "goals_against": 2 }),
|
||||
).await;
|
||||
assert_eq!(loss["outcome"], "loss");
|
||||
assert_eq!(loss["session"]["losses"], 1);
|
||||
}
|
||||
|
||||
/// Play all 30 matches and verify the session auto-completes at the correct tier.
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_session_auto_completes_at_30_matches() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "Champs30Player").await;
|
||||
|
||||
let (_, start) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let session_id = start["session"]["id"].as_str().unwrap().to_string();
|
||||
|
||||
let mut last_json = serde_json::json!({});
|
||||
for i in 0..30 {
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
// Win 20 out of 30 → Gold 3 tier
|
||||
serde_json::json!({
|
||||
"goals_for": if i < 20 { 2 } else { 0 },
|
||||
"goals_against": if i < 20 { 0 } else { 2 }
|
||||
}),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK, "match {i} failed: {json}");
|
||||
last_json = json;
|
||||
}
|
||||
|
||||
assert_eq!(last_json["auto_completed"], true);
|
||||
assert_eq!(last_json["session"]["status"], "completed");
|
||||
assert_eq!(last_json["session"]["wins"], 20);
|
||||
assert_eq!(last_json["session"]["reward_tier"], "Gold 3");
|
||||
assert_eq!(last_json["session"]["reward_coins"], 15_000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_claim_rewards() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "ChampsClaimPlayer").await;
|
||||
|
||||
let (_, start) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let session_id = start["session"]["id"].as_str().unwrap().to_string();
|
||||
|
||||
// Play all 30 (win all → Elite tier)
|
||||
for _ in 0..30 {
|
||||
json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
serde_json::json!({ "goals_for": 2, "goals_against": 0 }),
|
||||
).await;
|
||||
}
|
||||
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/claim"),
|
||||
serde_json::json!({}),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["tier"], "Elite");
|
||||
assert_eq!(json["coins_awarded"], 50_000);
|
||||
assert!(json["pack_granted"].is_string(), "Elite should grant an icon pack");
|
||||
|
||||
// Double-claim should fail
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/claim"),
|
||||
serde_json::json!({}),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_claim_active_session_fails() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "EarlyClaimPlayer").await;
|
||||
|
||||
let (_, start) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let session_id = start["session"]["id"].as_str().unwrap();
|
||||
|
||||
// Only 1 match played — claim should reject
|
||||
json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
serde_json::json!({ "goals_for": 2, "goals_against": 0 }),
|
||||
).await;
|
||||
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/claim"),
|
||||
serde_json::json!({}),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_history_grows() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "HistoryGrowPlayer").await;
|
||||
|
||||
let (_, hist) = json_get(&app, "/fut-champs/history").await;
|
||||
assert_eq!(hist["total"], 0);
|
||||
|
||||
// Complete a session
|
||||
let (_, start) = json_post(&app, "/fut-champs/start", serde_json::json!({})).await;
|
||||
let session_id = start["session"]["id"].as_str().unwrap().to_string();
|
||||
for _ in 0..30 {
|
||||
json_post(
|
||||
&app,
|
||||
&format!("/fut-champs/{session_id}/result"),
|
||||
serde_json::json!({ "goals_for": 1, "goals_against": 0 }),
|
||||
).await;
|
||||
}
|
||||
|
||||
let (_, hist) = json_get(&app, "/fut-champs/history").await;
|
||||
assert_eq!(hist["total"], 1);
|
||||
assert_eq!(hist["history"][0]["status"], "completed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fut_champs_tier_boundaries() {
|
||||
// Verify tier calculation for edge win counts
|
||||
use openfut_core::models::fut_champs::ChampsTier;
|
||||
assert_eq!(ChampsTier::from_wins(27).label(), "Elite");
|
||||
assert_eq!(ChampsTier::from_wins(26).label(), "World Class");
|
||||
assert_eq!(ChampsTier::from_wins(23).label(), "World Class");
|
||||
assert_eq!(ChampsTier::from_wins(20).label(), "Gold 3");
|
||||
assert_eq!(ChampsTier::from_wins(0).label(), "Bronze 1");
|
||||
assert_eq!(ChampsTier::from_wins(1).label(), "Bronze 2");
|
||||
assert_eq!(ChampsTier::from_wins(5).label(), "Silver 1");
|
||||
assert_eq!(ChampsTier::from_wins(14).label(), "Gold 1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rivals_weekly_reward_claim() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "RivalsClaimPlayer").await;
|
||||
|
||||
// Play a match to create the season row
|
||||
json_post(&app, "/matches/result", serde_json::json!({
|
||||
"squad_id": "dummy", "opponent_name": "Bot",
|
||||
"goals_for": 1, "goals_against": 0, "mode": "squad_battles"
|
||||
})).await;
|
||||
|
||||
let (s, json) = json_post(&app, "/rivals/claim-weekly", serde_json::json!({})).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["week_number"], 1);
|
||||
assert!(json["coins_awarded"].as_i64().unwrap() > 0);
|
||||
assert!(json["new_balance"].as_i64().unwrap() > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rivals_reward_increments_week_counter() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "RivalsWeekCounter").await;
|
||||
|
||||
json_post(&app, "/matches/result", serde_json::json!({
|
||||
"squad_id": "dummy", "opponent_name": "Bot",
|
||||
"goals_for": 1, "goals_against": 0, "mode": "squad_battles"
|
||||
})).await;
|
||||
|
||||
json_post(&app, "/rivals/claim-weekly", serde_json::json!({})).await;
|
||||
let (s, json) = json_post(&app, "/rivals/claim-weekly", serde_json::json!({})).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["week_number"], 2, "second claim should be week 2");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user