366 lines
13 KiB
Rust
366 lines
13 KiB
Rust
//! 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": [{"uuid": &alex_uuid, "kind": "action", "detail": "block_broken:DIAMOND_ORE@minecraft:overworld +4"}]
|
|
})),
|
|
)
|
|
.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");
|
|
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();
|
|
assert_eq!(progress("q_d_mine_diamond"), 4);
|
|
assert_eq!(progress("q_d_mine_coal"), 0);
|
|
|
|
// 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;
|
|
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
|
|
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);
|
|
|
|
// 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;
|
|
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;
|
|
assert_eq!(s, StatusCode::OK, "{response}");
|
|
}
|
|
|
|
#[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);
|
|
let (s, repeat_like) = t.call("POST", &format!("/api/v1/social/posts/{post_id}/like"), Some(&steve_token), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert_eq!(repeat_like["liked"], false);
|
|
|
|
// 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);
|
|
assert_eq!(profile["posts"][0]["liked_by_me"], true);
|
|
let (s, author_profile) = t.call("GET", &format!("/api/v1/social/profile/{alex_uuid}"), Some(&alex_token), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert_eq!(author_profile["posts"][0]["liked_by_me"], false);
|
|
}
|