#![cfg(feature = "styles")]
use actix_web::http::header::CONTENT_TYPE;
use actix_web::test::{TestRequest, call_service, read_body, read_body_json};
use indoc::indoc;
use insta::assert_json_snapshot;
use martin::config::file::srv::SrvConfig;
use serde_json::Value;
pub mod utils;
pub use utils::*;
macro_rules! create_app {
($sources:expr) => {{
let state = mock_sources(mock_cfg($sources)).await.0;
let app = ::actix_web::App::new()
.app_data(actix_web::web::Data::new(
::martin::srv::Catalog::new(
#[cfg(any(feature = "sprites", feature = "fonts", feature = "styles"))]
&state,
)
.unwrap(),
))
.app_data(actix_web::web::Data::new(SrvConfig::default()));
#[cfg(feature = "_tiles")]
let app = app.app_data(actix_web::web::Data::new(state.tile_manager.clone()));
let app = app
.app_data(actix_web::web::Data::new(state.styles))
.configure(|c| ::martin::srv::router(c, &SrvConfig::default()));
::actix_web::test::init_service(app).await
}};
}
fn test_get(path: &str) -> TestRequest {
TestRequest::get().uri(path)
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
const CONFIG_STYLES: &str = indoc! {"
styles:
rendering: true
sources:
maplibre_demo: ../tests/fixtures/styles/maplibre_demo.json
"};
#[cfg(any(not(feature = "rendering"), not(target_os = "linux")))]
const CONFIG_STYLES: &str = indoc! {"
styles:
sources:
maplibre_demo: ../tests/fixtures/styles/maplibre_demo.json
"};
#[actix_rt::test]
#[tracing_test::traced_test]
async fn catalog_multiple_styles() {
let app = create_app! { CONFIG_STYLES };
let req = test_get("/catalog").to_request();
let response = call_service(&app, req).await;
let response = assert_response(response).await;
let body: Value = read_body_json(response).await;
insta::with_settings!({sort_maps => true}, {
assert_json_snapshot!(body["styles"], @r#"
{
"maplibre_demo": {
"path": "../tests/fixtures/styles/maplibre_demo.json"
}
}
"#);
});
}
#[actix_rt::test]
#[tracing_test::traced_test]
async fn style_json_not_found() {
let app = create_app! { CONFIG_STYLES };
let req = test_get("/style/nonexistent_style").to_request();
let response = call_service(&app, req).await;
assert_eq!(response.status(), 404);
assert_eq!(
response.headers().get(CONTENT_TYPE).unwrap(),
"text/plain; charset=utf-8"
);
let body = String::from_utf8(read_body(response).await.to_vec()).unwrap();
assert_eq!(body, "No such style exists");
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
mod render_tests {
use rstest::rstest;
use super::*;
const PNG_MAGIC: &[u8] = &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
const JPEG_MAGIC: &[u8] = &[0xFF, 0xD8, 0xFF];
#[rstest]
#[case::single_style(CONFIG_STYLES, "/style/maplibre_demo/0/0/0.png")]
#[case::single_style_zoom_1(CONFIG_STYLES, "/style/maplibre_demo/1/0/0.png")]
#[case::single_style_corner(CONFIG_STYLES, "/style/maplibre_demo/1/1/0.png")]
#[case::single_style_mid_zoom(CONFIG_STYLES, "/style/maplibre_demo/5/15/15.png")]
#[tokio::test]
#[tracing_test::traced_test]
async fn render_tile_png(#[case] config: &str, #[case] path: &str) {
let app = create_app! { config };
let req = test_get(path).to_request();
let response = call_service(&app, req).await;
let response = assert_response(response).await;
assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "image/png");
let body = read_body(response).await;
assert!(
body.len() > 1000,
"PNG should have reasonable size for {path}, got {}",
body.len()
);
assert_eq!(&body[..8], PNG_MAGIC, "Response is not a valid PNG");
let img = image::load_from_memory_with_format(&body, image::ImageFormat::Png)
.expect("Failed to decode PNG response");
assert_eq!(
(img.width(), img.height()),
(512, 512),
"Tile must be 512x512"
);
}
#[rstest]
#[case::jpeg_ext(CONFIG_STYLES, "/style/maplibre_demo/0/0/0.jpeg")]
#[case::jpg_alias(CONFIG_STYLES, "/style/maplibre_demo/0/0/0.jpg")]
#[case::jpeg_zoom_1(CONFIG_STYLES, "/style/maplibre_demo/1/0/0.jpeg")]
#[tokio::test]
#[tracing_test::traced_test]
async fn render_tile_jpeg(#[case] config: &str, #[case] path: &str) {
let app = create_app! { config };
let req = test_get(path).to_request();
let response = call_service(&app, req).await;
let response = assert_response(response).await;
assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "image/jpeg");
let body = read_body(response).await;
assert!(
body.len() > 1000,
"JPEG should have reasonable size for {path}, got {}",
body.len()
);
assert_eq!(&body[..3], JPEG_MAGIC, "Response is not a valid JPEG");
let img = image::load_from_memory_with_format(&body, image::ImageFormat::Jpeg)
.expect("Failed to decode JPEG response");
assert_eq!(
(img.width(), img.height()),
(512, 512),
"Tile must be 512x512"
);
}
#[tokio::test]
#[tracing_test::traced_test]
async fn render_tile_not_found_style() {
let app = create_app! { CONFIG_STYLES };
let req = test_get("/style/nonexistent_style/0/0/0.png").to_request();
let response = call_service(&app, req).await;
assert_eq!(response.status(), 404);
let body = String::from_utf8(read_body(response).await.to_vec()).unwrap();
assert_eq!(body, "No such style exists");
}
#[tokio::test]
#[tracing_test::traced_test]
async fn render_tile_impossible() {
let app = create_app! { CONFIG_STYLES };
let req = test_get("/style/maplibre_demo/0/4000/4000.png").to_request();
let response = call_service(&app, req).await;
assert_eq!(response.status(), 400);
let body = String::from_utf8(read_body(response).await.to_vec()).unwrap();
assert_eq!(body, "Invalid tile coordinates for zoom level");
}
#[tokio::test]
#[tracing_test::traced_test]
async fn render_different_tiles_differ() {
let app = create_app! { CONFIG_STYLES };
let req_a = test_get("/style/maplibre_demo/0/0/0.png").to_request();
let resp_a = call_service(&app, req_a).await;
let body_a = read_body(assert_response(resp_a).await).await;
let req_b = test_get("/style/maplibre_demo/1/1/0.png").to_request();
let resp_b = call_service(&app, req_b).await;
let body_b = read_body(assert_response(resp_b).await).await;
assert_ne!(
body_a, body_b,
"Different tile coordinates must produce different images"
);
}
#[tokio::test]
#[tracing_test::traced_test]
async fn render_concurrent_requests() {
let app = create_app! { CONFIG_STYLES };
let coords = [
"/style/maplibre_demo/0/0/0.png",
"/style/maplibre_demo/1/0/0.png",
"/style/maplibre_demo/1/1/0.png",
"/style/maplibre_demo/1/0/1.png",
"/style/maplibre_demo/1/1/1.png",
];
let futures = coords
.iter()
.map(|path| call_service(&app, test_get(path).to_request()));
let responses = futures::future::join_all(futures).await;
let mut bodies = Vec::new();
for (i, response) in responses.into_iter().enumerate() {
let response = assert_response(response).await;
assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "image/png");
let body = read_body(response).await;
assert!(
body.len() > 1000,
"Concurrent request {i} should produce a valid image"
);
assert_eq!(
&body[..8],
PNG_MAGIC,
"Concurrent request {i} is not valid PNG"
);
bodies.push(body);
}
let unique_count = bodies
.iter()
.collect::<std::collections::HashSet<_>>()
.len();
assert!(
unique_count > 1,
"All {0} concurrent responses are identical — renderer may be ignoring coordinates",
bodies.len()
);
}
}