use std::{
io::{Read, Write},
net::{TcpListener, TcpStream},
path::Path,
sync::{Arc, Mutex},
};
use super::{geofabrik, list_local, resolve_geofabrik_url};
fn start_json_server(body: String) -> String {
start_recording_json_server(body).0
}
fn start_recording_json_server(body: String) -> (String, Arc<Mutex<Vec<String>>>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let url = format!("http://127.0.0.1:{port}/index.json");
let body = Arc::new(body);
let requests: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let requests_srv = requests.clone();
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let body = Arc::clone(&body);
let requests = Arc::clone(&requests_srv);
std::thread::spawn(move || serve_json(stream, &body, &requests));
}
});
(url, requests)
}
fn serve_json(mut stream: TcpStream, body: &str, requests: &Mutex<Vec<String>>) {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap_or(0);
requests
.lock()
.unwrap()
.push(String::from_utf8_lossy(&buf[..n]).to_string());
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(response.as_bytes());
}
fn tmp(name: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(name);
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
fn sqlite(name: &str) -> String {
tmp(name).join("test.sqlite3").to_str().unwrap().to_string()
}
#[test]
fn _00_fetches_and_caches_when_no_cache_exists() {
let db = sqlite("ls_t00");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"europe/germany","name":"Germany","urls":{"pbf":"http://example.com/germany.osm.pbf"}}},{"type":"Feature","properties":{"id":"europe/france","name":"France","urls":{"pbf":"http://example.com/france.osm.pbf"}}}]}"#.to_string();
let url = start_json_server(json);
let items = geofabrik(&db, false, &url);
assert_eq!(items.len(), 2);
assert_eq!(items[0].id, "europe/france");
assert_eq!(items[0].url, "http://example.com/france.osm.pbf");
assert_eq!(items[1].id, "europe/germany");
assert!(Path::new(&db).exists());
}
#[test]
fn _01_returns_cached_without_refetching() {
let db = sqlite("ls_t01");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"asia/japan","name":"Japan","urls":{"pbf":"http://example.com/japan.osm.pbf"}}}]}"#.to_string();
let (url, requests) = start_recording_json_server(json);
geofabrik(&db, false, &url);
let after_first = requests.lock().unwrap().len();
geofabrik(&db, false, &url);
assert_eq!(requests.lock().unwrap().len(), after_first);
}
#[test]
fn _02_recreate_cache_forces_refetch() {
let db = sqlite("ls_t02");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"africa/kenya","name":"Kenya","urls":{"pbf":"http://example.com/kenya.osm.pbf"}}}]}"#.to_string();
let (url, requests) = start_recording_json_server(json);
geofabrik(&db, false, &url);
let after_first = requests.lock().unwrap().len();
geofabrik(&db, true, &url);
assert!(requests.lock().unwrap().len() > after_first);
}
#[test]
fn _03_feature_without_pbf_url_has_dash_url() {
let db = sqlite("ls_t03");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"oceania/australia","name":"Australia"}}]}"#.to_string();
let url = start_json_server(json);
let items = geofabrik(&db, false, &url);
assert_eq!(items.len(), 1);
assert_eq!(items[0].url, "-");
}
#[test]
fn _04_resolve_geofabrik_url_returns_url_for_known_id() {
let db = sqlite("ls_t04");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"south-america/brazil","name":"Brazil","urls":{"pbf":"http://example.com/brazil.osm.pbf"}}}]}"#.to_string();
let url = start_json_server(json);
let result = resolve_geofabrik_url(&db, "south-america/brazil", &url);
assert_eq!(result.as_deref(), Some("http://example.com/brazil.osm.pbf"));
}
#[test]
fn _05_resolve_geofabrik_url_returns_none_for_unknown_id() {
let db = sqlite("ls_t05");
let json = r#"{"type":"FeatureCollection","features":[]}"#.to_string();
let url = start_json_server(json);
let result = resolve_geofabrik_url(&db, "nonexistent/region", &url);
assert!(result.is_none());
}
#[test]
fn _06_resolve_geofabrik_url_returns_none_for_entry_without_url() {
let db = sqlite("ls_t06");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"oceania/australia","name":"Australia"}}]}"#.to_string();
let url = start_json_server(json);
let result = resolve_geofabrik_url(&db, "oceania/australia", &url);
assert!(result.is_none());
}
#[test]
fn _07_list_local_returns_empty_when_no_pbf_dir() {
let dir = tmp("ls_t07");
let items = list_local(dir.to_str().unwrap());
assert!(items.is_empty());
}
#[test]
fn _08_list_local_returns_sorted_pbf_files() {
let dir = tmp("ls_t08");
std::fs::write(dir.join("brazil.osm.pbf"), b"fake").unwrap();
std::fs::write(dir.join("andorra.osm.pbf"), b"fake content longer").unwrap();
std::fs::write(dir.join("readme.txt"), b"ignored").unwrap();
let items = list_local(dir.to_str().unwrap());
assert_eq!(items.len(), 2);
assert_eq!(items[0].path.file_name().unwrap(), "andorra.osm.pbf");
assert_eq!(items[1].path.file_name().unwrap(), "brazil.osm.pbf");
assert_eq!(items[0].size_bytes, 19);
assert!(Path::new(&items[0].path).exists());
}
#[test]
fn _09_resolve_geofabrik_url_uses_cache_without_refetching() {
let db = sqlite("ls_t09");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"europe/spain","name":"Spain","urls":{"pbf":"http://example.com/spain.osm.pbf"}}}]}"#.to_string();
let (url, requests) = start_recording_json_server(json);
geofabrik(&db, false, &url);
let after_geofabrik = requests.lock().unwrap().len();
let result = resolve_geofabrik_url(&db, "europe/spain", &url);
assert_eq!(result.as_deref(), Some("http://example.com/spain.osm.pbf"));
assert_eq!(requests.lock().unwrap().len(), after_geofabrik);
}
#[test]
fn _10_list_local_returns_empty_for_nonexistent_path() {
let dir = tmp("ls_t10");
let missing = dir.join("does_not_exist");
let items = list_local(missing.to_str().unwrap());
assert!(items.is_empty());
}
#[test]
fn _11_sends_custom_user_agent_header() {
let db = sqlite("ls_t11");
let json = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"europe/spain","name":"Spain","urls":{"pbf":"http://example.com/spain.osm.pbf"}}}]}"#.to_string();
let (url, requests) = start_recording_json_server(json);
geofabrik(&db, false, &url);
let requests = requests.lock().unwrap();
let user_agent = requests[0]
.lines()
.find(|l| l.to_lowercase().starts_with("user-agent:"))
.and_then(|l| l.split_once(':').map(|x| x.1))
.map(|v| v.trim().to_string());
assert_eq!(
user_agent,
Some(format!("geolite/{}", env!("CARGO_PKG_VERSION")))
);
}