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(&format!("{}/api", server.url()), r#"{"key":"val"}"#);
113 let resp = client.send(req).await.unwrap();
114
115 assert_eq!(resp.status_code, 201);
116 assert!(resp.is_success());
117 let parsed: serde_json::Value = resp.json().unwrap();
118 assert_eq!(parsed["created"], true);
119 mock.assert_async().await;
120 }
121
122 #[tokio::test]
123 async fn test_send_with_custom_headers() {
124 let mut server = mockito::Server::new_async().await;
125 let mock = server
126 .mock("GET", "/auth")
127 .match_header("authorization", "Bearer xyz")
128 .match_header("accept", "application/json")
129 .with_status(200)
130 .with_body("{}")
131 .create_async()
132 .await;
133
134 let client = NativeHttpClient::new().unwrap();
135 let req = Request::get(&format!("{}/auth", server.url()))
136 .with_header("Authorization", "Bearer xyz")
137 .with_header("Accept", "application/json");
138 let resp = client.send(req).await.unwrap();
139
140 assert!(resp.is_success());
141 mock.assert_async().await;
142 }
143
144 #[tokio::test]
145 async fn test_send_non_success_status() {
146 let mut server = mockito::Server::new_async().await;
147 let mock = server
148 .mock("GET", "/missing")
149 .with_status(404)
150 .with_body("not found")
151 .create_async()
152 .await;
153
154 let client = NativeHttpClient::new().unwrap();
155 let req = Request::get(&format!("{}/missing", server.url()));
156 let resp = client.send(req).await.unwrap();
157
158 assert_eq!(resp.status_code, 404);
159 assert!(!resp.is_success());
160 assert_eq!(resp.body, "not found");
161 mock.assert_async().await;
162 }
163
164 #[tokio::test]
165 async fn test_send_server_error() {
166 let mut server = mockito::Server::new_async().await;
167 let mock = server
168 .mock("GET", "/err")
169 .with_status(500)
170 .with_body("internal error")
171 .create_async()
172 .await;
173
174 let client = NativeHttpClient::new().unwrap();
175 let req = Request::get(&format!("{}/err", server.url()));
176 let resp = client.send(req).await.unwrap();
177
178 assert_eq!(resp.status_code, 500);
179 assert!(!resp.is_success());
180 mock.assert_async().await;
181 }
182
183 #[tokio::test]
184 async fn test_response_headers_collected() {
185 let mut server = mockito::Server::new_async().await;
186 let mock = server
187 .mock("GET", "/headers")
188 .with_status(200)
189 .with_header("x-custom", "value123")
190 .with_body("")
191 .create_async()
192 .await;
193
194 let client = NativeHttpClient::new().unwrap();
195 let req = Request::get(&format!("{}/headers", server.url()));
196 let resp = client.send(req).await.unwrap();
197
198 assert_eq!(
199 resp.headers.get("x-custom").map(String::as_str),
200 Some("value123")
201 );
202 mock.assert_async().await;
203 }
204
205 #[tokio::test]
206 async fn test_empty_method_returns_error() {
207 let client = NativeHttpClient::new().unwrap();
208 let mut req = Request::get("https://example.com");
209 req.method = String::new();
210 let result = client.send(req).await;
211 assert!(result.is_err());
212 }
213}