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
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:
1 parent
ce49447b6e
commit
60edc99780
58 files changed
+3533
-1375
No files matched your search
@@ -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,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user