Phase 10: card upgrade system (chemistry styles, position change, training)
CI / Build, lint & test (push) Failing after 1m37s
CI / Build, lint & test (push) Failing after 1m37s
- GET /chemistry-styles — lists 18 FUT-style chemistry styles with stat boost breakdowns - POST /collection/:id/chemistry-style — apply a style (Shadow, Hunter, Anchor, etc.) - POST /collection/:id/position — override a player's position for 500 coins - POST /collection/:id/training — add +1/+2/+3 OVR training bonus (max +3 total) - GET /collection now includes chemistry_style, position_override, training_bonus, effective_overall and effective_position fields per owned card - Migration 0007 adds three columns to owned_cards with safe defaults - 10 new integration tests — all 55 pass, clippy clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -984,3 +984,187 @@ async fn test_match_result_returns_season_info() {
|
||||
// season_end is None for non-final match
|
||||
assert!(result["season_end"].is_null());
|
||||
}
|
||||
|
||||
// ── Phase 10: Card Upgrades ───────────────────────────────────────────────────
|
||||
|
||||
async fn get_first_owned_card_id(app: &axum::Router) -> String {
|
||||
// Open the starter pack to get a card
|
||||
let (_, packs) = json_get(app, "/packs").await;
|
||||
let pack_id = packs["packs"][0]["pack_id"].as_str().unwrap().to_string();
|
||||
json_post(app, &format!("/packs/open/{pack_id}"), serde_json::json!({})).await;
|
||||
let (_, coll) = json_get(app, "/collection").await;
|
||||
coll["collection"][0]["owned_card_id"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chemistry_styles_list() {
|
||||
let app = build_test_app().await;
|
||||
let (s, json) = json_get(&app, "/chemistry-styles").await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
assert!(json["chemistry_styles"].is_array());
|
||||
assert!(json["total"].as_u64().unwrap() >= 18, "expect 18 built-in styles");
|
||||
// "basic" must always be present
|
||||
let styles = json["chemistry_styles"].as_array().unwrap();
|
||||
assert!(styles.iter().any(|s| s["id"] == "basic"));
|
||||
assert!(styles.iter().any(|s| s["id"] == "shadow"));
|
||||
assert!(styles.iter().any(|s| s["id"] == "hunter"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_apply_chemistry_style() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "ChemPlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/chemistry-style"),
|
||||
serde_json::json!({ "style_id": "hunter" }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["chemistry_style"], "hunter");
|
||||
assert_eq!(json["style_details"]["name"], "Hunter");
|
||||
assert!(json["style_details"]["boosts"]["pace"].as_i64().unwrap() > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_apply_invalid_chemistry_style_returns_404() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "BadChemPlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/chemistry-style"),
|
||||
serde_json::json!({ "style_id": "nonexistent_style" }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_collection_includes_upgrade_fields() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "UpgradeFieldPlayer").await;
|
||||
get_first_owned_card_id(&app).await; // opens starter pack
|
||||
|
||||
let (s, coll) = json_get(&app, "/collection").await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let card = &coll["collection"][0];
|
||||
assert!(card["chemistry_style"].is_string());
|
||||
assert!(card["training_bonus"].is_number());
|
||||
assert!(card["effective_overall"].is_number());
|
||||
assert!(card["effective_position"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_position_change_deducts_coins() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "PosChangePlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
let (_, club_before) = json_get(&app, "/club").await;
|
||||
let coins_before = club_before["coins"].as_i64().unwrap();
|
||||
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/position"),
|
||||
serde_json::json!({ "position": "CM" }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["position_override"], "CM");
|
||||
assert_eq!(json["cost_coins"], 500);
|
||||
|
||||
let (_, club_after) = json_get(&app, "/club").await;
|
||||
assert_eq!(
|
||||
club_after["coins"].as_i64().unwrap(),
|
||||
coins_before - 500
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_position_change_invalid_position() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "BadPosPlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/position"),
|
||||
serde_json::json!({ "position": "STRIKER" }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_training_boost_increases_effective_overall() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "TrainingPlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/training"),
|
||||
serde_json::json!({ "boost": 2 }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK, "{json}");
|
||||
assert_eq!(json["training_bonus"], 2);
|
||||
assert_eq!(
|
||||
json["effective_overall"].as_i64().unwrap(),
|
||||
json["base_overall"].as_i64().unwrap() + 2
|
||||
);
|
||||
assert_eq!(json["max_bonus"], 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_training_capped_at_max() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "CapTrainPlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
// Apply +3 (the max)
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/training"),
|
||||
serde_json::json!({ "boost": 3 }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
|
||||
// Trying to add more should fail
|
||||
let (s, json) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/training"),
|
||||
serde_json::json!({ "boost": 1 }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST, "{json}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_training_invalid_boost_value() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "BadTrainPlayer").await;
|
||||
let owned_id = get_first_owned_card_id(&app).await;
|
||||
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
&format!("/collection/{owned_id}/training"),
|
||||
serde_json::json!({ "boost": 5 }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_upgrades_on_nonexistent_card_return_404() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "OwnerAlpha").await;
|
||||
|
||||
// Use a made-up owned_card_id that doesn't belong to this club
|
||||
let (s, _) = json_post(
|
||||
&app,
|
||||
"/collection/nonexistent-owned-card-id/chemistry-style",
|
||||
serde_json::json!({ "style_id": "basic" }),
|
||||
).await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user