use std::time::Duration;
use flatland_client_lib::{connect, ConnectOptions, GameClient};
use flatland_protocol::AuthCredential;
#[tokio::test]
#[ignore = "requires dev server on 127.0.0.1:7373 (make dev-server)"]
async fn dev_local_stays_connected_and_receives_world() {
let opts = ConnectOptions::to_server("probe", "127.0.0.1:7373".parse().unwrap());
let session = connect(opts).await.expect("connect");
let mut client = GameClient::new(session);
client.wait_until_ready().await.expect("welcome");
assert!(
!client.state.resource_nodes.is_empty(),
"welcome snapshot should include resource nodes, got {}",
client.state.resource_nodes.len()
);
assert_eq!(client.state.world_width_m, 256.0);
let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
while tokio::time::Instant::now() < deadline {
if let Some(ev) = client.next_event().await {
client.handle_event(ev).await.expect("event");
if !client.state.connected {
panic!("disconnected early: {:?}", client.state.logs.back());
}
}
}
assert!(client.state.ticks_received > 20, "expected ticks");
assert!(client.state.connected);
client.disconnect();
}
#[tokio::test]
#[ignore = "requires dev server, control plane, and ~/.config/flatland/session.json"]
async fn session_auth_stays_connected_when_logged_in() {
let path = dirs::config_dir()
.map(|p| p.join("flatland").join("session.json"))
.filter(|p| p.exists())
.or_else(|| {
dirs::data_dir()
.map(|p| p.join("flatland").join("session.json"))
.filter(|p| p.exists())
});
let Some(path) = path else {
eprintln!("skip session_auth_stays_connected_when_logged_in: no session.json");
return;
};
#[derive(serde::Deserialize)]
struct SessionConfig {
session_token: String,
}
let raw = std::fs::read_to_string(&path).expect("read session");
let session_cfg: SessionConfig = serde_json::from_str(&raw).expect("parse session");
let api_base =
std::env::var("FLATLAND_API_URL").unwrap_or_else(|_| "http://127.0.0.1:7380".into());
let client = reqwest::Client::new();
#[derive(serde::Deserialize)]
struct Envelope<T> {
data: T,
}
#[derive(serde::Deserialize)]
struct CharList {
characters: Vec<CharRow>,
}
#[derive(serde::Deserialize)]
struct CharRow {
id: uuid::Uuid,
name: String,
position_x: f32,
position_y: f32,
}
let list: Envelope<CharList> = client
.get(format!("{api_base}/v1/characters"))
.header(
"Authorization",
format!("Bearer {}", session_cfg.session_token),
)
.send()
.await
.expect("characters request")
.json()
.await
.expect("characters json");
let character = list
.data
.characters
.first()
.expect("at least one character");
eprintln!(
"connecting as {} at ({}, {})",
character.name, character.position_x, character.position_y
);
let opts = ConnectOptions::to_server(&character.name, "127.0.0.1:7373".parse().unwrap())
.with_auth(
AuthCredential::Session {
token: session_cfg.session_token,
},
Some(character.id),
);
let session = connect(opts).await.expect("connect");
let mut client = GameClient::new(session);
client.wait_until_ready().await.expect("welcome");
let (px, py) = client.state.player_position();
eprintln!(
"spawn=({px:.1},{py:.1}) nodes={} buildings={} inside={:?}",
client.state.resource_nodes.len(),
client.state.buildings.len(),
client.state.effective_inside_building()
);
let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
while tokio::time::Instant::now() < deadline {
if let Some(ev) = client.next_event().await {
client.handle_event(ev).await.expect("event");
if !client.state.connected {
panic!(
"session auth disconnected: {:?}",
client.state.logs.iter().rev().take(3).collect::<Vec<_>>()
);
}
}
}
assert!(client.state.ticks_received > 20);
client.disconnect();
}