mod common;
use common::mock_client;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use ticksupply::Error;
use wiremock::matchers::{method, path};
use wiremock::{Mock, Request, ResponseTemplate};
fn count_responder(
counter: Arc<AtomicUsize>,
fail_first_n: usize,
then_status: u16,
then_body: serde_json::Value,
fail_status: u16,
fail_body: serde_json::Value,
) -> impl Fn(&Request) -> ResponseTemplate + Send + Sync + 'static {
move |_req: &Request| {
let n = counter.fetch_add(1, Ordering::SeqCst);
if n < fail_first_n {
ResponseTemplate::new(fail_status).set_body_json(&fail_body)
} else {
ResponseTemplate::new(then_status).set_body_json(&then_body)
}
}
}
#[tokio::test]
async fn get_is_retried_on_5xx() {
let server = wiremock::MockServer::start().await;
let client = ticksupply::Client::builder()
.api_key("k")
.base_url(format!("{}/v1", server.uri()))
.max_retries(2)
.build()
.unwrap();
let counter = Arc::new(AtomicUsize::new(0));
Mock::given(method("GET"))
.and(path("/v1/exchanges"))
.respond_with(count_responder(
counter.clone(),
2,
200,
serde_json::json!([]),
503,
serde_json::json!({ "error": { "code": "server_error", "message": "boom" } }),
))
.mount(&server)
.await;
let result = client.exchanges().list().await;
assert!(result.is_ok(), "expected ok after retries: {result:?}");
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn post_without_idempotency_key_is_not_retried() {
let (server, _client) = mock_client().await;
let client = ticksupply::Client::builder()
.api_key("k")
.base_url(format!("{}/v1", server.uri()))
.max_retries(3)
.build()
.unwrap();
let counter = Arc::new(AtomicUsize::new(0));
Mock::given(method("POST"))
.and(path("/v1/subscriptions"))
.respond_with(count_responder(
counter.clone(),
10,
200,
serde_json::json!({}),
503,
serde_json::json!({ "error": { "code": "server_error", "message": "boom" } }),
))
.mount(&server)
.await;
let err = client.subscriptions().create(42).send().await.unwrap_err();
assert!(matches!(err, Error::Api { status: 503, .. }));
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"POST should not retry without idempotency key"
);
}
#[tokio::test]
async fn post_with_idempotency_key_retries() {
let server = wiremock::MockServer::start().await;
let client = ticksupply::Client::builder()
.api_key("k")
.base_url(format!("{}/v1", server.uri()))
.max_retries(2)
.build()
.unwrap();
let counter = Arc::new(AtomicUsize::new(0));
Mock::given(method("POST"))
.and(path("/v1/subscriptions"))
.respond_with(count_responder(
counter.clone(),
2,
200,
serde_json::json!({
"id": "sub_x",
"status": "active",
"datastream": {
"datastream_id": 42,
"exchange": "binance",
"instrument": "BTCUSDT",
"stream_type": "trades",
"wire_format": "json"
},
"created_at": "2024-01-01T00:00:00Z",
"spans": []
}),
503,
serde_json::json!({ "error": { "code": "server_error", "message": "boom" } }),
))
.mount(&server)
.await;
let sub = client
.subscriptions()
.create(42)
.idempotency_key("00000000-0000-4000-8000-000000000000")
.send()
.await
.unwrap();
assert_eq!(sub.id, "sub_x");
assert_eq!(counter.load(Ordering::SeqCst), 3);
}