mcprs/
testing.rs

1//! # Módulo de Utilitários de Teste
2//!
3//! Este módulo fornece ferramentas para auxiliar no teste de componentes
4//! que dependem de HTTP, permitindo o mock de chamadas HTTP para isolamento
5//! de testes.
6//!
7//! ## Exemplo de Uso
8//!
9//! ```rust,no_run
10//! use mcprs::testing::{HttpClient, MockHttpClient};
11//! use mockall::predicate;
12//! use serde_json::json;
13//!
14//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15//! // Criar um cliente HTTP mockado
16//! let mut mock_client = MockHttpClient::new();
17//!
18//! // Configurar expectativas do mock
19//! mock_client
20//!     .expect_post()
21//!     .with(
22//!         predicate::eq("https://api.example.com/endpoint".to_string()),
23//!         predicate::always(),
24//!         predicate::always(),
25//!     )
26//!     .times(1)
27//!     .returning(|_, _, _| {
28//!         // Retornar uma resposta simulada
29//!         // ...
30//!         # Ok(reqwest::Response::from(
31//!         #    http::Response::builder()
32//!         #        .status(200)
33//!         #        .body("{}".to_string())
34//!         #        .unwrap(),
35//!         # ))
36//!     });
37//!
38//! // Usar o mock em um componente que depende de HTTP
39//! // ...
40//! # Ok(())
41//! # }
42//! ```
43
44use async_trait::async_trait;
45use mockall::automock;
46use reqwest::Response;
47
48/// Define uma interface abstrata para clientes HTTP.
49///
50/// Esta trait permite abstrair operações HTTP comuns, tornando
51/// mais fácil mock e testar componentes que fazem requisições HTTP.
52#[automock]
53#[async_trait]
54pub trait HttpClient: Send + Sync {
55    /// Executa uma requisição HTTP POST.
56    ///
57    /// # Argumentos
58    /// * `url` - URL para a requisição
59    /// * `body` - Corpo da requisição como bytes
60    /// * `headers` - Cabeçalhos HTTP como pares (nome, valor)
61    ///
62    /// # Retorna
63    /// * `Ok(Response)` - A resposta HTTP
64    /// * `Err(reqwest::Error)` - Se ocorrer um erro na requisição
65    async fn post(
66        &self,
67        url: String,
68        body: Vec<u8>,
69        headers: Vec<(String, String)>,
70    ) -> Result<Response, reqwest::Error>;
71
72    /// Executa uma requisição HTTP GET.
73    ///
74    /// # Argumentos
75    /// * `url` - URL para a requisição
76    /// * `headers` - Cabeçalhos HTTP como pares (nome, valor)
77    ///
78    /// # Retorna
79    /// * `Ok(Response)` - A resposta HTTP
80    /// * `Err(reqwest::Error)` - Se ocorrer um erro na requisição
81    async fn get(
82        &self,
83        url: String,
84        headers: Vec<(String, String)>,
85    ) -> Result<Response, reqwest::Error>;
86}
87
88/// Implementação concreta de `HttpClient` usando o crate reqwest.
89///
90/// Esta é a implementação padrão utilizada em produção.
91pub struct ReqwestClient {
92    /// Cliente reqwest subjacente
93    client: reqwest::Client,
94}
95
96impl ReqwestClient {
97    /// Cria uma nova instância de `ReqwestClient` com configuração padrão.
98    ///
99    /// # Exemplo
100    ///
101    /// ```
102    /// use mcprs::testing::ReqwestClient;
103    ///
104    /// let client = ReqwestClient::new();
105    /// ```
106    pub fn new() -> Self {
107        Self {
108            client: reqwest::Client::new(),
109        }
110    }
111
112    /// Cria uma nova instância com um cliente reqwest específico.
113    ///
114    /// # Argumentos
115    /// * `client` - Um cliente reqwest pré-configurado
116    ///
117    /// # Exemplo
118    ///
119    /// ```
120    /// use mcprs::testing::ReqwestClient;
121    ///
122    /// let reqwest_client = reqwest::Client::builder()
123    ///     .timeout(std::time::Duration::from_secs(30))
124    ///     .build()
125    ///     .unwrap();
126    ///
127    /// let client = ReqwestClient::with_client(reqwest_client);
128    /// ```
129    pub fn with_client(client: reqwest::Client) -> Self {
130        Self { client }
131    }
132}
133
134#[async_trait]
135impl HttpClient for ReqwestClient {
136    async fn post(
137        &self,
138        url: String,
139        body: Vec<u8>,
140        headers: Vec<(String, String)>,
141    ) -> Result<Response, reqwest::Error> {
142        let mut request = self.client.post(url);
143
144        for (key, value) in headers {
145            request = request.header(key, value);
146        }
147
148        request.body(body).send().await
149    }
150
151    async fn get(
152        &self,
153        url: String,
154        headers: Vec<(String, String)>,
155    ) -> Result<Response, reqwest::Error> {
156        let mut request = self.client.get(url);
157
158        for (key, value) in headers {
159            request = request.header(key, value);
160        }
161
162        request.send().await
163    }
164}
165
166/// Factory trait para criar instâncias de HttpClient.
167///
168/// Esta trait permite a injeção de dependência de fábricas
169/// de HttpClient, facilitando testes.
170#[automock]
171pub trait HttpClientFactory {
172    /// Cria uma nova instância de HttpClient.
173    ///
174    /// # Retorna
175    /// Uma implementação concreta de HttpClient encapsulada em Box
176    fn create_client(&self) -> Box<dyn HttpClient>;
177}
178
179/// Implementação padrão de HttpClientFactory que cria ReqwestClient.
180pub struct ReqwestClientFactory;
181
182impl HttpClientFactory for ReqwestClientFactory {
183    fn create_client(&self) -> Box<dyn HttpClient> {
184        Box::new(ReqwestClient::new())
185    }
186}
187
188impl Default for ReqwestClientFactory {
189    fn default() -> Self {
190        Self
191    }
192}
193
194impl Default for ReqwestClient {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use mockall::predicate;
204
205    // Teste apenas para demonstrar como usar o mock
206    #[tokio::test]
207    async fn test_mock_http_client() {
208        let mut mock = MockHttpClient::new();
209
210        // Configurar o comportamento do mock
211        mock.expect_post()
212            .with(
213                predicate::eq("https://test.example.com".to_string()),
214                predicate::always(),
215                predicate::always(),
216            )
217            .times(1)
218            .returning(|_, _, _| {
219                Ok(reqwest::Response::from(
220                    http::Response::builder()
221                        .status(200)
222                        .body("Test Response")
223                        .unwrap(),
224                ))
225            });
226
227        // Usar o mock
228        let result = mock
229            .post(
230                "https://test.example.com".to_string(),
231                b"test body".to_vec(),
232                vec![("Content-Type".to_string(), "text/plain".to_string())],
233            )
234            .await;
235
236        assert!(result.is_ok());
237        let response = result.unwrap();
238        assert_eq!(response.status(), 200);
239    }
240
241    #[tokio::test]
242    async fn test_mock_http_client_factory() {
243        let mut mock_factory = MockHttpClientFactory::new();
244
245        // Configurar a fábrica para retornar um mock configurado
246        mock_factory.expect_create_client().times(1).returning(|| {
247            // Criamos e configuramos um novo mock dentro do closure
248            let mut new_mock = MockHttpClient::new();
249            new_mock.expect_get().returning(|_, _| {
250                Ok(reqwest::Response::from(
251                    http::Response::builder()
252                        .status(200)
253                        .body("Factory Test")
254                        .unwrap(),
255                ))
256            });
257            Box::new(new_mock)
258        });
259
260        // Criar cliente via fábrica
261        let client = mock_factory.create_client();
262
263        // O teste real seria mais elaborado, isso é apenas para demonstrar o uso
264        assert!(client
265            .get("https://example.com".to_string(), vec![])
266            .await
267            .is_ok());
268    }
269}