use citygen_server::{ gateway::{router, App, Config}, generation::{cache_id, Request}, protocol::{CityJob, CreateCity}, }; use prost::Message; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; async fn request( address: std::net::SocketAddr, method: &str, path: &str, body: &[u8], ) -> (u16, Vec) { let mut stream = tokio::net::TcpStream::connect(address).await.unwrap(); stream.write_all(format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-protobuf\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",body.len()).as_bytes()).await.unwrap(); stream.write_all(body).await.unwrap(); let mut bytes = vec![]; stream.read_to_end(&mut bytes).await.unwrap(); let split = bytes.windows(4).position(|b| b == b"\r\n\r\n").unwrap(); let status = std::str::from_utf8(&bytes[..split]) .unwrap() .split_whitespace() .nth(1) .unwrap() .parse() .unwrap(); (status, bytes[split + 4..].to_vec()) } #[tokio::test] async fn isolated_typescript_worker_exports_city_and_opens_room_without_network() { let dir = tempfile::tempdir().unwrap(); let fixture = dir.path().join("osm.json"); std::fs::write(&fixture,r#"{"elements":[{"type":"way","id":1,"tags":{"highway":"residential"},"geometry":[{"lat":47.001,"lon":9.0},{"lat":47.001,"lon":9.004}]},{"type":"way","id":2,"tags":{"building":"house","building:levels":"1"},"geometry":[{"lat":47.0011,"lon":9.001},{"lat":47.0011,"lon":9.0012},{"lat":47.0013,"lon":9.0012},{"lat":47.0013,"lon":9.001},{"lat":47.0011,"lon":9.001}]},{"type":"way","id":3,"tags":{"building":"industrial","building:levels":"1"},"geometry":[{"lat":47.0006,"lon":9.002},{"lat":47.0006,"lon":9.0023},{"lat":47.0009,"lon":9.0023},{"lat":47.0009,"lon":9.002},{"lat":47.0006,"lon":9.002}]}]}"#).unwrap(); let app = App::new(Config { cities: dir.path().join("cities"), saves: dir.path().join("rooms"), osm_fixture: Some(fixture), project_root: std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .to_owned(), ..Default::default() }) .unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); let serve_app = app.clone(); let server = tokio::spawn(async move { axum::serve( listener, router(serve_app).into_make_service_with_connect_info::(), ) .await .unwrap(); }); let bbox = [47., 9., 47.002, 9.004]; let message = CreateCity { bbox: bbox.to_vec(), name: "Test town".into(), seed: 1, }; let (status, body) = request( address, "POST", "/api/cities/jobs", &message.encode_to_vec(), ) .await; assert_eq!(status, 200, "{}", String::from_utf8_lossy(&body)); let job = CityJob::decode(&body[..]).unwrap(); assert_eq!(job.city_id, cache_id(bbox, 1)); let ready = tokio::time::timeout(Duration::from_secs(30), async { loop { let (status, body) = request(address, "GET", &format!("/api/cities/jobs/{}", job.id), &[]).await; assert_eq!(status, 200); let state = CityJob::decode(&body[..]).unwrap(); assert_ne!(state.status, "failed", "{}", state.error); if state.status == "ready" { break state; } tokio::time::sleep(Duration::from_millis(25)).await; } }) .await .unwrap(); assert!(!ready.room_id.is_empty()); let city = citygen_server::city::City::load(&app.config.cities, &ready.city_id).unwrap(); assert!(!city.graph.points.is_empty()); assert!(city.manifest.navigation.is_some()); let duplicate = app .jobs .submit( app.clone(), Request { bbox: [47.0000001, 9., 47.002, 9.004], name: "Same area".into(), seed: 1, }, ) .unwrap(); assert_eq!(duplicate.id, job.id); let (status, body) = request( address, "POST", "/api/cities/jobs", &message.encode_to_vec(), ) .await; assert_eq!(status, 200, "repeated POST resumes the accepted job"); assert_eq!(CityJob::decode(&body[..]).unwrap().id, job.id); let different = CreateCity { seed: 2, ..message }; let (status, body) = request( address, "POST", "/api/cities/jobs", &different.encode_to_vec(), ) .await; assert_eq!(status, 429); let error: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert!(error["retryAfterSeconds"].as_u64().unwrap() <= 60); app.stop(); server.abort(); assert_eq!( citygen_server::storage::load(&app.config.saves, &ready.room_id) .unwrap() .city_id, ready.city_id ); }