Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
119 lines
8.5 KiB
Rust
119 lines
8.5 KiB
Rust
//! Bundles of rewards beyond XP: set by an admin, applied when earned, and delivered to the game server.
|
|
|
|
mod common;
|
|
use common::*;
|
|
|
|
#[tokio::test]
|
|
async fn an_achievement_can_pay_money_chunks_items_and_permissions() {
|
|
let t = setup().await;
|
|
let admin = t.login("admin", "supersecret").await;
|
|
t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Steve", "password": "password123"}))).await;
|
|
let steve_token = t.login("Steve", "password123").await;
|
|
let steve = t.uuid("Steve").await;
|
|
let (_, srv) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "SMP", "instance_id": "smp"}))).await;
|
|
let sid = srv["server"]["id"].as_i64().unwrap();
|
|
let key = srv["token"].as_str().unwrap().to_string();
|
|
let game = |path: &str, body: Value| {
|
|
let (t, key, path) = (&t, key.clone(), path.to_string());
|
|
async move { t.call("POST", &format!("/api/server/v1/{path}"), Some(&key), Some(body)).await }
|
|
};
|
|
let (s, ach) = t.call("POST", "/api/admin/achievements", Some(&admin), Some(json!({
|
|
"id": "ach_test", "title": "Tester", "description": "x", "category": "general", "icon_frame": "task", "icon_item": "minecraft:stone",
|
|
"icon_bg": "#000000", "icon_border": "#ffffff", "xp_reward": 10}))).await;
|
|
assert_eq!(s, StatusCode::OK, "{ach}");
|
|
|
|
// Bad bundles are refused; good ones are normalised.
|
|
for a in [json!({"type": "money", "amount": -5}), json!({"type": "item", "item": "bad item!", "amount": 1}), json!({"type": "nope"})] {
|
|
assert_eq!(t.call("PUT", "/api/admin/reward-bundles/achievement/ach_test", Some(&admin), Some(json!({"actions": [a]}))).await.0, StatusCode::BAD_REQUEST);
|
|
}
|
|
assert_eq!(t.call("PUT", "/api/admin/reward-bundles/banana/x", Some(&admin), Some(json!({"actions": []}))).await.0, StatusCode::BAD_REQUEST);
|
|
let (s, saved) = t.call("PUT", "/api/admin/reward-bundles/achievement/ach_test", Some(&admin), Some(json!({"actions": [
|
|
{"type": "money", "amount": 250},
|
|
{"type": "claim_chunks", "amount": 8},
|
|
{"type": "item", "item": "diamond", "amount": 3},
|
|
{"type": "permission", "node": "essentials.fly", "minutes": 10080},
|
|
{"type": "group", "group": "vip"},
|
|
{"type": "message", "text": "Well done!"}]}))).await;
|
|
assert_eq!(s, StatusCode::OK, "{saved}");
|
|
assert_eq!(saved["actions"][2]["item"], "minecraft:diamond");
|
|
|
|
// Players see short summaries (never command text).
|
|
let (_, sums) = t.call("GET", "/api/v1/reward-bundles", Some(&steve_token), None).await;
|
|
assert_eq!(sums["achievement:ach_test"][0], "$250.00");
|
|
assert_eq!(sums["achievement:ach_test"][2], "Diamond x3");
|
|
assert_eq!(sums["achievement:ach_test"][3], "Permission: essentials.fly for 7 days");
|
|
|
|
// Earning it applies the panel-side rewards straight away…
|
|
sqlx::query("INSERT INTO server_online (server_id, uuid, name, joined_at) VALUES (?, ?, 'Steve', 'now')").bind(sid).bind(&steve).execute(&t.db).await.unwrap();
|
|
sqlx::query("INSERT INTO user_achievements (user_uuid, achievement_id, unlocked_at) VALUES (?, 'ach_test', 'now')").bind(&steve).execute(&t.db).await.unwrap();
|
|
let (s, polled) = game("rewards/poll", json!({})).await;
|
|
assert_eq!(s, StatusCode::OK, "{polled}");
|
|
let kinds: Vec<String> = polled["deliveries"].as_array().unwrap().iter().map(|d| d["kind"].as_str().unwrap().to_string()).collect();
|
|
assert_eq!(kinds, ["item", "permission", "group", "message"], "{polled}");
|
|
assert_eq!(polled["deliveries"][0]["payload"]["item"], "minecraft:diamond");
|
|
assert_eq!(polled["deliveries"][0]["name"], "Steve");
|
|
let balance: f64 = sqlx::query_scalar("SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?").bind(sid).bind(&steve).fetch_one(&t.db).await.unwrap();
|
|
assert_eq!(balance, 1250.0, "the starting 1000 plus 250");
|
|
let chunks: i64 = sqlx::query_scalar("SELECT claim_chunks FROM player_bonuses WHERE uuid = ?").bind(&steve).fetch_one(&t.db).await.unwrap();
|
|
assert_eq!(chunks, 8);
|
|
|
|
// …and a delivery is handed out once until acknowledged.
|
|
assert!(game("rewards/poll", json!({})).await.1["deliveries"].as_array().unwrap().is_empty());
|
|
let ids: Vec<i64> = polled["deliveries"].as_array().unwrap().iter().map(|d| d["id"].as_i64().unwrap()).collect();
|
|
let (s, _) = game("rewards/ack", json!({"done": [ids[0], ids[1]], "failed": [{"id": ids[2], "error": "no such group"}]})).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
let (_, log) = t.call("GET", "/api/admin/reward-deliveries", Some(&admin), None).await;
|
|
let log = log.as_array().unwrap();
|
|
assert_eq!(log.len(), 4);
|
|
assert!(log.iter().any(|d| d["error"] == "no such group"));
|
|
assert_eq!(log.iter().filter(|d| !d["delivered_at"].is_null()).count(), 2);
|
|
|
|
// The extra chunks lift the player's guild's claim limit.
|
|
let (_, g) = t.call("POST", "/api/v1/guilds", Some(&steve_token), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
|
|
let (_, detail) = t.call("GET", &format!("/api/v1/guilds/{}", g["id"].as_str().unwrap()), Some(&steve_token), None).await;
|
|
assert_eq!(detail["max_claims"].as_i64(), Some(16 + 8), "{detail}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn deliveries_wait_for_the_player_to_be_online() {
|
|
let t = setup().await;
|
|
let admin = t.login("admin", "supersecret").await;
|
|
t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Alex", "password": "password123"}))).await;
|
|
let alex = t.uuid("Alex").await;
|
|
let (_, srv) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "SMP", "instance_id": "smp"}))).await;
|
|
let sid = srv["server"]["id"].as_i64().unwrap();
|
|
let key = srv["token"].as_str().unwrap().to_string();
|
|
sqlx::query("INSERT INTO reward_deliveries (uuid, kind, payload, source, created_at) VALUES (?, 'item', '{\"type\":\"item\",\"item\":\"minecraft:apple\",\"amount\":1}', 'x', 'now')").bind(&alex).execute(&t.db).await.unwrap();
|
|
let poll = || async { t.call("POST", "/api/server/v1/rewards/poll", Some(&key), Some(json!({}))).await.1 };
|
|
assert!(poll().await["deliveries"].as_array().unwrap().is_empty(), "offline players get nothing yet");
|
|
sqlx::query("INSERT INTO server_online (server_id, uuid, name, joined_at) VALUES (?, ?, 'Alex', 'now')").bind(sid).bind(&alex).execute(&t.db).await.unwrap();
|
|
assert_eq!(poll().await["deliveries"].as_array().unwrap().len(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_rank_milestone_can_grant_more_than_its_title() {
|
|
let t = setup().await;
|
|
let admin = t.login("admin", "supersecret").await;
|
|
t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Alex", "password": "password123"}))).await;
|
|
let alex = t.uuid("Alex").await;
|
|
let (_, srv) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "SMP", "instance_id": "smp"}))).await;
|
|
let sid = srv["server"]["id"].as_i64().unwrap();
|
|
let key = srv["token"].as_str().unwrap().to_string();
|
|
let (_, made) = t.call("POST", "/api/admin/rewards", Some(&admin), Some(json!({"level_type": "global", "level_req": 3, "reward_type": "title", "reward_name": "Veteran"}))).await;
|
|
let id = made["id"].as_i64().unwrap();
|
|
let (s, _) = t.call("PUT", &format!("/api/admin/reward-bundles/level_reward/{id}"), Some(&admin), Some(json!({"actions": [
|
|
{"type": "money", "amount": 500}, {"type": "command", "command": "/give {player} cake 1"}]}))).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
sqlx::query("INSERT INTO server_online (server_id, uuid, name, joined_at) VALUES (?, ?, 'Alex', 'now')").bind(sid).bind(&alex).execute(&t.db).await.unwrap();
|
|
let (s, r) = t.call("POST", "/api/server/v1/player/xp", Some(&key), Some(json!({"operation_id": "lv", "uuid": alex, "scope": "global", "amount": 100000}))).await;
|
|
assert_eq!(s, StatusCode::OK, "{r}");
|
|
let (_, polled) = t.call("POST", "/api/server/v1/rewards/poll", Some(&key), Some(json!({}))).await;
|
|
assert_eq!(polled["deliveries"][0]["kind"], "command", "{polled}");
|
|
assert_eq!(polled["deliveries"][0]["payload"]["command"], "give {player} cake 1");
|
|
let balance: f64 = sqlx::query_scalar("SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?").bind(sid).bind(&alex).fetch_one(&t.db).await.unwrap();
|
|
assert_eq!(balance, 1500.0);
|
|
// Earning the same milestone twice pays once.
|
|
t.call("POST", "/api/server/v1/player/xp", Some(&key), Some(json!({"operation_id": "lv2", "uuid": alex, "scope": "global", "amount": 100}))).await;
|
|
assert!(t.call("POST", "/api/server/v1/rewards/poll", Some(&key), Some(json!({}))).await.1["deliveries"].as_array().unwrap().is_empty());
|
|
}
|