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
//! Cliente HTTP: URLs, headers, query, corpo e hooks.

mod common;

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;

use apibrasil::core::{HttpClient, ResponseData};
use apibrasil::{ApiBrasil, Config, FnHooks, Method, RequestOptions, ResponseType, RetryConfig};
use common::{
    build_api, build_api_anonymous, build_api_with, ok, raw_bytes, FakeTransport, BASE_URL,
};
use serde_json::json;

#[tokio::test]
async fn url_nao_duplica_barras() {
    let (api, transport) = build_api_with(Config {
        base_url: Some(format!("{BASE_URL}/")),
        ..Default::default()
    });

    api.request(Method::Get, "/balance", None).await.unwrap();

    assert_eq!(transport.last().url, format!("{BASE_URL}/balance"));
}

#[tokio::test]
async fn query_sai_ordenada_e_ignora_nulos() {
    let (api, transport) = build_api();

    api.with_options(
        RequestOptions::new().query(json!({ "z": 1, "a": "x y", "n": null, "b": true })),
    )
    .request(Method::Get, "balance", None)
    .await
    .unwrap();

    assert_eq!(
        transport.last().url,
        format!("{BASE_URL}/balance?a=x+y&b=true&z=1")
    );
}

#[tokio::test]
async fn headers_do_cliente_e_da_requisicao_sao_aplicados() {
    let (api, transport) = build_api_with(Config::new().header("X-Cliente", "sdk"));

    api.with_options(RequestOptions::new().header("X-Chamada", "1"))
        .request(Method::Get, "balance", None)
        .await
        .unwrap();

    assert_eq!(transport.last_header("X-Cliente").as_deref(), Some("sdk"));
    assert_eq!(transport.last_header("X-Chamada").as_deref(), Some("1"));
}

#[tokio::test]
async fn tokens_da_requisicao_sobrescrevem_os_do_cliente() {
    let (api, transport) = build_api();

    api.with_options(
        RequestOptions::new()
            .bearer_token("outro-jwt")
            .device_token("outro-device"),
    )
    .request(Method::Get, "balance", None)
    .await
    .unwrap();

    assert_eq!(
        transport.last_header("Authorization").as_deref(),
        Some("Bearer outro-jwt")
    );
    assert_eq!(
        transport.last_header("DeviceToken").as_deref(),
        Some("outro-device")
    );
}

#[tokio::test]
async fn sem_credenciais_nao_envia_headers_de_autenticacao() {
    let (api, transport) = build_api_anonymous();

    api.catalog().status().await.unwrap();

    assert_eq!(transport.last_header("Authorization"), None);
    assert_eq!(transport.last_header("DeviceToken"), None);
}

#[tokio::test]
async fn tokens_definidos_depois_valem_para_todos_os_servicos() {
    let (api, transport) = build_api_anonymous();

    api.set_bearer_token("jwt-novo")
        .set_device_token("dev-novo");
    api.whatsapp()
        .send_text(json!({ "text": "oi" }))
        .await
        .unwrap();

    assert_eq!(
        transport.last_header("Authorization").as_deref(),
        Some("Bearer jwt-novo")
    );
    assert_eq!(
        transport.last_header("DeviceToken").as_deref(),
        Some("dev-novo")
    );

    api.clear_bearer_token().clear_device_token();
    api.whatsapp()
        .send_text(json!({ "text": "oi" }))
        .await
        .unwrap();

    assert_eq!(transport.last_header("Authorization"), None);
    assert_eq!(transport.last_header("DeviceToken"), None);
}

#[tokio::test]
async fn with_device_isola_o_device_e_mantem_as_credenciais() {
    let transport = FakeTransport::new();
    let api = ApiBrasil::new(Config {
        transport: Some(Arc::new(transport.clone())),
        retry: Some(RetryConfig::none()),
        base_url: Some(BASE_URL.to_string()),
        bearer_token: Some("jwt".to_string()),
        device_token: Some("dev-1".to_string()),
        ..Default::default()
    });

    let outro = api.with_device("dev-2");
    outro.whatsapp().send_text(None).await.unwrap();

    assert_eq!(
        transport.last_header("DeviceToken").as_deref(),
        Some("dev-2")
    );
    assert_eq!(
        transport.last_header("Authorization").as_deref(),
        Some("Bearer jwt")
    );

    api.whatsapp().send_text(None).await.unwrap();
    assert_eq!(
        transport.last_header("DeviceToken").as_deref(),
        Some("dev-1")
    );
}

