feat: add global & server leveling, quests, achievements, guilds land-claiming, social & toggleable configs

This commit is contained in:
SCOPEDD committed 2026-09-29 03:24:27 -04:00
1 parent 4b79f69c20
commit b4cef85b1f
40 files changed
+10261 -27

No files matched your search

+341
View File
@@ -0,0 +1,341 @@
//! Integration tests for v2 features:
//! - Global & Server Leveling
//! - Daily & Weekly Quests
//! - Guilds & Grief Protection Land Claiming
//! - Friends, DMs, Posts & Player Profiles
//! - Server Feature Toggles (config bypass)
mod common;
use common::*;
async fn setup_users(t: &TestApp) -> (String, String, String, String) {
let admin = t.login("admin", "supersecret").await;
let (s, v1) = t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Alex", "password": "password123"}))).await;
assert_eq!(s, StatusCode::OK, "{v1}");
let (s, v2) = t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Steve", "password": "password123"}))).await;
assert_eq!(s, StatusCode::OK, "{v2}");
let alex_token = t.login("Alex", "password123").await;
let steve_token = t.login("Steve", "password123").await;
let alex_uuid = t.uuid("Alex").await;
let steve_uuid = t.uuid("Steve").await;
(alex_token, steve_token, alex_uuid, steve_uuid)
}
async fn create_test_server(t: &TestApp, admin: &str) -> (i64, String) {
let (s, v) = t.call("POST", "/api/admin/servers", Some(admin), Some(json!({"name": "Survival Hub"}))).await;
assert_eq!(s, StatusCode::OK, "{v}");
let server_id = v["server"]["id"].as_i64().unwrap();
let token = v["token"].as_str().unwrap().to_string();
(server_id, token)
}
#[tokio::test]
async fn leveling_and_quests_progression() {
let t = setup().await;
let admin = t.login("admin", "supersecret").await;
let (alex_token, _, alex_uuid, _) = setup_users(&t).await;
let (_server_id, server_token) = create_test_server(&t, &admin).await;
// 1. Initial level check
let (s, lvl) = t.call("GET", "/api/v1/leveling/me", Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK, "{lvl}");
assert_eq!(lvl["global_level"], 1);
assert_eq!(lvl["global_xp"], 0);
// 2. Fetch seeded quests
let (s, quests) = t.call("GET", "/api/v1/quests/me", Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK, "{quests}");
let quest_list = quests.as_array().unwrap();
assert!(!quest_list.is_empty(), "Expected seeded quests to be populated");
// 3. Game Server sync with leveling ENABLED
let (s, sync_res) = t
.call(
"POST",
"/api/server/v1/sync",
Some(&server_token),
Some(json!({
"features": {
"leveling_enabled": true,
"global_xp_multiplier": 1.0,
"server_xp_multiplier": 1.5,
"quests_enabled": true,
"achievements_enabled": true,
"guilds_enabled": true,
"land_claiming_enabled": true
},
"players": [
{"uuid": &alex_uuid, "username": "Alex", "ip": "127.0.0.1", "playtime_secs": 60}
],
"stats": [
{
"uuid": &alex_uuid,
"name": "Alex",
"blocks_broken": 20,
"blocks_placed": 10,
"mob_kills": 5,
"player_kills": 0,
"deaths": 0,
"playtime_secs": 60
}
],
"events": []
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{sync_res}");
// 4. Verify Alex gained XP from the server actions
let (s, lvl_after) = t.call("GET", "/api/v1/leveling/me", Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK, "{lvl_after}");
let current_xp = lvl_after["global_xp"].as_i64().unwrap();
assert!(current_xp > 0, "Alex should have earned XP");
// 5. Game Server sync with leveling DISABLED (server host turned it off in config)
let (s, sync_off) = t
.call(
"POST",
"/api/server/v1/sync",
Some(&server_token),
Some(json!({
"features": {
"leveling_enabled": false,
"quests_enabled": false
},
"players": [
{"uuid": &alex_uuid, "username": "Alex", "ip": "127.0.0.1", "playtime_secs": 120}
],
"stats": [
{
"uuid": &alex_uuid,
"name": "Alex",
"blocks_broken": 50,
"blocks_placed": 50,
"mob_kills": 20,
"player_kills": 0,
"deaths": 0,
"playtime_secs": 60
}
],
"events": []
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{sync_off}");
// Verify XP did NOT increase when leveling is disabled
let (s, lvl_no_change) = t.call("GET", "/api/v1/leveling/me", Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK);
assert_eq!(lvl_no_change["global_xp"].as_i64().unwrap(), current_xp);
}
#[tokio::test]
async fn guilds_and_land_claims() {
let t = setup().await;
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;
// 1. Alex founds a guild
let (s, guild) = t
.call(
"POST",
"/api/v1/guilds",
Some(&alex_token),
Some(json!({
"instance_id": "smp-pack",
"name": "Iron Fortress",
"tag": "IRON",
"description": "Defenders of the realm"
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{guild}");
let guild_id = guild["id"].as_str().unwrap().to_string();
assert_eq!(guild["tag"], "IRON");
// 2. Alex claims chunk (3, 7) in the Overworld
let (s, claim) = t
.call(
"POST",
&format!("/api/v1/guilds/{guild_id}/claim"),
Some(&alex_token),
Some(json!({
"server_id": server_id,
"instance_id": "smp-pack",
"dimension": "minecraft:overworld",
"chunk_x": 3,
"chunk_z": 7
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{claim}");
let claim_id = claim["id"].as_i64().unwrap();
// 3. Server checks chunk permissions for in-game protection
// Alex (guild leader) attempts to build -> allowed
let (s, check_alex) = t
.call(
"POST",
"/api/server/v1/guilds/check-chunk",
Some(&server_token),
Some(json!({
"dimension": "minecraft:overworld",
"chunk_x": 3,
"chunk_z": 7,
"uuid": &alex_uuid
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{check_alex}");
assert_eq!(check_alex["allowed"], true);
// Steve (non-member) attempts to build -> DENIED with grief protection reason
let (s, check_steve) = t
.call(
"POST",
"/api/server/v1/guilds/check-chunk",
Some(&server_token),
Some(json!({
"dimension": "minecraft:overworld",
"chunk_x": 3,
"chunk_z": 7,
"uuid": &steve_uuid
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{check_steve}");
assert_eq!(check_steve["allowed"], false);
assert_eq!(check_steve["claimed"], true);
// Unclaimed wilderness chunk (100, 100) -> allowed for all players
let (s, check_wild) = t
.call(
"POST",
"/api/server/v1/guilds/check-chunk",
Some(&server_token),
Some(json!({
"dimension": "minecraft:overworld",
"chunk_x": 100,
"chunk_z": 100,
"uuid": &steve_uuid
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{check_wild}");
assert_eq!(check_wild["allowed"], true);
// 4. Alex unclaims the chunk
let (s, _) = t.call("DELETE", &format!("/api/v1/guilds/claims/{claim_id}"), Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK);
// After unclaiming, Steve is now allowed to build at (3, 7)
let (s, check_unclaimed) = t
.call(
"POST",
"/api/server/v1/guilds/check-chunk",
Some(&server_token),
Some(json!({
"dimension": "minecraft:overworld",
"chunk_x": 3,
"chunk_z": 7,
"uuid": &steve_uuid
})),
)
.await;
assert_eq!(s, StatusCode::OK);
assert_eq!(check_unclaimed["allowed"], true);
}
#[tokio::test]
async fn social_friends_dms_and_profiles() {
let t = setup().await;
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;
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})),
)
.await;
assert_eq!(s, StatusCode::OK);
// Verify both see each other in their friends list
let (s, alex_friends) = t.call("GET", "/api/v1/social/friends", Some(&alex_token), None).await;
assert_eq!(s, StatusCode::OK);
assert_eq!(alex_friends[0]["uuid"], steve_uuid);
assert_eq!(alex_friends[0]["status"], "accepted");
// 3. Direct Messaging
let (s, msg) = t
.call(
"POST",
&format!("/api/v1/messages/{steve_uuid}"),
Some(&alex_token),
Some(json!({
"content": "Hey Steve! Ready for the wither fight?"
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{msg}");
// Steve reads messages from Alex
let (s, msgs) = t.call("GET", &format!("/api/v1/messages/{alex_uuid}"), Some(&steve_token), None).await;
assert_eq!(s, StatusCode::OK, "{msgs}");
assert_eq!(msgs[0]["content"], "Hey Steve! Ready for the wither fight?");
// 4. User Profile & Posts
let (s, _) = t
.call(
"PUT",
"/api/v1/social/profile/me",
Some(&alex_token),
Some(json!({
"bio": "Redstone expert and dragon hunter."
})),
)
.await;
assert_eq!(s, StatusCode::OK);
// Alex posts a social update
let (s, post) = t
.call(
"POST",
"/api/v1/social/posts",
Some(&alex_token),
Some(json!({
"content": "Defeated the Ender Dragon on hardcore mode!",
"image_url": "https://example.com/screenshot.png"
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{post}");
let post_id = post["id"].as_i64().unwrap();
// Steve likes Alex's post
let (s, like_res) = t.call("POST", &format!("/api/v1/social/posts/{post_id}/like"), Some(&steve_token), None).await;
assert_eq!(s, StatusCode::OK, "{like_res}");
assert_eq!(like_res["ok"], true);
// Steve views Alex's full profile
let (s, profile) = t.call("GET", &format!("/api/v1/social/profile/{alex_uuid}"), Some(&steve_token), None).await;
assert_eq!(s, StatusCode::OK, "{profile}");
assert_eq!(profile["bio"], "Redstone expert and dragon hunter.");
assert_eq!(profile["posts"][0]["likes_count"], 1);
}