mod common;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use apibrasil::{Config, FnHooks, RetryConfig};
use common::{build_api_with, http_error, ok};
use serde_json::json;
fn retry_rapido(retries: u32) -> RetryConfig {
RetryConfig {
retries,
min_delay: Duration::from_millis(1),
max_delay: Duration::from_millis(5),
..Default::default()
}
}
#[tokio::test]
async fn refaz_em_429_ate_o_limite_de_tentativas() {
let (api, transport) = build_api_with(Config::new().retry(retry_rapido(2)));
transport
.respond_with(http_error(429, json!({ "message": "devagar" }), &[]))
.respond_with(http_error(429, json!({ "message": "devagar" }), &[]))
.respond_with(ok(json!({ "error": false })));
api.consulta().cpf(json!({ "cpf": "0" })).await.unwrap();
assert_eq!(transport.count(), 3);
}
#[tokio::test]
async fn desiste_depois_de_esgotar_as_tentativas() {
let (api, transport) = build_api_with(Config::new().retry(retry_rapido(1)));
transport.set_fallback(http_error(429, json!({ "message": "devagar" }), &[]));
let error = api.consulta().cpf(None).await.unwrap_err();
assert!(error.is_rate_limit());
assert_eq!(transport.count(), 2);
}
#[tokio::test]
async fn refaz_em_falhas_de_conexao() {
let (api, transport) = build_api_with(Config::new().retry(retry_rapido(2)));
transport
.fail_with(apibrasil::Error::network("conexão recusada"))
.respond_with(ok(json!({ "ok": true })));
api.consulta().cpf(None).await.unwrap();
assert_eq!(transport.count(), 2);
}
#[tokio::test]
async fn nao_refaz_em_timeout_para_nao_duplicar_cobranca() {
let (api, transport) = build_api_with(Config::new().retry(retry_rapido(3)));
transport.fail_with(apibrasil::Error::timeout("tempo limite"));
let error = api.consulta().cpf(None).await.unwrap_err();
assert!(error.is_timeout());
assert_eq!(transport.count(), 1);
}
#[tokio::test]
async fn nao_refaz_em_erros_de_negocio() {
for status in [400, 401, 402, 403, 404, 500] {
let (api, transport) = build_api_with(Config::new().retry(retry_rapido(3)));
transport.set_fallback(http_error(status, json!({ "message": "x" }), &[]));
api.consulta().cpf(None).await.unwrap_err();
assert_eq!(transport.count(), 1, "status {status} não deve ser refeito");
}
}
#[tokio::test]
async fn retry_desligado_faz_uma_unica_tentativa() {
let (api, transport) = build_api_with(Config::new().retry(RetryConfig::none()));
transport.set_fallback(http_error(429, json!({}), &[]));
api.consulta().cpf(None).await.unwrap_err();
assert_eq!(transport.count(), 1);
}
#[tokio::test]
async fn status_extras_podem_ser_refeitos() {
let (api, transport) = build_api_with(Config::new().retry(RetryConfig {
retry_on_statuses: vec![429, 503],
..retry_rapido(1)
}));
transport
.respond_with(http_error(503, json!({}), &[]))
.respond_with(ok(json!({ "ok": true })));
api.consulta().cpf(None).await.unwrap();
assert_eq!(transport.count(), 2);
}
#[tokio::test]
async fn hook_de_retry_informa_tentativa_e_motivo() {
let retries = Arc::new(AtomicUsize::new(0));
let hooks = {
let retries = retries.clone();
FnHooks::new().on_retry(move |info| {
assert_eq!(info.reason, "HTTP 429");
assert_eq!(info.attempt, 1);
assert_eq!(info.delay, Duration::from_millis(50));
retries.fetch_add(1, Ordering::SeqCst);
})
};
let (api, transport) = build_api_with(
Config::new()
.retry(RetryConfig {
retries: 1,
min_delay: Duration::from_millis(1),
max_delay: Duration::from_millis(1),
..Default::default()
})
.hooks(hooks),
);
transport
.respond_with(http_error(429, json!({}), &[("Retry-After", "0.05")]))
.respond_with(ok(json!({ "ok": true })));
api.consulta().cpf(None).await.unwrap();
assert_eq!(retries.load(Ordering::SeqCst), 1);
}
#[test]
fn backoff_cresce_e_respeita_o_teto() {
let retry = RetryConfig {
retries: 5,
min_delay: Duration::from_millis(100),
max_delay: Duration::from_millis(500),
retry_on_statuses: vec![429],
};
assert!(retry.backoff_delay(0) >= Duration::from_millis(50));
assert!(retry.backoff_delay(0) < Duration::from_millis(100));
assert!(retry.backoff_delay(1) >= Duration::from_millis(100));
assert!(retry.backoff_delay(1) < Duration::from_millis(200));
assert_eq!(retry.backoff_delay(20), Duration::from_millis(500));
}
#[test]
fn politica_padrao_refaz_so_em_429() {
let retry = RetryConfig::default();
assert_eq!(retry.retries, 2);
assert_eq!(retry.max_attempts(), 3);
assert!(retry.retries_status(429));
assert!(!retry.retries_status(500));
assert!(!retry.retries_status(402));
assert_eq!(RetryConfig::none().max_attempts(), 1);
}