#[tokio::test]
async fn resposta_vazia_vira_objeto_vazio() {
    let (api, transport) = build_api();
    transport.respond_with(apibrasil::TransportResponse {
        status: 200,
        headers: Default::default(),
        data: ResponseData::Value(serde_json::Value::Null),
        body: Vec::new(),
    });

    let response = api.request(Method::Get, "balance", None).await.unwrap();

    assert_eq!(response, json!({}));
}

#[tokio::test]
async fn resposta_que_nao_e_objeto_e_embrulhada_em_data() {
    let (api, transport) = build_api();
    transport.respond_with(ok(json!([1, 2, 3])));

    let response = api.request(Method::Get, "servers", None).await.unwrap();

    assert_eq!(response, json!({ "data": [1, 2, 3] }));
}

#[tokio::test]
async fn execute_devolve_a_resposta_sem_normalizar() {
    let (api, transport) = build_api();
    transport.respond_with(ok(json!([1, 2, 3])));

    let data = api.execute(Method::Get, "servers", None).await.unwrap();

    assert_eq!(data.into_value(), json!([1, 2, 3]));
}

#[tokio::test]
async fn download_pede_bytes_e_devolve_o_corpo_cru() {
    let (api, transport) = build_api();
    transport.respond_with(raw_bytes(200, &[0x25, 0x50, 0x44, 0x46]));

    let bytes = api.download("inter/boleto/1/pdf").await.unwrap();

    assert_eq!(bytes, vec![0x25, 0x50, 0x44, 0x46]);
    assert_eq!(transport.last().response_type, ResponseType::Bytes);
}

#[tokio::test]
async fn timeout_da_requisicao_chega_ao_transporte() {
    let (api, transport) = build_api_with(Config::new().timeout(Duration::from_secs(7)));

    api.request(Method::Get, "balance", None).await.unwrap();
    assert_eq!(transport.last().timeout, Some(Duration::from_secs(7)));

    api.with_options(RequestOptions::new().timeout(Duration::from_secs(1)))
        .request(Method::Get, "balance", None)
        .await
        .unwrap();
    assert_eq!(transport.last().timeout, Some(Duration::from_secs(1)));
}

#[tokio::test]
async fn hooks_recebem_requisicao_e_resposta() {
    let requisicoes = Arc::new(AtomicUsize::new(0));
    let respostas = Arc::new(AtomicUsize::new(0));

    let hooks = {
        let requisicoes = requisicoes.clone();
        let respostas = respostas.clone();
        FnHooks::new()
            .on_request(move |info| {
                assert_eq!(info.method, Method::Post);
                assert!(info.url.ends_with("/whatsapp/sendText"));
                assert_eq!(info.attempt, 0);
                assert!(info.headers.contains_key("Authorization"));
                requisicoes.fetch_add(1, Ordering::SeqCst);
            })
            .on_response(move |info| {
                assert_eq!(info.status, 200);
                respostas.fetch_add(1, Ordering::SeqCst);
            })
    };

    let (api, _transport) = build_api_with(Config::new().hooks(hooks));
    api.whatsapp()
        .send_text(json!({ "text": "oi" }))
        .await
        .unwrap();

    assert_eq!(requisicoes.load(Ordering::SeqCst), 1);
    assert_eq!(respostas.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn config_reflete_o_estado_atual_do_cliente() {
    let (api, _transport) = build_api();

    api.set_bearer_token("outro");
    let config = api.config();

    assert_eq!(config.bearer_token.as_deref(), Some("outro"));
    assert_eq!(config.device_token.as_deref(), Some("dev"));
    assert_eq!(config.base_url.as_deref(), Some(BASE_URL));
}

#[test]
fn config_do_ambiente_tem_prioridade_menor() {
    let doambiente = Config::from_env();
    let explicita = doambiente.merge(Config::new().bearer_token("explicito"));

    assert_eq!(explicita.bearer_token.as_deref(), Some("explicito"));
}

#[test]
fn cliente_http_usa_os_padroes_da_plataforma() {
    let client = HttpClient::new(Config {
        base_url: Some(BASE_URL.to_string()),
        ..Default::default()
    });

    assert_eq!(client.base_url(), BASE_URL);
    assert_eq!(client.timeout(), apibrasil::DEFAULT_TIMEOUT);
    assert_eq!(client.url("/balance"), format!("{BASE_URL}/balance"));
}