Fix Rust formatting
Server integrations / Paper and protocol tests (push) Failing after 20s
Server integrations / fabric / 1.20.1 (push) Failing after 11s
Server integrations / fabric / 1.21.1 (push) Failing after 14s
Server integrations / fabric / 26.1.2 (push) Failing after 17s
Server integrations / fabric / 26.2 (push) Failing after 12s
Server integrations / fabric / 26.3 (push) Failing after 6s
Server integrations / forge / 1.20.1 (push) Failing after 6s
Server integrations / forge / 1.21.1 (push) Failing after 8s
Server integrations / forge / 26.1.2 (push) Failing after 8s
Server integrations / forge / 26.2 (push) Failing after 8s
Server integrations / forge / 26.3 (push) Failing after 7s
CI / Web UIs (type-check + build) (panel/web) (push) Canceled after 0s
CI / Launcher (Windows build check) (push) Canceled after 2m53s
CI / Real installs (Mojang, Fabric, Quilt, Forge, NeoForge) (push) Canceled after 0s
CI / Docker image builds (push) Canceled after 0s
CI / Rust (fmt, clippy, tests) (push) Canceled after 1m22s
CI / Web UIs (type-check + build) (launcher) (push) Canceled after 1m27s

This commit is contained in:
SCOPEDD committed 2026-09-30 22:56:38 -04:00
1 parent ce49447b6e
commit 60edc99780
58 files changed
+3533 -1375

No files matched your search

