use std::time::Duration;
use openmeteo_rs::{Client, Error, ForecastBuilder, HourlyVar, Result};
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn forecast_error_request(client: &Client) -> ForecastBuilder<'_> {
client
.forecast(52.52, 13.41)
.hourly([HourlyVar::Temperature2m])
.forecast_days(1)
}
#[tokio::test]
async fn http_400_api_error_body_maps_to_api_error() -> Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/forecast"))
.and(query_param("latitude", "52.52"))
.and(query_param("longitude", "13.41"))
.and(query_param("hourly", "temperature_2m"))
.and(query_param("forecast_days", "1"))
.respond_with(ResponseTemplate::new(400).set_body_raw(
r#"{"error":true,"reason":"invalid latitude"}"#,
"application/json",
))
.mount(&server)
.await;
let client = Client::builder().forecast_base_url(server.uri())?.build()?;
let err = forecast_error_request(&client).send().await.unwrap_err();
match err {
Error::Api { status, reason } => {
assert_eq!(status, 400);
assert_eq!(reason, "invalid latitude");
}
other => panic!("expected API error, got {other:?}"),
}
Ok(())
}
#[tokio::test]
async fn http_400_empty_api_error_reason_falls_back_to_body_text() -> Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/forecast"))
.respond_with(
ResponseTemplate::new(400)
.set_body_raw(r#"{"error":true,"reason":""}"#, "application/json"),
)
.mount(&server)
.await;
let client = Client::builder().forecast_base_url(server.uri())?.build()?;
let err = forecast_error_request(&client).send().await.unwrap_err();
match err {
Error::Api { status, reason } => {
assert_eq!(status, 400);
assert_eq!(reason, r#"{"error":true,"reason":""}"#);
}
other => panic!("expected API error, got {other:?}"),
}
Ok(())
}
#[tokio::test]
async fn elevation_http_400_maps_before_sanitizing_success_body() -> Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/elevation"))
.respond_with(ResponseTemplate::new(400).set_body_raw(
r#"{"error":true,"reason":"bad coordinate nan"}"#,
"application/json",
))
.mount(&server)
.await;
let client = Client::builder()
.elevation_base_url(server.uri())?
.build()?;
let err = client.elevation([(52.52, 13.41)]).await.unwrap_err();
match err {
Error::Api { status, reason } => {
assert_eq!(status, 400);
assert_eq!(reason, "bad coordinate nan");
}
other => panic!("expected API error, got {other:?}"),
}
Ok(())
}
#[tokio::test]
async fn http_429_with_retry_after_maps_to_rate_limited() -> Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/forecast"))
.respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "30"))
.mount(&server)
.await;
let client = Client::builder().forecast_base_url(server.uri())?.build()?;
let err = forecast_error_request(&client).send().await.unwrap_err();
match err {
Error::RateLimited { retry_after } => {
assert_eq!(retry_after, Some(Duration::from_secs(30)));
}
other => panic!("expected rate limit error, got {other:?}"),
}
Ok(())
}
#[tokio::test]
async fn http_429_without_retry_after_maps_to_rate_limited_without_duration() -> Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/forecast"))
.respond_with(ResponseTemplate::new(429))
.mount(&server)
.await;
let client = Client::builder().forecast_base_url(server.uri())?.build()?;
let err = forecast_error_request(&client).send().await.unwrap_err();
match err {
Error::RateLimited { retry_after } => {
assert_eq!(retry_after, None);
}
other => panic!("expected rate limit error, got {other:?}"),
}
Ok(())
}
#[tokio::test]
async fn transport_timeout_maps_to_timeout_error() -> Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/forecast"))
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_millis(100)))
.mount(&server)
.await;
let client = Client::builder()
.forecast_base_url(server.uri())?
.timeout(Duration::from_millis(10))
.build()?;
let err = forecast_error_request(&client).send().await.unwrap_err();
match err {
Error::Timeout(duration) => {
assert_eq!(duration, Duration::from_millis(10));
}
other => panic!("expected timeout error, got {other:?}"),
}
Ok(())
}