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}