+17 -3
View File
@@ -32,13 +32,22 @@ async fn claim_index_follows_claims_and_membership() {
assert!(same.get("claims").is_none());
// A guild, then a claim: the revision moves and the index lists the chunk.
let (_, g) = t.call("POST", "/api/v1/guilds", Some(&tokens[0]), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
let (_, g) =
t.call("POST", "/api/v1/guilds", Some(&tokens[0]), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
let gid = g["id"].as_str().unwrap().to_string();
let (_, r1) = index(Some(rev0.clone())).await;
assert_eq!(r1["unchanged"], false, "guild creation bumps the revision");
let claim = |x: i32| {
let (t, tok, gid) = (&t, tokens[0].clone(), gid.clone());
async move { t.call("POST", &format!("/api/v1/guilds/{gid}/claim"), Some(&tok), Some(json!({"server_id": sid, "dimension": "minecraft:overworld", "chunk_x": x, "chunk_z": -4}))).await }
async move {
t.call(
"POST",
&format!("/api/v1/guilds/{gid}/claim"),
Some(&tok),
Some(json!({"server_id": sid, "dimension": "minecraft:overworld", "chunk_x": x, "chunk_z": -4})),
)
.await
}
};
let (s, c1) = claim(3).await;
assert_eq!(s, StatusCode::OK, "{c1}");
@@ -53,7 +62,12 @@ async fn claim_index_follows_claims_and_membership() {
assert_eq!(index(Some(rev2.clone())).await.1["unchanged"], true);
// Membership changes bump it too.
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES (?, ?, 'Steve', 'member', 'now')").bind(&gid).bind(&steve_uuid).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES (?, ?, 'Steve', 'member', 'now')")
.bind(&gid)
.bind(&steve_uuid)
.execute(&t.db)
.await
.unwrap();
let (_, idx) = index(Some(rev2.clone())).await;
assert_eq!(idx["unchanged"], false);
assert_eq!(idx["members"][&gid].as_array().unwrap().len(), 2);
+29 -8
View File
@@ -29,12 +29,21 @@ async fn plugins_read_players_and_change_progress() {
assert_eq!(call("player/info", json!({"uuid": "nope"})).await.0, StatusCode::BAD_REQUEST);
// Info: levels, balance, guild, quests.
sqlx::query("INSERT INTO server_economy (server_id, uuid, username, balance, updated_at) VALUES (?, ?, 'Steve', 321.5, 'now')").bind(sid).bind(&steve).execute(&t.db).await.unwrap();
let (_, g) = t.call("POST", "/api/v1/guilds", Some(&tokens[0]), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
sqlx::query("INSERT INTO server_economy (server_id, uuid, username, balance, updated_at) VALUES (?, ?, 'Steve', 321.5, 'now')")
.bind(sid)
.bind(&steve)
.execute(&t.db)
.await
.unwrap();
let (_, g) =
t.call("POST", "/api/v1/guilds", Some(&tokens[0]), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
t.call("GET", "/api/v1/quests/my", Some(&tokens[0]), None).await;
let (s, info) = call("player/info", json!({"uuid": steve})).await;
assert_eq!(s, StatusCode::OK, "{info}");
assert_eq!((info["name"].as_str(), info["balance"].as_f64(), info["guild"]["tag"].as_str(), info["guild"]["role"].as_str()), (Some("Steve"), Some(321.5), Some("IRON"), Some("leader")));
assert_eq!(
(info["name"].as_str(), info["balance"].as_f64(), info["guild"]["tag"].as_str(), info["guild"]["role"].as_str()),
(Some("Steve"), Some(321.5), Some("IRON"), Some("leader"))
);
// Founding a guild unlocks an achievement worth XP, so the player starts above zero.
let base = info["global"]["xp"].as_i64().unwrap();
assert!(base > 0);
@@ -53,21 +62,33 @@ async fn plugins_read_players_and_change_progress() {
let (_, r) = call("player/xp", xp("x3", "global", -200)).await;
assert_eq!(r["xp"].as_i64(), Some(base + 1000));
assert_eq!(call("player/xp", xp("x4", "global", 0)).await.0, StatusCode::BAD_REQUEST);
assert_eq!(call("player/xp", json!({"operation_id": "x5", "uuid": "00000000-0000-0000-0000-00000000dead", "amount": 5})).await.0, StatusCode::NOT_FOUND);
assert_eq!(
call("player/xp", json!({"operation_id": "x5", "uuid": "00000000-0000-0000-0000-00000000dead", "amount": 5})).await.0,
StatusCode::NOT_FOUND
);
let (_, info) = call("player/info", json!({"uuid": steve})).await;
assert_eq!((info["global"]["xp"].as_i64(), info["server"]["xp"].as_i64(), info["server"]["level"].as_i64()), (Some(base + 1000), Some(400), Some(2)));
assert_eq!(
(info["global"]["xp"].as_i64(), info["server"]["xp"].as_i64(), info["server"]["level"].as_i64()),
(Some(base + 1000), Some(400), Some(2))
);
// Quest objectives by quest id (complete it), and by action.
let (_, quests) = t.call("GET", "/api/v1/quests/my", Some(&tokens[0]), None).await;
let quest = quests.as_array().unwrap().iter().find(|q| q["quest"]["period"] == "daily").unwrap()["quest"]["id"].as_str().unwrap().to_string();
let quest =
quests.as_array().unwrap().iter().find(|q| q["quest"]["period"] == "daily").unwrap()["quest"]["id"].as_str().unwrap().to_string();
let (s, r) = call("player/quest-objective", json!({"operation_id": "q1", "uuid": steve, "objective": quest, "amount": 0})).await;
assert_eq!((s, r["advanced"].clone()), (StatusCode::OK, json!(true)), "{r}");
let (_, quests) = t.call("GET", "/api/v1/quests/my", Some(&tokens[0]), None).await;
let done = quests.as_array().unwrap().iter().find(|q| q["quest"]["id"] == quest.as_str()).unwrap();
assert_eq!(done["completed"], true);
let (_, r) = call("player/quest-objective", json!({"operation_id": "q2", "uuid": steve, "objective": "block_broken:DIAMOND_ORE@minecraft:overworld", "amount": 3})).await;
let (_, r) = call(
"player/quest-objective",
json!({"operation_id": "q2", "uuid": steve, "objective": "block_broken:DIAMOND_ORE@minecraft:overworld", "amount": 3}),
)
.await;
assert_eq!(r["advanced"], true);
let (_, r) = call("player/quest-objective", json!({"operation_id": "q3", "uuid": steve, "objective": "nothing:at_all", "amount": 3})).await;
let (_, r) =
call("player/quest-objective", json!({"operation_id": "q3", "uuid": steve, "objective": "nothing:at_all", "amount": 3})).await;
assert_eq!(r["advanced"], false);
let (_, info) = call("player/info", json!({"uuid": steve})).await;
assert_eq!(info["quests"]["daily"]["completed"], 1);
+13 -3
View File
@@ -20,7 +20,9 @@ async fn discord_messages_are_customised_and_previewed_with_real_data() {
tpl["achievement"]["image"] = json!("https://cdn.example.com/banner.png");
let (s, _) = t.call("PUT", "/api/admin/discord/studio/templates", Some(&admin), Some(json!({"templates": tpl.clone()}))).await;
assert_eq!(s, StatusCode::OK);
let (s, p) = t.call("POST", "/api/admin/discord/studio/preview", Some(&admin), Some(json!({"kind": "achievement", "style": tpl["achievement"]}))).await;
let (s, p) = t
.call("POST", "/api/admin/discord/studio/preview", Some(&admin), Some(json!({"kind": "achievement", "style": tpl["achievement"]})))
.await;
assert_eq!(s, StatusCode::OK, "{p}");
let e = &p["embeds"][0];
assert_eq!(e["title"], "🎉 admin did it!");
@@ -34,7 +36,12 @@ async fn discord_messages_are_customised_and_previewed_with_real_data() {
let (_, srv) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "Survival SMP", "instance_id": "smp"}))).await;
let sid = srv["server"]["id"].as_i64().unwrap();
t.call("POST", "/api/v1/guilds", Some(&steve_token), Some(json!({"instance_id": "smp", "name": "Iron Wolves", "tag": "IRON"}))).await;
sqlx::query("INSERT INTO server_economy (server_id, uuid, username, balance, updated_at) VALUES (?, ?, 'Steve', 123456, 'now')").bind(sid).bind(t.uuid("Steve").await).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO server_economy (server_id, uuid, username, balance, updated_at) VALUES (?, ?, 'Steve', 123456, 'now')")
.bind(sid)
.bind(t.uuid("Steve").await)
.execute(&t.db)
.await
.unwrap();
for (kind, needle) in [("status", "Survival SMP"), ("guilds", "Iron Wolves"), ("baltop", "$123,456"), ("leaderboard", "Leaderboard")] {
let cfg = all["live"][kind].clone();
let (s, p) = t.call("POST", "/api/admin/discord/studio/preview", Some(&admin), Some(json!({"kind": kind, "live": cfg}))).await;
@@ -47,7 +54,10 @@ async fn discord_messages_are_customised_and_previewed_with_real_data() {
live["status"]["interval_secs"] = json!(5);
live["status"]["limit"] = json!(500);
live["status"]["webhook_url"] = json!("https://evil.example.com/hook");
assert_eq!(t.call("PUT", "/api/admin/discord/studio/live", Some(&admin), Some(json!({"live": live.clone()}))).await.0, StatusCode::BAD_REQUEST);
assert_eq!(
t.call("PUT", "/api/admin/discord/studio/live", Some(&admin), Some(json!({"live": live.clone()}))).await.0,
StatusCode::BAD_REQUEST
);
live["status"]["webhook_url"] = json!("https://discord.com/api/webhooks/1/abc");
assert_eq!(t.call("PUT", "/api/admin/discord/studio/live", Some(&admin), Some(json!({"live": live}))).await.0, StatusCode::OK);
let (_, again) = t.call("GET", "/api/admin/discord/studio", Some(&admin), None).await;
+23 -6
View File
@@ -20,21 +20,32 @@ async fn emails_use_templates_audiences_and_opt_outs() {
assert_eq!(t.call("PUT", "/api/admin/email/templates", Some(&admin), Some(ok)).await.0, StatusCode::OK);
// Placeholders fill per player, and the HTML is branded and has an unsubscribe link.
let (s, p) = t.call("POST", "/api/admin/email/preview", Some(&admin), Some(json!({"subject": "Hi {player}", "body": "Hello **{player}**\n\n[button: Go]({panel_url})"}))).await;
let (s, p) = t
.call(
"POST",
"/api/admin/email/preview",
Some(&admin),
Some(json!({"subject": "Hi {player}", "body": "Hello **{player}**\n\n[button: Go]({panel_url})"})),
)
.await;
assert_eq!(s, StatusCode::OK, "{p}");
assert_eq!(p["subject"], "Hi admin");
assert!(p["html"].as_str().unwrap().contains("<strong>admin</strong>") && p["html"].as_str().unwrap().contains("Unsubscribe"));
assert!(p["text"].as_str().unwrap().contains("/api/v1/email/unsubscribe?u="));
// Only active players with an email count.
let count = |a: Value| { let (t, admin) = (&t, admin.clone()); async move { t.call("POST", "/api/admin/email/audience", Some(&admin), Some(json!({"audience": a}))).await.1 } };
let count = |a: Value| {
let (t, admin) = (&t, admin.clone());
async move { t.call("POST", "/api/admin/email/audience", Some(&admin), Some(json!({"audience": a}))).await.1 }
};
assert_eq!(count(json!({"kind": "all"})).await["count"], 2, "Steve and Alex have an address; Notch and admin don't");
assert_eq!(count(json!({"kind": "players", "names": ["steve", "nobody"]})).await["count"], 1);
// Opting out through the link removes a player from marketing mail, but not from account notices.
let steve = t.uuid("Steve").await;
let token = {
let secret: String = sqlx::query_scalar("SELECT value FROM kv WHERE key = 'email_unsubscribe_secret'").fetch_one(&t.db).await.unwrap();
let secret: String =
sqlx::query_scalar("SELECT value FROM kv WHERE key = 'email_unsubscribe_secret'").fetch_one(&t.db).await.unwrap();
let secret = secret.trim_matches('"').to_string();
use sha2::{Digest, Sha256};
Sha256::digest(format!("{secret}:{steve}").as_bytes()).iter().take(16).map(|b| format!("{b:02x}")).collect::<String>()
@@ -44,15 +55,21 @@ async fn emails_use_templates_audiences_and_opt_outs() {
assert_eq!(count(json!({"kind": "all"})).await["count"], 2, "a wrong token changes nothing");
t.fetch(&format!("/api/v1/email/unsubscribe?u={steve}&t={token}")).await;
assert_eq!(count(json!({"kind": "all"})).await["count"], 1);
let (_, important) = t.call("POST", "/api/admin/email/audience", Some(&admin), Some(json!({"audience": {"kind": "all"}, "important": true}))).await;
let (_, important) =
t.call("POST", "/api/admin/email/audience", Some(&admin), Some(json!({"audience": {"kind": "all"}, "important": true}))).await;
assert_eq!(important["count"], 2);
// Without SMTP configured, a send is queued but each message fails with a clear reason in the log.
let (s, sent) = t.call("POST", "/api/admin/email/send", Some(&admin), Some(json!({"subject": "Hi", "body": "Hello", "audience": {"kind": "all"}}))).await;
let (s, sent) = t
.call("POST", "/api/admin/email/send", Some(&admin), Some(json!({"subject": "Hi", "body": "Hello", "audience": {"kind": "all"}})))
.await;
assert_eq!(s, StatusCode::OK, "{sent}");
for _ in 0..40 {
let (_, all) = t.call("GET", "/api/admin/email", Some(&admin), None).await;
if all["log"][0]["failed"] == 1 { assert!(all["log"][0]["last_error"].as_str().unwrap().contains("Resend")); return; }
if all["log"][0]["failed"] == 1 {
assert!(all["log"][0]["last_error"].as_str().unwrap().contains("Resend"));
return;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
panic!("the send was never logged");
+30 -23
View File
@@ -38,7 +38,8 @@ async fn leveling_and_quests_progression() {
let (alex_token, _, alex_uuid, _) = setup_users(&t).await;
let (_server_id, server_token) = create_test_server(&t, &admin).await;
// Hand every quest to every player so progress can be checked per quest.
let (s, v) = t.call("PUT", "/api/admin/progression", Some(&admin), Some(json!({"daily_quest_limit": 0, "weekly_quest_limit": 0}))).await;
let (s, v) =
t.call("PUT", "/api/admin/progression", Some(&admin), Some(json!({"daily_quest_limit": 0, "weekly_quest_limit": 0}))).await;
assert_eq!(s, StatusCode::OK, "{v}");
// 1. Initial level check
@@ -97,8 +98,8 @@ async fn leveling_and_quests_progression() {
assert!(current_xp > 0, "Alex should have earned XP");
let (s, quests_after) = t.call("GET", "/api/v1/quests/me", Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK);
let progress = |id: &str| quests_after.as_array().unwrap().iter()
.find(|q| q["quest"]["id"] == id).unwrap()["progress"].as_i64().unwrap();
let progress =
|id: &str| quests_after.as_array().unwrap().iter().find(|q| q["quest"]["id"] == id).unwrap()["progress"].as_i64().unwrap();
assert_eq!(progress("q_d_mine_diamond"), 4);
assert_eq!(progress("q_d_mine_coal"), 0);
@@ -146,8 +147,14 @@ async fn guilds_and_land_claims() {
let admin = t.login("admin", "supersecret").await;
let (alex_token, _, alex_uuid, steve_uuid) = setup_users(&t).await;
let (server_id, server_token) = create_test_server(&t, &admin).await;
let (status, updated) = t.call("PUT", &format!("/api/admin/servers/{server_id}"), Some(&admin),
Some(json!({"name": "Survival Hub", "instance_id": "smp-pack"}))).await;
let (status, updated) = t
.call(
"PUT",
&format!("/api/admin/servers/{server_id}"),
Some(&admin),
Some(json!({"name": "Survival Hub", "instance_id": "smp-pack"})),
)
.await;
assert_eq!(status, StatusCode::OK, "{updated}");
// 1. Alex founds a guild
@@ -261,11 +268,23 @@ async fn guilds_and_land_claims() {
assert_eq!(check_unclaimed["allowed"], true);
// The in-game command uses the server unclaim route rather than DELETE.
let (s, _) = t.call("POST", "/api/server/v1/guilds/claim", Some(&server_token),
Some(json!({"uuid": &alex_uuid, "dimension": "minecraft:overworld", "chunk_x": 3, "chunk_z": 7}))).await;
let (s, _) = t
.call(
"POST",
"/api/server/v1/guilds/claim",
Some(&server_token),
Some(json!({"uuid": &alex_uuid, "dimension": "minecraft:overworld", "chunk_x": 3, "chunk_z": 7})),
)
.await;
assert_eq!(s, StatusCode::OK);
let (s, response) = t.call("POST", "/api/server/v1/guilds/unclaim", Some(&server_token),
Some(json!({"uuid": &alex_uuid, "dimension": "minecraft:overworld", "chunk_x": 3, "chunk_z": 7}))).await;
let (s, response) = t
.call(
"POST",
"/api/server/v1/guilds/unclaim",
Some(&server_token),
Some(json!({"uuid": &alex_uuid, "dimension": "minecraft:overworld", "chunk_x": 3, "chunk_z": 7})),
)
.await;
assert_eq!(s, StatusCode::OK, "{response}");
}
@@ -275,24 +294,12 @@ async fn social_friends_dms_and_profiles() {
let (alex_token, steve_token, alex_uuid, steve_uuid) = setup_users(&t).await;
// 1. Alex sends friend request to Steve
let (s, _) = t
.call(
"POST",
"/api/v1/social/friends/request",
Some(&alex_token),
Some(json!({"friend_username": "Steve"})),
)
.await;
let (s, _) = t.call("POST", "/api/v1/social/friends/request", Some(&alex_token), Some(json!({"friend_username": "Steve"}))).await;
assert_eq!(s, StatusCode::OK);
// 2. Steve accepts friend request
let (s, _) = t
.call(
"POST",
"/api/v1/social/friends/respond",
Some(&steve_token),
Some(json!({"friend_uuid": &alex_uuid, "accept": true})),
)
.call("POST", "/api/v1/social/friends/respond", Some(&steve_token), Some(json!({"friend_uuid": &alex_uuid, "accept": true})))
.await;
assert_eq!(s, StatusCode::OK);
+35 -9
View File
@@ -21,14 +21,20 @@ async fn fixture() -> Fixture {
for n in ["Alex", "Steve", "Mia"] {
t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": n, "password": "password123"}))).await;
}
let (alex, steve, mia) = (t.login("Alex", "password123").await, t.login("Steve", "password123").await, t.login("Mia", "password123").await);
let (alex, steve, mia) =
(t.login("Alex", "password123").await, t.login("Steve", "password123").await, t.login("Mia", "password123").await);
let (alex_uuid, steve_uuid) = (t.uuid("Alex").await, 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 server = srv["token"].as_str().unwrap().to_string();
let (_, g) = t.call("POST", "/api/v1/guilds", Some(&alex), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
let gid = g["id"].as_str().unwrap().to_string();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES (?, ?, 'Steve', 'member', '2026-01-01')").bind(&gid).bind(&steve_uuid).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES (?, ?, 'Steve', 'member', '2026-01-01')")
.bind(&gid)
.bind(&steve_uuid)
.execute(&t.db)
.await
.unwrap();
// Mia leads a second guild.
t.call("POST", "/api/v1/guilds", Some(&mia), Some(json!({"instance_id": "smp", "name": "Void", "tag": "VOID"}))).await;
Fixture { t, server, sid, alex, steve, alex_uuid, steve_uuid, gid }
@@ -39,7 +45,12 @@ impl Fixture {
self.t.call("POST", &format!("/api/server/v1/{path}"), Some(&self.server), Some(body)).await
}
async fn guild_balance(&self) -> f64 {
sqlx::query_scalar("SELECT COALESCE((SELECT balance FROM guild_wallets WHERE guild_id = ? AND server_id = ?), 0)").bind(&self.gid).bind(self.sid).fetch_one(&self.t.db).await.unwrap()
sqlx::query_scalar("SELECT COALESCE((SELECT balance FROM guild_wallets WHERE guild_id = ? AND server_id = ?), 0)")
.bind(&self.gid)
.bind(self.sid)
.fetch_one(&self.t.db)
.await
.unwrap()
}
}
@@ -87,12 +98,18 @@ async fn bank_deposits_withdrawals_and_roles() {
#[tokio::test]
async fn shop_sales_and_guild_payments() {
let f = fixture().await;
let credit = json!({"operation_id": "sale-1", "uuid": f.steve_uuid, "username": "Steve", "amount": 64.0, "description": "64x Cobblestone"});
let credit =
json!({"operation_id": "sale-1", "uuid": f.steve_uuid, "username": "Steve", "amount": 64.0, "description": "64x Cobblestone"});
let (s, r) = f.post("guilds/bank/credit", credit.clone()).await;
assert_eq!(s, StatusCode::OK, "{r}");
assert_eq!(f.post("guilds/bank/credit", credit).await.1, r);
assert_eq!(f.guild_balance().await, 64.0);
let (_, nobody) = f.post("guilds/bank/credit", json!({"operation_id": "sale-2", "uuid": "00000000-0000-0000-0000-000000000009", "username": "X", "amount": 5.0})).await;
let (_, nobody) = f
.post(
"guilds/bank/credit",
json!({"operation_id": "sale-2", "uuid": "00000000-0000-0000-0000-000000000009", "username": "X", "amount": 5.0}),
)
.await;
assert!(nobody["error"].as_str().unwrap().contains("not in a guild"));
let pay = |op: &str, who: &str, tag: &str, amount: f64| json!({"operation_id": op, "uuid": who, "to_tag": tag, "amount": amount});
@@ -103,7 +120,8 @@ async fn shop_sales_and_guild_payments() {
let (s, r) = f.post("guilds/bank/pay", pay("p5", &f.alex_uuid, "void", 24.0)).await;
assert_eq!(s, StatusCode::OK, "{r}");
assert_eq!(r["balance"], 40.0);
let void_balance: f64 = sqlx::query_scalar("SELECT balance FROM guild_wallets WHERE guild_id <> ?").bind(&f.gid).fetch_one(&f.t.db).await.unwrap();
let void_balance: f64 =
sqlx::query_scalar("SELECT balance FROM guild_wallets WHERE guild_id <> ?").bind(&f.gid).fetch_one(&f.t.db).await.unwrap();
assert_eq!(void_balance, 24.0);
let kinds: Vec<String> = sqlx::query_scalar("SELECT kind FROM guild_wallet_transactions ORDER BY id").fetch_all(&f.t.db).await.unwrap();
assert_eq!(kinds, ["sale", "transfer_out", "transfer_in"]);
@@ -126,7 +144,13 @@ async fn guild_market_listings_and_purchases() {
// Mia's guild buys it with guild money: she's an officer but the bank is empty first.
let mia_uuid = f.t.uuid("Mia").await;
let buy = |op: &str| json!({"operation_id": op, "listing_id": listing, "buyer_uuid": mia_uuid, "buyer_name": "Mia", "as_guild": true});
sqlx::query("INSERT INTO guild_wallets (server_id, guild_id, balance, updated_at) SELECT ?, id, 50, 'now' FROM guilds WHERE tag = 'VOID'").bind(f.sid).execute(&f.t.db).await.unwrap();
sqlx::query(
"INSERT INTO guild_wallets (server_id, guild_id, balance, updated_at) SELECT ?, id, 50, 'now' FROM guilds WHERE tag = 'VOID'",
)
.bind(f.sid)
.execute(&f.t.db)
.await
.unwrap();
let (s, e) = f.post("economy/market/buy", buy("b1")).await;
assert_eq!(s, StatusCode::BAD_REQUEST, "{e}");
let still: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM server_market").fetch_one(&f.t.db).await.unwrap();
@@ -136,7 +160,8 @@ async fn guild_market_listings_and_purchases() {
assert_eq!(s, StatusCode::OK, "{r}");
assert_eq!(r["new_balance"], 380.0);
assert_eq!(f.guild_balance().await, 120.0, "the sale is paid into the listing guild's bank");
let alex_balance: Option<f64> = sqlx::query_scalar("SELECT balance FROM server_economy WHERE uuid = ?").bind(&f.alex_uuid).fetch_optional(&f.t.db).await.unwrap();
let alex_balance: Option<f64> =
sqlx::query_scalar("SELECT balance FROM server_economy WHERE uuid = ?").bind(&f.alex_uuid).fetch_optional(&f.t.db).await.unwrap();
assert!(alex_balance.is_none(), "the lister is not paid personally");
let kinds: Vec<String> = sqlx::query_scalar("SELECT kind FROM guild_wallet_transactions ORDER BY id").fetch_all(&f.t.db).await.unwrap();
assert_eq!(kinds, ["purchase", "sale"]);
@@ -150,6 +175,7 @@ async fn guild_market_listings_and_purchases() {
let (s, r) = f.post("economy/market/buy", pay).await;
assert_eq!(s, StatusCode::OK, "{r}");
assert_eq!(r["new_balance"], 880.0);
let steve_balance: f64 = sqlx::query_scalar("SELECT balance FROM server_economy WHERE uuid = ?").bind(&f.steve_uuid).fetch_one(&f.t.db).await.unwrap();
let steve_balance: f64 =
sqlx::query_scalar("SELECT balance FROM server_economy WHERE uuid = ?").bind(&f.steve_uuid).fetch_one(&f.t.db).await.unwrap();
assert_eq!(steve_balance, 1120.0);
}
+14 -5
View File
@@ -6,7 +6,8 @@ async fn leader_approves_join_request_and_role_controls_claims() {
let t = setup().await;
let admin = t.login("admin", "supersecret").await;
for name in ["Leader", "Applicant"] {
let (status, body) = t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username":name,"password":"password123"}))).await;
let (status, body) =
t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username":name,"password":"password123"}))).await;
assert_eq!(status, StatusCode::OK, "{body}");
}
let leader = t.login("Leader", "password123").await;
@@ -14,7 +15,8 @@ async fn leader_approves_join_request_and_role_controls_claims() {
let applicant_uuid = t.uuid("Applicant").await;
let (_, server) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name":"SMP","instance_id":"smp"}))).await;
let server_id = server["server"]["id"].as_i64().unwrap();
let (status, guild) = t.call("POST", "/api/v1/guilds", Some(&leader), Some(json!({"instance_id":"smp","name":"Pimps","tag":"PIMP"}))).await;
let (status, guild) =
t.call("POST", "/api/v1/guilds", Some(&leader), Some(json!({"instance_id":"smp","name":"Pimps","tag":"PIMP"}))).await;
assert_eq!(status, StatusCode::OK, "{guild}");
let id = guild["id"].as_str().unwrap();
let path = format!("/api/v1/guilds/{id}");
@@ -23,11 +25,18 @@ async fn leader_approves_join_request_and_role_controls_claims() {
assert_eq!(t.call("GET", &format!("{path}/requests"), Some(&applicant), None).await.0, StatusCode::FORBIDDEN);
let (_, requests) = t.call("GET", &format!("{path}/requests"), Some(&leader), None).await;
assert_eq!(requests[0]["message"], "Let me in");
assert_eq!(t.call("POST", &format!("{path}/requests/{applicant_uuid}/respond"), Some(&leader), Some(json!({"accept":true}))).await.0, StatusCode::OK);
assert_eq!(
t.call("POST", &format!("{path}/requests/{applicant_uuid}/respond"), Some(&leader), Some(json!({"accept":true}))).await.0,
StatusCode::OK
);
let (status, role) = t.call("POST", &format!("{path}/roles"), Some(&leader), Some(json!({"name":"Builder","can_claim":false,"can_post":true}))).await;
let (status, role) =
t.call("POST", &format!("{path}/roles"), Some(&leader), Some(json!({"name":"Builder","can_claim":false,"can_post":true}))).await;
assert_eq!(status, StatusCode::OK, "{role}");
assert_eq!(t.call("PUT", &format!("{path}/members/{applicant_uuid}/role"), Some(&leader), Some(json!({"role":"Builder"}))).await.0, StatusCode::OK);
assert_eq!(
t.call("PUT", &format!("{path}/members/{applicant_uuid}/role"), Some(&leader), Some(json!({"role":"Builder"}))).await.0,
StatusCode::OK
);
let claim = json!({"server_id":server_id,"dimension":"minecraft:overworld","chunk_x":1,"chunk_z":1});
assert_eq!(t.call("POST", &format!("{path}/claim"), Some(&applicant), Some(claim)).await.0, StatusCode::FORBIDDEN);
let role_id = role["id"].as_i64().unwrap();
+65 -15
View File
@@ -24,7 +24,14 @@ impl Env {
(self.t.login(name, "password123").await, self.t.uuid(name).await)
}
async fn report(&self, name: &str, data: Value) -> (StatusCode, Value) {
self.t.call("POST", "/api/server/v1/integrations/report", Some(&self.server), Some(json!({"name": name, "version": "5.4.1", "data": data}))).await
self.t
.call(
"POST",
"/api/server/v1/integrations/report",
Some(&self.server),
Some(json!({"name": name, "version": "5.4.1", "data": data})),
)
.await
}
async fn sync(&self, online: &[(&str, &str)]) -> Value {
let online: Vec<Value> = online.iter().map(|(u, n)| json!({"uuid": u, "name": n})).collect();
@@ -48,19 +55,31 @@ async fn luckperms_ranks_mapping_and_sync_modes() {
}
let (_, groups) = e.t.call("GET", "/api/admin/groups", Some(&e.admin), None).await;
let id = |n: &str| groups.as_array().unwrap().iter().find(|g| g["name"] == n).unwrap()["id"].as_i64().unwrap();
let (s, _) = e.t.call("PUT", &format!("/api/admin/groups/{}/luckperms", id("VIP")), Some(&e.admin), Some(json!({"luckperms_group": "vip"}))).await;
let (s, _) =
e.t.call("PUT", &format!("/api/admin/groups/{}/luckperms", id("VIP")), Some(&e.admin), Some(json!({"luckperms_group": "vip"})))
.await;
assert_eq!(s, StatusCode::OK);
let (s, _) = e.t.call("PUT", &format!("/api/admin/groups/{}/luckperms", id("Staff")), Some(&e.admin), Some(json!({"luckperms_group": "bad group!"}))).await;
let (s, _) =
e.t.call(
"PUT",
&format!("/api/admin/groups/{}/luckperms", id("Staff")),
Some(&e.admin),
Some(json!({"luckperms_group": "bad group!"})),
)
.await;
assert_eq!(s, StatusCode::BAD_REQUEST);
e.t.call("PUT", &format!("/api/admin/groups/{}/luckperms", id("Staff")), Some(&e.admin), Some(json!({"luckperms_group": "Moderator"}))).await;
e.t.call("PUT", &format!("/api/admin/groups/{}/luckperms", id("Staff")), Some(&e.admin), Some(json!({"luckperms_group": "Moderator"})))
.await;
let lp = |steve_groups: Value| json!({
let lp = |steve_groups: Value| {
json!({
"groups": [{"name": "vip", "weight": 10}, {"name": "moderator", "weight": 50}],
"players": [
{"uuid": steve, "primary": "VIP", "display": "VIP", "prefix": "§6[VIP] ", "weight": 10, "groups": steve_groups, "permissions": ["scopenet.command.*", "essentials.fly"]},
{"uuid": alex, "primary": "default", "groups": ["default"]},
{"uuid": "not-a-uuid", "primary": "x"}
]});
]})
};
// Default mode is display-only: ranks are stored, no group changes.
let (s, r) = e.report("luckperms", lp(json!(["default", "vip"]))).await;
@@ -77,7 +96,8 @@ async fn luckperms_ranks_mapping_and_sync_modes() {
assert_eq!(view["integrations"][0]["data"]["groups"][1]["name"], "moderator");
// game_to_panel: mapped groups follow LuckPerms, including removal.
let (s, _) = e.t.call("PUT", "/api/admin/integrations/settings", Some(&e.admin), Some(json!({"luckperms_sync": "game_to_panel"}))).await;
let (s, _) =
e.t.call("PUT", "/api/admin/integrations/settings", Some(&e.admin), Some(json!({"luckperms_sync": "game_to_panel"}))).await;
assert_eq!(s, StatusCode::OK);
e.report("luckperms", lp(json!(["default", "vip", "moderator"]))).await;
assert_eq!(e.groups_of("Steve").await, ["Staff", "VIP"]);
@@ -86,7 +106,11 @@ async fn luckperms_ranks_mapping_and_sync_modes() {
assert!(r["config"]["assign"].as_array().unwrap().is_empty());
// panel_to_game: the panel's groups are pushed to the game as assignments.
sqlx::query("INSERT INTO user_groups (user_id, group_id) SELECT u.id, ? FROM users u WHERE u.username = 'Alex'").bind(id("VIP")).execute(&e.t.db).await.unwrap();
sqlx::query("INSERT INTO user_groups (user_id, group_id) SELECT u.id, ? FROM users u WHERE u.username = 'Alex'")
.bind(id("VIP"))
.execute(&e.t.db)
.await
.unwrap();
e.t.call("PUT", "/api/admin/integrations/settings", Some(&e.admin), Some(json!({"luckperms_sync": "panel_to_game"}))).await;
let (_, r) = e.report("luckperms", lp(json!(["default", "moderator"]))).await;
let assign = r["config"]["assign"].as_array().unwrap();
@@ -107,7 +131,11 @@ async fn luckperms_ranks_mapping_and_sync_modes() {
async fn other_integrations_are_stored_per_server() {
let e = env().await;
e.report("spark", json!({"tps": {"m1": 19.98}, "mspt": {"mean": 12.5, "p95": 31.0}})).await;
e.report("worldguard", json!({"regions": [{"world": "world", "id": "spawn", "min": [-50, 0, -50], "max": [50, 255, 50], "owners": ["admin"]}]})).await;
e.report(
"worldguard",
json!({"regions": [{"world": "world", "id": "spawn", "min": [-50, 0, -50], "max": [50, 255, 50], "owners": ["admin"]}]}),
)
.await;
e.report("spark", json!({"tps": {"m1": 20.0}})).await;
let (_, view) = e.t.call("GET", &format!("/api/admin/servers/{}/integrations", e.sid), Some(&e.admin), None).await;
let names: Vec<&str> = view["integrations"].as_array().unwrap().iter().map(|i| i["name"].as_str().unwrap()).collect();
@@ -127,7 +155,11 @@ async fn game_servers_receive_events_with_their_sync() {
// Level-ups: global and per-server.
let adjust = |scope: &str, mode: &str, amount: i64| {
let (t, admin, uuid, sid) = (&e.t, e.admin.clone(), steve.clone(), e.sid);
let body = if scope == "global" { json!({"scope": "global", "mode": mode, "amount": amount}) } else { json!({"scope": "server", "server_id": sid, "mode": mode, "amount": amount}) };
let body = if scope == "global" {
json!({"scope": "global", "mode": mode, "amount": amount})
} else {
json!({"scope": "server", "server_id": sid, "mode": mode, "amount": amount})
};
async move { t.call("POST", &format!("/api/admin/progression/players/{uuid}/adjust"), Some(&admin), Some(body)).await }
};
adjust("global", "set_xp", 10).await;
@@ -135,7 +167,8 @@ async fn game_servers_receive_events_with_their_sync() {
adjust("server", "set_xp", 10).await;
adjust("server", "set_xp", 300).await; // level 1 -> 2
let n = e.sync(&[(&steve, "Steve")]).await["notifications"].clone();
let kinds: Vec<(&str, &str)> = n.as_array().unwrap().iter().map(|x| (x["kind"].as_str().unwrap(), x["data"]["scope"].as_str().unwrap_or(""))).collect();
let kinds: Vec<(&str, &str)> =
n.as_array().unwrap().iter().map(|x| (x["kind"].as_str().unwrap(), x["data"]["scope"].as_str().unwrap_or(""))).collect();
assert_eq!(kinds, [("level_up", "global"), ("level_up", "server")], "{n}");
assert_eq!((n[0]["data"]["previous"].as_i64(), n[0]["data"]["level"].as_i64()), (Some(1), Some(5)));
assert_eq!(n[0]["uuid"], steve);
@@ -147,10 +180,27 @@ async fn game_servers_receive_events_with_their_sync() {
// Achievements and guild changes.
let ach: String = sqlx::query_scalar("SELECT id FROM achievements LIMIT 1").fetch_one(&e.t.db).await.unwrap();
sqlx::query("INSERT INTO user_achievements (user_uuid, achievement_id, unlocked_at) VALUES (?, ?, 'now')").bind(&steve).bind(&ach).execute(&e.t.db).await.unwrap();
sqlx::query("INSERT INTO guilds (id, instance_id, name, tag, leader_uuid, created_at) VALUES ('g1', 'smp', 'Iron', 'IRON', ?, 'now')").bind(&alex).execute(&e.t.db).await.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES ('g1', ?, 'Steve', 'member', 'now')").bind(&steve).execute(&e.t.db).await.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES ('g1', ?, 'Alex', 'leader', 'now')").bind(&alex).execute(&e.t.db).await.unwrap();
sqlx::query("INSERT INTO user_achievements (user_uuid, achievement_id, unlocked_at) VALUES (?, ?, 'now')")
.bind(&steve)
.bind(&ach)
.execute(&e.t.db)
.await
.unwrap();
sqlx::query("INSERT INTO guilds (id, instance_id, name, tag, leader_uuid, created_at) VALUES ('g1', 'smp', 'Iron', 'IRON', ?, 'now')")
.bind(&alex)
.execute(&e.t.db)
.await
.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES ('g1', ?, 'Steve', 'member', 'now')")
.bind(&steve)
.execute(&e.t.db)
.await
.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES ('g1', ?, 'Alex', 'leader', 'now')")
.bind(&alex)
.execute(&e.t.db)
.await
.unwrap();
sqlx::query("DELETE FROM guild_members WHERE uuid = ?").bind(&steve).execute(&e.t.db).await.unwrap();
let n = e.sync(&[(&steve, "Steve")]).await["notifications"].clone();
let kinds: Vec<&str> = n.as_array().unwrap().iter().map(|x| x["kind"].as_str().unwrap()).collect();
+37 -6
View File
@@ -34,7 +34,13 @@ async fn world() -> World {
impl World {
async fn online(&self, uuid: &str, name: &str) {
sqlx::query("INSERT OR REPLACE INTO server_online (server_id, uuid, name, joined_at) VALUES (?, ?, ?, '2026-01-01T00:00:00Z')").bind(self.sid).bind(uuid).bind(name).execute(&self.t.db).await.unwrap();
sqlx::query("INSERT OR REPLACE INTO server_online (server_id, uuid, name, joined_at) VALUES (?, ?, ?, '2026-01-01T00:00:00Z')")
.bind(self.sid)
.bind(uuid)
.bind(name)
.execute(&self.t.db)
.await
.unwrap();
}
async fn poll(&self) -> Value {
self.t.call("POST", "/api/server/v1/actions/poll", Some(&self.server), Some(json!({}))).await.1
@@ -55,7 +61,14 @@ async fn tpa_and_messages_are_queued_once_for_the_game_server() {
w.online(&w.alex_uuid, "Alex").await;
assert_eq!(w.t.call("POST", &path, Some(&w.alex), Some(tpa)).await.0, StatusCode::OK);
let (s, r) = w.t.call("POST", &path, Some(&w.alex), Some(json!({"action": "message", "target_uuid": w.steve_uuid, "text": " hi\n\tthere\u{7} friend "}))).await;
let (s, r) =
w.t.call(
"POST",
&path,
Some(&w.alex),
Some(json!({"action": "message", "target_uuid": w.steve_uuid, "text": " hi\n\tthere\u{7} friend "})),
)
.await;
assert_eq!(s, StatusCode::OK, "{r}");
let polled = w.poll().await;
@@ -70,7 +83,10 @@ async fn tpa_and_messages_are_queued_once_for_the_game_server() {
assert_eq!(bad(json!({"action": "message", "target_uuid": w.steve_uuid, "text": " "})).await.0, StatusCode::BAD_REQUEST);
assert_eq!(bad(json!({"action": "kill", "target_uuid": w.steve_uuid})).await.0, StatusCode::BAD_REQUEST);
assert_eq!(bad(json!({"action": "tpa", "target_uuid": w.alex_uuid})).await.0, StatusCode::BAD_REQUEST);
assert_eq!(w.t.call("POST", &path, None, Some(json!({"action": "tpa", "target_uuid": w.steve_uuid}))).await.0, StatusCode::UNAUTHORIZED);
assert_eq!(
w.t.call("POST", &path, None, Some(json!({"action": "tpa", "target_uuid": w.steve_uuid}))).await.0,
StatusCode::UNAUTHORIZED
);
// A burst is slowed down.
let mut last = StatusCode::OK;
@@ -91,7 +107,14 @@ async fn guild_invites_need_consent() {
assert_eq!(s, StatusCode::OK, "{r}");
let in_guild = |uuid: &str| {
let (db, gid, uuid) = (w.t.db.clone(), w.gid.clone(), uuid.to_string());
async move { sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM guild_members WHERE guild_id = ? AND uuid = ?)").bind(gid).bind(uuid).fetch_one(&db).await.unwrap() }
async move {
sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM guild_members WHERE guild_id = ? AND uuid = ?)")
.bind(gid)
.bind(uuid)
.fetch_one(&db)
.await
.unwrap()
}
};
assert!(!in_guild(&w.steve_uuid).await, "nobody is added without saying yes");
@@ -125,14 +148,22 @@ async fn invites_work_in_game_too() {
};
let (s, r) = server("guilds/invite/send", json!({"uuid": w.alex_uuid, "target": "steve"})).await;
assert_eq!(s, StatusCode::OK, "{r}");
assert_eq!(server("guilds/invite/send", json!({"uuid": w.steve_uuid, "target": "alex"})).await.0, StatusCode::FORBIDDEN, "not a leader");
assert_eq!(
server("guilds/invite/send", json!({"uuid": w.steve_uuid, "target": "alex"})).await.0,
StatusCode::FORBIDDEN,
"not a leader"
);
assert_eq!(server("guilds/invite/send", json!({"uuid": w.alex_uuid, "target": "nobody"})).await.0, StatusCode::NOT_FOUND);
let (_, list) = server("guilds/invite/list", json!({"uuid": w.steve_uuid})).await;
assert_eq!(list["invites"][0]["guild_tag"], "IRON");
let (s, r) = server("guilds/invite/respond", json!({"uuid": w.steve_uuid, "tag": "iron", "accept": false})).await;
assert_eq!((s, r["accepted"].clone()), (StatusCode::OK, json!(false)));
assert_eq!(server("guilds/invite/respond", json!({"uuid": w.steve_uuid, "accept": true})).await.0, StatusCode::NOT_FOUND, "a declined invitation is gone");
assert_eq!(
server("guilds/invite/respond", json!({"uuid": w.steve_uuid, "accept": true})).await.0,
StatusCode::NOT_FOUND,
"a declined invitation is gone"
);
server("guilds/invite/send", json!({"uuid": w.alex_uuid, "target": "Steve"})).await;
let (s, r) = server("guilds/invite/respond", json!({"uuid": w.steve_uuid, "accept": true})).await;
+25 -3
View File
@@ -13,14 +13,29 @@ async fn servers_in_an_economy_group_share_balances() {
let mut tokens = vec![];
let mut ids = vec![];
for (name, group) in [("SMP", "network"), ("Survival", "Network"), ("Modded", "")] {
let (s, r) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": name, "instance_id": name.to_lowercase(), "economy_group": group}))).await;
let (s, r) = t
.call(
"POST",
"/api/admin/servers",
Some(&admin),
Some(json!({"name": name, "instance_id": name.to_lowercase(), "economy_group": group})),
)
.await;
assert_eq!(s, StatusCode::OK, "{r}");
tokens.push(r["token"].as_str().unwrap().to_string());
ids.push(r["server"]["id"].as_i64().unwrap());
}
let adjust = |token: String, op: &'static str, delta: f64| {
let (t, uuid) = (&t, uuid.clone());
async move { t.call("POST", "/api/server/v1/economy/adjust", Some(&token), Some(json!({"uuid": uuid, "username": "Alex", "delta": delta, "operation_id": op, "description": "test"}))).await }
async move {
t.call(
"POST",
"/api/server/v1/economy/adjust",
Some(&token),
Some(json!({"uuid": uuid, "username": "Alex", "delta": delta, "operation_id": op, "description": "test"})),
)
.await
}
};
let balance = |token: String| {
let (t, uuid) = (&t, uuid.clone());
@@ -38,7 +53,14 @@ async fn servers_in_an_economy_group_share_balances() {
assert_eq!(balance(tokens[2].clone()).await.1["balance"], 1000.0);
// Bad group names are refused.
let (s, _) = t.call("PUT", &format!("/api/admin/servers/{}", ids[2]), Some(&admin), Some(json!({"name": "Modded", "instance_id": "modded", "economy_group": "a<b>"}))).await;
let (s, _) = t
.call(
"PUT",
&format!("/api/admin/servers/{}", ids[2]),
Some(&admin),
Some(json!({"name": "Modded", "instance_id": "modded", "economy_group": "a<b>"})),
)
.await;
assert_eq!(s, StatusCode::BAD_REQUEST);
// Removing the server that holds the shared balances keeps them for the rest of the group.
+8 -2
View File
@@ -20,7 +20,12 @@ async fn profile_shows_the_whole_player() {
sqlx::query("INSERT INTO friendships (user_uuid, friend_uuid, status, action_uuid, created_at, updated_at) VALUES (?, ?, 'accepted', ?, 'x', 'x')").bind(a).bind(b).bind(a).execute(&t.db).await.unwrap();
}
t.call("POST", "/api/v1/guilds", Some(&steve), Some(json!({"instance_id": "smp", "name": "Iron", "tag": "IRON"}))).await;
sqlx::query("INSERT INTO server_economy (server_id, uuid, username, balance, updated_at) VALUES (?, ?, 'Steve', 4321.5, 'x')").bind(sid).bind(&su).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO server_economy (server_id, uuid, username, balance, updated_at) VALUES (?, ?, 'Steve', 4321.5, 'x')")
.bind(sid)
.bind(&su)
.execute(&t.db)
.await
.unwrap();
sqlx::query("INSERT INTO player_ranks (server_id, uuid, primary_group, display, prefix, groups, permissions, weight, updated_at) VALUES (?, ?, 'vip', 'VIP', '[VIP]', '[\"vip\"]', '[]', 10, 'x')").bind(sid).bind(&su).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO player_stats (server_id, uuid, name, playtime_secs, first_seen, last_seen) VALUES (?, ?, 'Steve', 7200, 'x', '2026-01-01T00:00:00Z')").bind(sid).bind(&su).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO server_events (server_id, uuid, name, kind, detail, created_at) VALUES (?, ?, 'Steve', 'join', NULL, '2026-01-01T00:00:00Z'), (?, ?, 'Steve', 'chat', 'secret', '2026-01-01T00:00:01Z')").bind(sid).bind(&su).bind(sid).bind(&su).execute(&t.db).await.unwrap();
@@ -43,6 +48,7 @@ async fn profile_shows_the_whole_player() {
let (s, _) = t.call("PUT", "/api/v1/profiles/me", Some(&alex), Some(json!({"bio": "hi", "accent_color": "#22D3EE"}))).await;
assert_eq!(s, StatusCode::OK);
assert_eq!(t.call("GET", &format!("/api/v1/profiles/{au}"), Some(&alex), None).await.1["accent_color"], "#22d3ee");
let (s, _) = t.call("PUT", "/api/v1/profiles/me", Some(&alex), Some(json!({"bio": "hi", "accent_color": "red; background:url(x)"}))).await;
let (s, _) =
t.call("PUT", "/api/v1/profiles/me", Some(&alex), Some(json!({"bio": "hi", "accent_color": "red; background:url(x)"}))).await;
assert_eq!(s, StatusCode::BAD_REQUEST);
}
+7 -1
View File
@@ -47,6 +47,12 @@ async fn the_landing_page_decides_what_the_public_sees_of_the_map() {
// Another server's map isn't exposed, and turning the section off closes it.
assert_eq!(t.call("GET", "/api/v1/public/map/999", None, None).await.0, StatusCode::NOT_FOUND);
t.call("PUT", "/api/admin/landing", Some(&admin), Some(json!({"blocks": [{"id": "m1", "type": "map", "enabled": false, "options": {}}]}))).await;
t.call(
"PUT",
"/api/admin/landing",
Some(&admin),
Some(json!({"blocks": [{"id": "m1", "type": "map", "enabled": false, "options": {}}]})),
)
.await;
assert_eq!(t.call("GET", "/api/v1/public/map/0", None, None).await.0, StatusCode::NOT_FOUND);
}
+31 -4
View File
@@ -15,14 +15,28 @@ async fn renaming_a_rank_title_reaches_players() {
.bind(sid).bind(&uuid).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO user_levels (uuid, global_xp, global_level, updated_at) VALUES (?, 100000, 10, 'now') ON CONFLICT(uuid) DO UPDATE SET global_xp = 100000").bind(&uuid).execute(&t.db).await.unwrap();
let (s, made) = t.call("POST", "/api/admin/rewards", Some(&admin), Some(json!({"level_type":"server","server_id":sid,"level_req":2,"reward_type":"title","reward_name":"Squire"}))).await;
let (s, made) = t
.call(
"POST",
"/api/admin/rewards",
Some(&admin),
Some(json!({"level_type":"server","server_id":sid,"level_req":2,"reward_type":"title","reward_name":"Squire"})),
)
.await;
assert_eq!(s, StatusCode::OK, "{made}");
let id = made["id"].as_i64().unwrap();
let rank = |v: &Value| v["server_levels"][0]["rank_name"].clone();
let (_, levels) = t.call("GET", &format!("/api/v1/levels/player/{uuid}"), Some(&steve), None).await;
assert_eq!(rank(&levels), "Squire", "{levels}");
let (s, r) = t.call("PUT", &format!("/api/admin/rewards/{id}"), Some(&admin), Some(json!({"level_type":"server","server_id":sid,"level_req":2,"reward_type":"title","reward_name":"Knight"}))).await;
let (s, r) = t
.call(
"PUT",
&format!("/api/admin/rewards/{id}"),
Some(&admin),
Some(json!({"level_type":"server","server_id":sid,"level_req":2,"reward_type":"title","reward_name":"Knight"})),
)
.await;
assert_eq!(s, StatusCode::OK, "{r}");
let (_, levels) = t.call("GET", &format!("/api/v1/levels/player/{uuid}"), Some(&steve), None).await;
assert_eq!(rank(&levels), "Knight", "a renamed rank shows up straight away");
@@ -40,11 +54,24 @@ async fn renaming_a_global_title_reaches_the_profile_and_reward_list() {
let steve = t.login("Steve", "password123").await;
let uuid = t.uuid("Steve").await;
sqlx::query("INSERT INTO user_levels (uuid, global_xp, global_level, updated_at) VALUES (?, 100000, 10, 'now') ON CONFLICT(uuid) DO UPDATE SET global_xp = 100000").bind(&uuid).execute(&t.db).await.unwrap();
let (_, made) = t.call("POST", "/api/admin/levels/rewards", Some(&admin), Some(json!({"level": 100, "reward_type": "title", "reward_value": "Explorer"}))).await;
let (_, made) = t
.call(
"POST",
"/api/admin/levels/rewards",
Some(&admin),
Some(json!({"level": 100, "reward_type": "title", "reward_value": "Explorer"})),
)
.await;
let id = made["id"].as_i64().unwrap();
let (_, me) = t.call("GET", "/api/v1/levels/me", Some(&steve), None).await;
assert_eq!(me["title"], "Explorer", "{me}");
t.call("PUT", &format!("/api/admin/levels/rewards/{id}"), Some(&admin), Some(json!({"level": 100, "reward_type": "title", "reward_value": "Pathfinder"}))).await;
t.call(
"PUT",
&format!("/api/admin/levels/rewards/{id}"),
Some(&admin),
Some(json!({"level": 100, "reward_type": "title", "reward_value": "Pathfinder"})),
)
.await;
let (_, me) = t.call("GET", "/api/v1/levels/me", Some(&steve), None).await;
assert_eq!(me["title"], "Pathfinder", "{me}");
let rewards = me["rewards"].to_string();
+92 -18
View File
@@ -24,16 +24,29 @@ async fn an_achievement_can_pay_money_chunks_items_and_permissions() {
// 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/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": [
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;
{"type": "message", "text": "Well done!"}]})),
)
.await;
assert_eq!(s, StatusCode::OK, "{saved}");
assert_eq!(saved["actions"][2]["item"], "minecraft:diamond");
@@ -44,17 +57,32 @@ async fn an_achievement_can_pay_money_chunks_items_and_permissions() {
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();
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();
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();
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.
@@ -69,7 +97,8 @@ async fn an_achievement_can_pay_money_chunks_items_and_permissions() {
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 (_, 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}");
}
@@ -86,7 +115,12 @@ async fn deliveries_wait_for_the_player_to_be_online() {
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();
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);
}
@@ -99,22 +133,62 @@ async fn a_rank_milestone_can_grant_more_than_its_title() {
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 (_, 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;
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;
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();
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());
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());
}
#[tokio::test]
+42 -9
View File
@@ -68,12 +68,20 @@ async fn search_befriend_message_invite() {
/// Every table and column is scanned, so a table added later is covered too.
async fn mentions(t: &TestApp, needle: &str) -> Vec<String> {
let tables: Vec<String> = sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").fetch_all(&t.db).await.unwrap();
let tables: Vec<String> = sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
.fetch_all(&t.db)
.await
.unwrap();
let mut hits = Vec::new();
for table in tables {
let cols: Vec<String> = sqlx::query_scalar(&format!("SELECT name FROM pragma_table_info('{table}')")).fetch_all(&t.db).await.unwrap();
let cols: Vec<String> =
sqlx::query_scalar(&format!("SELECT name FROM pragma_table_info('{table}')")).fetch_all(&t.db).await.unwrap();
for col in cols {
let n: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM \"{table}\" WHERE CAST(\"{col}\" AS TEXT) = ?")).bind(needle).fetch_one(&t.db).await.unwrap();
let n: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM \"{table}\" WHERE CAST(\"{col}\" AS TEXT) = ?"))
.bind(needle)
.fetch_one(&t.db)
.await
.unwrap();
if n > 0 {
hits.push(format!("{table}.{col}"));
}
@@ -130,10 +138,16 @@ async fn deleting_an_account_removes_all_its_data() {
.bind(sid).bind(&steve_uuid).execute(&t.db).await.unwrap();
// Guilds: Steve leads one with Alex in it, and is sole member of another.
let (s, g) = t.call("POST", "/api/v1/guilds", Some(&steve), Some(json!({"instance_id": "smp", "name": "Rockheads", "tag": "ROCK"}))).await;
let (s, g) =
t.call("POST", "/api/v1/guilds", Some(&steve), Some(json!({"instance_id": "smp", "name": "Rockheads", "tag": "ROCK"}))).await;
assert_eq!(s, StatusCode::OK, "{g}");
let gid = g["id"].as_str().unwrap().to_string();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES (?, ?, 'Alex', 'member', '2026-01-01')").bind(&gid).bind(&alex_uuid).execute(&t.db).await.unwrap();
sqlx::query("INSERT INTO guild_members (guild_id, uuid, name, role, joined_at) VALUES (?, ?, 'Alex', 'member', '2026-01-01')")
.bind(&gid)
.bind(&alex_uuid)
.execute(&t.db)
.await
.unwrap();
sqlx::query("INSERT INTO guild_wallet_transactions (server_id, guild_id, actor_uuid, kind, amount, created_at) VALUES (?, ?, ?, 'deposit', 5, 'now')").bind(sid).bind(&gid).bind(&steve_uuid).execute(&t.db).await.unwrap();
let (s, g2) = t.call("POST", "/api/v1/guilds", Some(&admin), Some(json!({"instance_id": "smp", "name": "Solo", "tag": "SOLO"}))).await;
assert_eq!(s, StatusCode::OK, "{g2}");
@@ -147,11 +161,26 @@ async fn deleting_an_account_removes_all_its_data() {
// Nothing refers to them any more (their name appears only as plain text in free-form rows we don't keep).
assert_eq!(mentions(&t, &steve_uuid).await, vec!["reserved_usernames.uuid".to_string()]);
for table in ["users", "user_levels", "player_stats", "friendships", "direct_messages", "user_profiles", "user_posts", "server_market", "server_economy"] {
let n: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table} WHERE CAST(uuid AS TEXT) = ?")).bind(&steve_uuid).fetch_one(&t.db).await.unwrap_or(0);
for table in [
"users",
"user_levels",
"player_stats",
"friendships",
"direct_messages",
"user_profiles",
"user_posts",
"server_market",
"server_economy",
] {
let n: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table} WHERE CAST(uuid AS TEXT) = ?"))
.bind(&steve_uuid)
.fetch_one(&t.db)
.await
.unwrap_or(0);
assert_eq!(n, 0, "{table}");
}
let name_hits: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username = 'Steve' COLLATE NOCASE").fetch_one(&t.db).await.unwrap();
let name_hits: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username = 'Steve' COLLATE NOCASE").fetch_one(&t.db).await.unwrap();
assert_eq!(name_hits, 0);
// Other people keep what is theirs: Alex is now the guild's leader; the
@@ -160,7 +189,11 @@ async fn deleting_an_account_removes_all_its_data() {
assert_eq!(leader, alex_uuid);
let from: String = sqlx::query_scalar("SELECT from_name FROM economy_transactions").fetch_one(&t.db).await.unwrap();
assert_eq!(from, "Deleted player");
let likes: i64 = sqlx::query_scalar("SELECT likes_count FROM user_posts WHERE id = ?").bind(alex_post["id"].as_i64().unwrap()).fetch_one(&t.db).await.unwrap();
let likes: i64 = sqlx::query_scalar("SELECT likes_count FROM user_posts WHERE id = ?")
.bind(alex_post["id"].as_i64().unwrap())
.fetch_one(&t.db)
.await
.unwrap();
assert_eq!(likes, 0);
let (_, friends) = t.call("GET", "/api/v1/friends", Some(&alex), None).await;
assert!(friends.as_array().unwrap().is_empty());
+17 -3
View File
@@ -102,7 +102,10 @@ async fn players_and_overlay_are_served_to_viewers() {
assert_eq!(s, StatusCode::OK);
let overlay = json!({"claims": [{"guild_id": "g1", "name": "Iron", "tag": "IRON", "color": 123}], "pins": [{"id": "spawn-0", "kind": "spawn", "label": "Spawn"}]});
assert_eq!(w.t.call("POST", "/api/server/v1/map/overlay", Some(&w.server), Some(overlay)).await.0, StatusCode::OK);
assert_eq!(w.t.call("POST", "/api/server/v1/map/overlay", Some(&w.server), Some(json!({"claims": "nope"}))).await.0, StatusCode::BAD_REQUEST);
assert_eq!(
w.t.call("POST", "/api/server/v1/map/overlay", Some(&w.server), Some(json!({"claims": "nope"}))).await.0,
StatusCode::BAD_REQUEST
);
let (s, o) = w.t.call("GET", &format!("/api/v1/servers/{}/map/overlay", w.sid), Some(&w.alex), None).await;
assert_eq!(s, StatusCode::OK);
@@ -124,9 +127,20 @@ async fn the_map_can_be_switched_off_and_reset() {
assert_eq!(cfg["epoch"], 1, "game servers see a new epoch and redraw");
let (_, info) = w.t.call("GET", &format!("/api/v1/servers/{}/map", w.sid), Some(&w.alex), None).await;
assert_eq!(info["ready"], false);
assert_eq!(w.t.call("GET", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.alex), None).await.0, StatusCode::FORBIDDEN, "admin only");
assert_eq!(
w.t.call("GET", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.alex), None).await.0,
StatusCode::FORBIDDEN,
"admin only"
);
let (s, _) = w.t.call("PUT", &format!("/api/admin/servers/{}", w.sid), Some(&w.admin), Some(json!({"name": "SMP", "instance_id": "smp", "map_enabled": false}))).await;
let (s, _) =
w.t.call(
"PUT",
&format!("/api/admin/servers/{}", w.sid),
Some(&w.admin),
Some(json!({"name": "SMP", "instance_id": "smp", "map_enabled": false})),
)
.await;
assert_eq!(s, StatusCode::OK);
assert_eq!(w.upload("minecraft:overworld", record(0, 0, 0, b"a")).await.0, StatusCode::FORBIDDEN);
let (_, cfg) = w.t.call("GET", "/api/server/v1/map/config", Some(&w.server), None).await;