use everymap_core::auth::ApiKeyProvider;
use everymap_core::domains::routing::{
AvoidType, DepartureTime, RouteOptions, Router, TransportMode,
};
use everymap_core::types::Coordinate;
use everymap_providers_google::client::GoogleClient;
use everymap_providers_google::GoogleRouter;
use std::sync::Arc;
use wiremock::matchers::{body_partial_json, method};
use wiremock::{Match, Mock, MockServer, Request, ResponseTemplate};
const FIELD_MASK: &str = "routes.distanceMeters,routes.duration,routes.travelMode,routes.polyline.encodedPolyline,routes.viewport,routes.legs";
const ENCODED_POLYLINE: &str = "_m_I??c~_I??~o~@";
struct ExactHeader(&'static str, &'static str);
impl Match for ExactHeader {
fn matches(&self, request: &Request) -> bool {
request
.headers
.get(self.0)
.and_then(|value| value.to_str().ok())
.map(|value| value == self.1)
.unwrap_or(false)
}
}
fn mock_route_response() -> serde_json::Value {
serde_json::json!({
"routes": [
{
"distanceMeters": 1052000,
"duration": "36720s",
"staticDuration": "36000s",
"travelMode": "DRIVE",
"polyline": { "encodedPolyline": ENCODED_POLYLINE },
"viewport": {
"high": { "latitude": 52.5170, "longitude": 13.3780 },
"low": { "latitude": 48.8566, "longitude": 2.3522 }
},
"legs": [
{
"distanceMeters": 1052000,
"duration": "36720s",
"startLocation": { "latLng": { "latitude": 52.5163, "longitude": 13.3777 } },
"endLocation": { "latLng": { "latitude": 48.8566, "longitude": 2.3522 } },
"steps": [
{
"distanceMeters": 500,
"staticDuration": "60s",
"startLocation": { "latLng": { "latitude": 52.5163, "longitude": 13.3777 } },
"endLocation": { "latLng": { "latitude": 52.5170, "longitude": 13.3780 } },
"navigationInstruction": { "instructions": "Head north" },
"travelMode": "DRIVE"
},
{
"distanceMeters": 1051500,
"staticDuration": "36660s",
"startLocation": { "latLng": { "latitude": 52.5170, "longitude": 13.3780 } },
"endLocation": { "latLng": { "latitude": 48.8566, "longitude": 2.3522 } },
"navigationInstruction": { "instructions": "Continue to Paris" },
"travelMode": "DRIVE"
}
]
}
],
"routeLabels": ["DEFAULT_ROUTE"]
}
]
})
}
struct BodyWithoutField(&'static str);
impl Match for BodyWithoutField {
fn matches(&self, request: &Request) -> bool {
serde_json::from_slice::<serde_json::Value>(&request.body)
.map(|body| body.get(self.0).is_none())
.unwrap_or(false)
}
}
fn google_router(server: &MockServer) -> GoogleRouter {
let auth = Arc::new(ApiKeyProvider::new(
"test-key".to_string(),
"key".to_string(),
));
let client = Arc::new(GoogleClient::new(auth));
GoogleRouter::with_base_url(client, server.uri())
}
#[tokio::test]
async fn test_routing_contract() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(ExactHeader("X-Goog-FieldMask", FIELD_MASK))
.and(body_partial_json(serde_json::json!({
"origin": { "location": { "latLng": { "latitude": 52.5163, "longitude": 13.3777 } } },
"destination": { "location": { "latLng": { "latitude": 48.8566, "longitude": 2.3522 } } },
"travelMode": "DRIVE"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
transport_mode: Some(TransportMode::Car),
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
let route = &result.routes[0];
assert_eq!(route.distance, 1052000.0);
assert_eq!(route.duration, 36720.0);
assert_eq!(route.transport_mode, Some(TransportMode::Car));
assert!(!route.geometry.points.is_empty());
assert_eq!(route.steps.len(), 2);
assert_eq!(route.steps[0].instruction, Some("Head north".to_string()));
assert_eq!(route.steps[0].distance, Some(500.0));
assert_eq!(route.steps[0].duration, Some(60.0));
assert!(route.steps[0].start_coordinate.is_some());
assert!(route.steps[0].end_coordinate.is_some());
assert!(route.bounding_box.is_some());
}
#[tokio::test]
async fn test_routing_with_pedestrian_mode() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(body_partial_json(
serde_json::json!({ "travelMode": "WALK" }),
))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
transport_mode: Some(TransportMode::Pedestrian),
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
assert_eq!(result.routes[0].distance, 1052000.0);
assert_eq!(result.routes[0].duration, 36720.0);
}
#[tokio::test]
async fn test_routing_error_response() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
"error": {
"code": 400,
"message": "Field mask is missing.",
"status": "INVALID_ARGUMENT"
}
})))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.52, 13.405).unwrap();
let end = Coordinate::new(48.856, 2.352).unwrap();
let options = RouteOptions::default();
let result = router.calculate_route(&start, &end, &options).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_routing_zero_results() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"routes": []
})))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.52, 13.405).unwrap();
let end = Coordinate::new(48.856, 2.352).unwrap();
let options = RouteOptions::default();
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert!(result.routes.is_empty());
}
#[tokio::test]
async fn test_routing_with_avoid_tolls() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(body_partial_json(serde_json::json!({
"routeModifiers": {
"avoidTolls": true,
"avoidFerries": true
}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
transport_mode: Some(TransportMode::Car),
avoid: vec![AvoidType::Tolls, AvoidType::Ferries],
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
}
#[tokio::test]
async fn test_routing_with_departure_time() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(body_partial_json(serde_json::json!({
"departureTime": "2024-06-01T00:00:00Z"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
transport_mode: Some(TransportMode::Car),
departure_time: Some(DepartureTime::Timestamp(1717200000)),
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
}
#[tokio::test]
async fn test_routing_with_arrival_time() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(BodyWithoutField("arrivalTime"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
transport_mode: Some(TransportMode::Car),
arrival_time: Some(DepartureTime::Timestamp(1717200000)),
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
}
#[tokio::test]
async fn test_routing_with_alternatives_and_language() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(body_partial_json(serde_json::json!({
"computeAlternativeRoutes": true,
"languageCode": "de-DE"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
transport_mode: Some(TransportMode::Car),
alternatives: Some(3),
language: Some("de-DE".to_string()),
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
}
#[tokio::test]
async fn test_routing_with_provider_extra_waypoints() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(body_partial_json(serde_json::json!({
"intermediates": [
{ "location": { "latLng": { "latitude": 51.5, "longitude": 7.1 } } },
{ "location": { "latLng": { "latitude": 50.1, "longitude": 8.3 } } }
],
"optimizeWaypoints": true
})))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_route_response()))
.mount(&server)
.await;
let router = google_router(&server);
let start = Coordinate::new(52.5163, 13.3777).unwrap();
let end = Coordinate::new(48.8566, 2.3522).unwrap();
let options = RouteOptions {
provider_extra: Some(serde_json::json!({
"waypoints": "51.5,7.1|50.1,8.3",
"optimize_waypoints": true
})),
..Default::default()
};
let result = router
.calculate_route(&start, &end, &options)
.await
.unwrap();
assert_eq!(result.routes.len(), 1);
}