1use super::{HttpClient, Request, Response};
7use crate::error::ScopeError;
8use async_trait::async_trait;
9use std::time::Duration;
10
11pub struct NativeHttpClient {
14 client: reqwest::Client,
15}
16
17impl NativeHttpClient {
18 pub fn new() -> Result<Self, ScopeError> {
20 let client = reqwest::Client::builder()
21 .timeout(Duration::from_secs(30))
22 .build()
23 .map_err(|e| ScopeError::Network(format!("failed to build HTTP client: {e}")))?;
24 Ok(Self { client })
25 }
26}
27
28#[async_trait]
29impl HttpClient for NativeHttpClient {
30 async fn send(&self, request: Request) -> Result<Response, ScopeError> {
31 let method: reqwest::Method = request
32 .method
33 .parse()
34 .map_err(|e| ScopeError::Network(format!("invalid HTTP method: {e}")))?;
35
36 let mut builder = self.client.request(method, &request.url);
37
38 for (k, v) in &request.headers {
39 builder = builder.header(k.as_str(), v.as_str());
40 }
41
42 if let Some(body) = request.body {
43 builder = builder.body(body);
44 }
45
46 let resp = builder.send().await?;
47 let status = resp.status().as_u16();
48
49 let mut headers = std::collections::HashMap::new();
50 for (k, v) in resp.headers() {
51 headers.insert(k.to_string(), v.to_str().unwrap_or("").to_string());
52 }
53
54 let body = resp.text().await?;
55
56 Ok(Response {
57 status_code: status,
58 headers,
59 body,
60 })
61 }
62}
63
64#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn test_native_client_creation() {
74 let client = NativeHttpClient::new();
75 assert!(client.is_ok());
76 }
77
78 #[tokio::test]
79 async fn test_send_get_request() {
80 let mut server = mockito::Server::new_async().await;
81 let mock = server
82 .mock("GET", "/test")
83 .with_status(200)
84 .with_header("x-test", "hello")
85 .with_body(r#"{"ok":true}"#)
86 .create_async()
87 .await;
88
89 let client = NativeHttpClient::new().unwrap();
90 let req = Request::get(&format!("{}/test", server.url()));
91 let resp = client.send(req).await.unwrap();
92
93 assert_eq!(resp.status_code, 200);
94 assert!(resp.is_success());
95 assert!(resp.body.contains("\"ok\":true"));
96 mock.assert_async().await;
97 }
98
99 #[tokio::test]
100 async fn test_send_post_json_request() {
101 let mut server = mockito::Server::new_async().await;
102 let mock = server
103 .mock("POST", "/api")
104 .match_header("content-type", "application/json")
105 .match_body(r#"{"key":"val"}"#)
106 .with_status(201)
107 .with_body(r#"{"created":true}"#)
108 .create_async()
109 .await;
110
111 let client = NativeHttpClient::new().unwrap();
112 let req = Request::post_json(
113 &format!("{}/api", server.url()),
114 r#"{"key":"val"}"#,
115 );
116 let resp = client.send(req).await.unwrap();
117
118 assert_eq!(resp.status_code, 201);
119 assert!(resp.is_success());
120 let parsed: serde_json::Value = resp.json().unwrap();
121 assert_eq!(parsed["created"], true);
122 mock.assert_async().await;
123 }
124
125 #[tokio::test]
126 async fn test_send_with_custom_headers() {
127 let mut server = mockito::Server::new_async().await;
128 let mock = server
129 .mock("GET", "/auth")
130 .match_header("authorization", "Bearer xyz")
131 .match_header("accept", "application/json")
132 .with_status(200)
133 .with_body("{}")
134 .create_async()
135 .await;
136
137 let client = NativeHttpClient::new().unwrap();
138 let req = Request::get(&format!("{}/auth", server.url()))
139 .with_header("Authorization", "Bearer xyz")
140 .with_header("Accept", "application/json");
141 let resp = client.send(req).await.unwrap();
142
143 assert!(resp.is_success());
144 mock.assert_async().await;
145 }
146
147 #[tokio::test]
148 async fn test_send_non_success_status() {
149 let mut server = mockito::Server::new_async().await;
150 let mock = server
151 .mock("GET", "/missing")
152 .with_status(404)
153 .with_body("not found")
154 .create_async()
155 .await;
156
157 let client = NativeHttpClient::new().unwrap();
158 let req = Request::get(&format!("{}/missing", server.url()));
159 let resp = client.send(req).await.unwrap();
160
161 assert_eq!(resp.status_code, 404);
162 assert!(!resp.is_success());
163 assert_eq!(resp.body, "not found");
164 mock.assert_async().await;
165 }
166
167 #[tokio::test]
168 async fn test_send_server_error() {
169 let mut server = mockito::Server::new_async().await;
170 let mock = server
171 .mock("GET", "/err")
172 .with_status(500)
173 .with_body("internal error")
174 .create_async()
175 .await;
176
177 let client = NativeHttpClient::new().unwrap();
178 let req = Request::get(&format!("{}/err", server.url()));
179 let resp = client.send(req).await.unwrap();
180
181 assert_eq!(resp.status_code, 500);
182 assert!(!resp.is_success());
183 mock.assert_async().await;
184 }
185
186 #[tokio::test]
187 async fn test_response_headers_collected() {
188 let mut server = mockito::Server::new_async().await;
189 let mock = server
190 .mock("GET", "/headers")
191 .with_status(200)
192 .with_header("x-custom", "value123")
193 .with_body("")
194 .create_async()
195 .await;
196
197 let client = NativeHttpClient::new().unwrap();
198 let req = Request::get(&format!("{}/headers", server.url()));
199 let resp = client.send(req).await.unwrap();
200
201 assert_eq!(
202 resp.headers.get("x-custom").map(String::as_str),
203 Some("value123")
204 );
205 mock.assert_async().await;
206 }
207
208 #[tokio::test]
209 async fn test_empty_method_returns_error() {
210 let client = NativeHttpClient::new().unwrap();
211 let mut req = Request::get("https://example.com");
212 req.method = String::new();
213 let result = client.send(req).await;
214 assert!(result.is_err());
215 }
216}