apibrasil 1.0.0

SDK oficial Rust da plataforma APIBrasil: WhatsApp, SMS, consultas de CPF/CNPJ, veiculos, CEP, correios, pagamentos PIX/boleto e mais.
Documentation
//! Mapeamento de status HTTP para a hierarquia de erros da SDK.

mod common;

use std::time::Duration;

use apibrasil::ErrorKind;
use common::{build_api, http_error};
use serde_json::json;

async fn erro_para(status: u16, body: serde_json::Value) -> apibrasil::Error {
    let (api, transport) = build_api();
    transport.respond_with(http_error(status, body, &[]));

    api.consulta()
        .cpf(json!({ "cpf": "00000000000" }))
        .await
        .expect_err("a chamada deveria falhar")
}

#[tokio::test]
async fn status_viram_categorias_de_erro() {
    let casos = [
        (400, ErrorKind::Validation),
        (401, ErrorKind::Authentication),
        (402, ErrorKind::InsufficientBalance),
        (403, ErrorKind::Permission),
        (404, ErrorKind::NotFound),
        (410, ErrorKind::NotFound),
        (418, ErrorKind::Api),
        (422, ErrorKind::Validation),
        (429, ErrorKind::RateLimit),
        (500, ErrorKind::Server),
        (503, ErrorKind::Server),
    ];

    for (status, esperado) in casos {
        let error = erro_para(status, json!({ "message": "falhou" })).await;
        assert_eq!(error.kind(), esperado, "status {status}");
        assert_eq!(error.status, Some(status));
    }
}

#[tokio::test]
async fn erro_preserva_mensagem_codigo_e_corpo() {
    let error = erro_para(404, json!({ "message": "sem dados", "code": "NOT_FOUND" })).await;

    assert_eq!(error.message, "sem dados");
    assert_eq!(error.code.as_deref(), Some("NOT_FOUND"));
    assert!(error.is_not_found());
    assert_eq!(error.response.as_ref().unwrap()["code"], json!("NOT_FOUND"));
}

#[tokio::test]
async fn erro_usa_o_campo_error_como_mensagem() {
    let error = erro_para(400, json!({ "error": "payload inválido" })).await;
    assert_eq!(error.message, "payload inválido");
}

#[tokio::test]
async fn erro_sem_mensagem_descreve_o_status() {
    let error = erro_para(500, json!({})).await;
    assert_eq!(error.message, "A API respondeu com HTTP 500.");
}

#[tokio::test]
async fn display_do_erro_traz_status_e_codigo() {
    let error = erro_para(402, json!({ "message": "sem saldo", "code": "NO_BALANCE" })).await;
    assert_eq!(error.to_string(), "sem saldo (HTTP 402) [NO_BALANCE]");
    assert!(error.is_insufficient_balance());
}

#[tokio::test]
async fn rate_limit_le_o_retry_after_em_segundos() {
    let (api, transport) = build_api();
    transport.respond_with(http_error(
        429,
        json!({ "message": "devagar" }),
        &[("Retry-After", "12")],
    ));

    let error = api.consulta().cpf(None).await.unwrap_err();

    assert!(error.is_rate_limit());
    assert_eq!(error.retry_after, Some(Duration::from_secs(12)));
}

#[tokio::test]
async fn rate_limit_le_o_retry_after_como_data_http() {
    let (api, transport) = build_api();
    transport.respond_with(http_error(
        429,
        json!({}),
        // Data no passado: nada a esperar.
        &[("retry-after", "Wed, 21 Oct 2015 07:28:00 GMT")],
    ));

    let error = api.consulta().cpf(None).await.unwrap_err();
    assert_eq!(error.retry_after, None);
}

#[tokio::test]
async fn falha_de_rede_vira_erro_de_rede() {
    let (api, transport) = build_api();
    transport.fail_with(apibrasil::Error::network("conexão recusada"));

    let error = api.consulta().cpf(None).await.unwrap_err();

    assert_eq!(error.kind(), ErrorKind::Network);
    assert!(error.is_network());
    assert!(!error.is_timeout());
    assert_eq!(error.status, None);
}

#[tokio::test]
async fn timeout_conta_como_falha_de_rede_mas_e_distinguivel() {
    let (api, transport) = build_api();
    transport.fail_with(apibrasil::Error::timeout("tempo limite"));

    let error = api.consulta().cpf(None).await.unwrap_err();

    assert_eq!(error.kind(), ErrorKind::Timeout);
    assert!(error.is_network());
    assert!(error.is_timeout());
}

#[tokio::test]
async fn erro_de_decodificacao_e_de_validacao() {
    let (api, transport) = build_api();
    transport.respond_with(common::ok(json!({ "response": "texto" })));

    #[derive(Debug, serde::Deserialize)]
    struct Esperado {
        #[allow(dead_code)]
        id: u32,
    }

    let response = api.whatsapp().send_text(None).await.unwrap();
    let error = response.decode::<Esperado>().unwrap_err();

    assert!(error.is_validation());
}