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
//! Transporte fake e utilitários compartilhados pelos testes.
//!
//! Cada arquivo de teste é um binário próprio e usa apenas parte destes
//! utilitários.
#![allow(dead_code)]

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use apibrasil::core::transport::{BoxFuture, Transport, TransportRequest, TransportResponse};
use apibrasil::{ApiBrasil, Config, Error, Json, Method, ResponseData, Result, RetryConfig};
use serde_json::{json, Value};

/// Base usada em todos os testes de rota.
pub const BASE_URL: &str = "https://gateway.apibrasil.io/api/v2";

enum Programmed {
    Response(TransportResponse),
    Failure(Error),
}

#[derive(Default)]
struct State {
    calls: Vec<TransportRequest>,
    queue: Vec<Programmed>,
    fallback: Option<TransportResponse>,
}

/// Transporte fake: grava todas as requisições e responde com uma fila
/// programável (ou um fallback 200).
#[derive(Clone, Default)]
pub struct FakeTransport {
    state: Arc<Mutex<State>>,
}

impl FakeTransport {
    /// Cria o transporte com o fallback `200 {"ok": true}`.
    pub fn new() -> Self {
        let transport = Self::default();
        transport.state.lock().unwrap().fallback = Some(ok(json!({ "ok": true })));
        transport
    }

    /// Enfileira uma resposta, consumida em ordem.
    pub fn respond_with(&self, response: TransportResponse) -> &Self {
        self.state
            .lock()
            .unwrap()
            .queue
            .push(Programmed::Response(response));
        self
    }

    /// Enfileira uma falha de transporte (sem resposta).
    pub fn fail_with(&self, error: Error) -> &Self {
        self.state
            .lock()
            .unwrap()
            .queue
            .push(Programmed::Failure(error));
        self
    }

    /// Define a resposta padrão quando a fila está vazia.
    pub fn set_fallback(&self, response: TransportResponse) -> &Self {
        self.state.lock().unwrap().fallback = Some(response);
        self
    }

    /// Última requisição recebida.
    pub fn last(&self) -> TransportRequest {
        self.state
            .lock()
            .unwrap()
            .calls
            .last()
            .cloned()
            .expect("nenhuma requisição foi feita")
    }

    /// Body JSON decodificado da última requisição.
    pub fn last_body(&self) -> Option<Json> {
        self.last().json_body()
    }

    /// Header da última requisição.
    pub fn last_header(&self, name: &str) -> Option<String> {
        let request = self.last();
        request
            .headers
            .iter()
            .find(|(key, _)| key.eq_ignore_ascii_case(name))
            .map(|(_, value)| value.clone())
    }

    /// Todas as requisições recebidas.
    pub fn calls(&self) -> Vec<TransportRequest> {
        self.state.lock().unwrap().calls.clone()
    }

    /// Quantas requisições foram feitas.
    pub fn count(&self) -> usize {
        self.state.lock().unwrap().calls.len()
    }
}

impl Transport for FakeTransport {
    fn execute<'a>(
        &'a self,
        request: TransportRequest,
    ) -> BoxFuture<'a, Result<TransportResponse>> {
        let next = {
            let mut state = self.state.lock().unwrap();
            state.calls.push(request);
            if state.queue.is_empty() {
                Programmed::Response(
                    state
                        .fallback
                        .clone()
                        .unwrap_or_else(|| ok(json!({ "ok": true }))),
                )
            } else {
                state.queue.remove(0)
            }
        };

        Box::pin(async move {
            match next {
                Programmed::Response(response) => Ok(response),
                Programmed::Failure(error) => Err(error),
            }
        })
    }
}

/// Resposta 200 com o corpo informado.
pub fn ok(data: Value) -> TransportResponse {
    TransportResponse::json(200, data)
}

/// Resposta de erro com status, corpo e headers.
pub fn http_error(status: u16, data: Value, headers: &[(&str, &str)]) -> TransportResponse {
    let mut response = TransportResponse::json(status, data);
    response.headers = headers
        .iter()
        .map(|(name, value)| (name.to_ascii_lowercase(), value.to_string()))
        .collect();
    response
}

/// Resposta com bytes crus.
pub fn raw_bytes(status: u16, body: &[u8]) -> TransportResponse {
    TransportResponse {
        status,
        headers: HashMap::new(),
        data: ResponseData::Bytes(body.to_vec()),
        body: body.to_vec(),
    }
}

/// Cria um cliente com transporte fake, retry desligado e credenciais
/// fixas — base de todos os testes de rota.
pub fn build_api() -> (ApiBrasil, FakeTransport) {
    build_api_with(Config::default())
}

/// Como [`build_api`], mas sem credenciais — nem as do ambiente, já que
/// tokens vazios contam como ausentes.
pub fn build_api_anonymous() -> (ApiBrasil, FakeTransport) {
    build_api_with(Config {
        bearer_token: Some(String::new()),
        device_token: Some(String::new()),
        ..Default::default()
    })
}

/// Como [`build_api`], sobrepondo a configuração.
pub fn build_api_with(overrides: Config) -> (ApiBrasil, FakeTransport) {
    let transport = FakeTransport::new();

    let config = 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".to_string()),
        ..Default::default()
    }
    .merge(overrides);

    (ApiBrasil::new(config), transport)
}

/// Confere método, URL e body da última requisição.
#[track_caller]
pub fn assert_request(transport: &FakeTransport, method: Method, path: &str, body: Option<Value>) {
    let request = transport.last();

    assert_eq!(request.method, method, "método da requisição");
    assert_eq!(
        request.url,
        format!("{BASE_URL}{path}"),
        "url da requisição"
    );

    if let Some(expected) = body {
        assert_eq!(transport.last_body(), Some(expected), "body da requisição");
    }
}