CI / Real Minecraft installs (manual) (push) Skipped
CI / Rust, web and container (push) Canceled after 0s
CI / Windows launcher (push) Canceled after 0s
Server integrations / paper (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.20.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.21.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.1.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.3 (push) Canceled after 0s
164 lines
8.3 KiB
Rust
164 lines
8.3 KiB
Rust
//! The SCOPENET Map: tiles in from a game server, tiles and overlay out to signed-in viewers.
|
|
|
|
mod common;
|
|
use common::*;
|
|
|
|
const PNG: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
|
|
|
|
fn record(zoom: u8, x: i32, y: i32, payload: &[u8]) -> Vec<u8> {
|
|
let mut out = vec![zoom];
|
|
out.extend(x.to_be_bytes());
|
|
out.extend(y.to_be_bytes());
|
|
let mut png = PNG.to_vec();
|
|
png.extend_from_slice(payload);
|
|
out.extend((png.len() as u32).to_be_bytes());
|
|
out.extend(png);
|
|
out
|
|
}
|
|
|
|
struct World {
|
|
t: TestApp,
|
|
admin: String,
|
|
alex: String,
|
|
server: String,
|
|
sid: i64,
|
|
}
|
|
|
|
async fn world() -> World {
|
|
let t = setup().await;
|
|
let admin = t.login("admin", "supersecret").await;
|
|
t.call("POST", "/api/admin/users", Some(&admin), Some(json!({"username": "Alex", "password": "password123"}))).await;
|
|
let alex = t.login("Alex", "password123").await;
|
|
let (_, srv) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "SMP", "instance_id": "smp"}))).await;
|
|
World { t, admin, alex, server: srv["token"].as_str().unwrap().to_string(), sid: srv["server"]["id"].as_i64().unwrap() }
|
|
}
|
|
|
|
impl World {
|
|
async fn upload(&self, dim: &str, body: Vec<u8>) -> (StatusCode, Value) {
|
|
self.t.call_raw("POST", &format!("/api/server/v1/map/tiles?dim={dim}"), Some(&self.server), body).await
|
|
}
|
|
async fn tile(&self, token: &str, dim: &str, z: u8, x: i32, y: i32) -> (StatusCode, Vec<u8>) {
|
|
self.t.fetch(&format!("/api/map/{}/{dim}/{z}/{x}/{y}.png?t={token}", self.sid)).await
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tiles_flow_from_the_game_server_to_signed_in_viewers() {
|
|
let w = world().await;
|
|
let (s, cfg) = w.t.call("GET", "/api/server/v1/map/config", Some(&w.server), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert_eq!((cfg["enabled"].as_bool(), cfg["epoch"].as_u64(), cfg["tile_size"].as_u64()), (Some(true), Some(0), Some(256)));
|
|
|
|
// Nothing drawn yet.
|
|
let (_, info) = w.t.call("GET", &format!("/api/v1/servers/{}/map", w.sid), Some(&w.alex), None).await;
|
|
assert_eq!((info["ready"].as_bool(), info["token"].is_null()), (Some(false), true));
|
|
assert!(info["message"].as_str().unwrap().contains("Waiting"));
|
|
|
|
let body = [record(0, -1, 2, b"aa"), record(0, 0, 0, b"bb"), record(1, 0, 0, b"cc")].concat();
|
|
let (s, r) = w.upload("minecraft:overworld", body).await;
|
|
assert_eq!((s, r["stored"].as_u64()), (StatusCode::OK, Some(3)));
|
|
assert_eq!(w.upload("minecraft:the_nether", record(0, 5, 5, b"n")).await.0, StatusCode::OK);
|
|
|
|
let (_, info) = w.t.call("GET", &format!("/api/v1/servers/{}/map", w.sid), Some(&w.alex), None).await;
|
|
assert_eq!(info["ready"], true);
|
|
let dims = info["dimensions"].as_array().unwrap();
|
|
assert_eq!((dims[0]["slug"].as_str(), dims[1]["label"].as_str()), (Some("overworld"), Some("The Nether")));
|
|
assert_eq!(dims[0]["bounds"]["min_y"], 0);
|
|
assert_eq!(dims[0]["bounds"]["max_x"], 0);
|
|
let token = info["token"].as_str().unwrap().to_string();
|
|
assert_eq!(info["tile_base"], format!("/api/map/{}", w.sid));
|
|
|
|
// The token opens tiles for plain <img> tags.
|
|
let (s, bytes) = w.tile(&token, "overworld", 0, -1, 2).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert!(bytes.ends_with(b"aa") && bytes.starts_with(&PNG));
|
|
assert_eq!(w.tile(&token, "overworld", 0, 7, 7).await.0, StatusCode::NOT_FOUND);
|
|
assert_eq!(w.tile(&token, "overworld", 9, 0, 0).await.0, StatusCode::NOT_FOUND);
|
|
assert_eq!(w.tile("wrong", "overworld", 0, -1, 2).await.0, StatusCode::UNAUTHORIZED);
|
|
assert_eq!(w.tile(&token, "..%2Fetc", 0, 0, 0).await.0, StatusCode::NOT_FOUND);
|
|
// Tokens belong to one server.
|
|
let (_, other) = w.t.call("POST", "/api/admin/servers", Some(&w.admin), Some(json!({"name": "Other", "instance_id": "o"}))).await;
|
|
let other_id = other["server"]["id"].as_i64().unwrap();
|
|
assert_eq!(w.t.fetch(&format!("/api/map/{other_id}/overworld/0/0/0.png?t={token}")).await.0, StatusCode::UNAUTHORIZED);
|
|
// Info needs an account.
|
|
assert_eq!(w.t.call("GET", &format!("/api/v1/servers/{}/map", w.sid), None, None).await.0, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn bad_uploads_are_refused() {
|
|
let w = world().await;
|
|
assert_eq!(w.upload("minecraft:overworld", b"not tiles at all".to_vec()).await.0, StatusCode::BAD_REQUEST);
|
|
assert_eq!(w.upload("../x", record(0, 0, 0, b"a")).await.0, StatusCode::BAD_REQUEST);
|
|
assert_eq!(w.upload("minecraft:overworld", record(12, 0, 0, b"a")).await.0, StatusCode::BAD_REQUEST);
|
|
let (s, _) = w.t.call_raw("POST", "/api/server/v1/map/tiles?dim=minecraft:overworld", None, record(0, 0, 0, b"a")).await;
|
|
assert_eq!(s, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn players_and_overlay_are_served_to_viewers() {
|
|
let w = world().await;
|
|
let (s, _) = w.t.call("POST", "/api/server/v1/map/players", Some(&w.server), Some(json!({"players": [
|
|
{"uuid": "11111111-2222-3333-4444-555555555555", "name": "Steve", "dimension": "minecraft:overworld", "x": 10.5, "y": 70.0, "z": -3.0, "yaw": 90.0}]}))).await;
|
|
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
|
|
);
|
|
|
|
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);
|
|
assert_eq!(o["players"][0]["name"], "Steve");
|
|
assert_eq!(o["claims"][0]["tag"], "IRON");
|
|
assert_eq!(o["pins"][0]["kind"], "spawn");
|
|
assert!(o["updated"].is_string());
|
|
let (_, info) = w.t.call("GET", &format!("/api/v1/servers/{}/map", w.sid), Some(&w.alex), None).await;
|
|
assert_eq!(info["players"], 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn map_diagnostics_are_admin_only_and_reset_with_tiles() {
|
|
let w = world().await;
|
|
let payload = json!({"stage":"waiting for readable chunks","error":"No readable full chunks","region_files":1});
|
|
assert_eq!(w.t.call("POST", "/api/server/v1/map/diagnostics", Some(&w.server), Some(payload)).await.0, StatusCode::OK);
|
|
assert_eq!(w.t.call("POST", "/api/server/v1/map/diagnostics", None, Some(json!({}))).await.0, StatusCode::UNAUTHORIZED);
|
|
let (_, status) = w.t.call("GET", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
|
|
assert_eq!(status["diagnostics"]["stage"], "waiting for readable chunks");
|
|
assert!(status["diagnostics"]["age_seconds"].is_number());
|
|
w.t.call("DELETE", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
|
|
let (_, status) = w.t.call("GET", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
|
|
assert!(status["diagnostics"].is_null());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_map_can_be_switched_off_and_reset() {
|
|
let w = world().await;
|
|
w.upload("minecraft:overworld", record(0, 0, 0, b"a")).await;
|
|
let (s, r) = w.t.call("DELETE", &format!("/api/admin/servers/{}/map", w.sid), Some(&w.admin), None).await;
|
|
assert_eq!(s, StatusCode::OK, "{r}");
|
|
let (_, cfg) = w.t.call("GET", "/api/server/v1/map/config", Some(&w.server), None).await;
|
|
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"
|
|
);
|
|
|
|
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;
|
|
assert_eq!(cfg["enabled"], false);
|
|
assert_eq!(w.t.call("GET", &format!("/api/v1/servers/{}/map/overlay", w.sid), Some(&w.alex), None).await.0, StatusCode::FORBIDDEN);
|
|
}
|