1use std::{
18 collections::{HashMap, VecDeque},
19 sync::{Arc, Mutex},
20};
21
22use async_trait::async_trait;
23
24use crate::error::SteamError;
25
26#[derive(Debug, Clone)]
28pub struct HttpResponse {
29 pub status: u16,
31 pub body: Vec<u8>,
33 pub headers: HashMap<String, String>,
35}
36
37impl HttpResponse {
38 pub fn ok(body: Vec<u8>) -> Self {
40 Self { status: 200, body, headers: HashMap::new() }
41 }
42
43 pub fn error(status: u16, body: Vec<u8>) -> Self {
45 Self { status, body, headers: HashMap::new() }
46 }
47
48 pub fn is_success(&self) -> bool {
50 (200..300).contains(&self.status)
51 }
52
53 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, SteamError> {
55 serde_json::from_slice(&self.body).map_err(|e| SteamError::ProtocolError(format!("Failed to parse JSON: {}", e)))
56 }
57
58 pub fn text(&self) -> Result<String, SteamError> {
60 String::from_utf8(self.body.clone()).map_err(|e| SteamError::ProtocolError(format!("Invalid UTF-8: {}", e)))
61 }
62}
63
64#[async_trait]
69pub trait HttpClient: Send + Sync {
70 async fn get(&self, url: &str) -> Result<HttpResponse, SteamError>;
75
76 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError>;
82
83 async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError>;
90
91 async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError>;
97}
98
99#[derive(Clone)]
103pub struct ReqwestHttpClient {
104 client: reqwest::Client,
105}
106
107impl ReqwestHttpClient {
108 pub fn new() -> Self {
116 use std::time::Duration;
117
118 use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, ACCEPT_CHARSET, USER_AGENT};
119
120 let mut headers = HeaderMap::new();
121 headers.insert(USER_AGENT, HeaderValue::from_static("Valve/Steam HTTP Client 1.0"));
122 headers.insert(ACCEPT, HeaderValue::from_static("text/html,*/*;q=0.9"));
123 headers.insert(ACCEPT_CHARSET, HeaderValue::from_static("ISO-8859-1,utf-8,*;q=0.7"));
124
125 let client = reqwest::Client::builder().default_headers(headers).pool_max_idle_per_host(10).pool_idle_timeout(Duration::from_secs(60)).timeout(Duration::from_secs(5)).gzip(true).build().unwrap_or_else(|_| reqwest::Client::new());
126
127 Self { client }
128 }
129
130 pub fn with_client(client: reqwest::Client) -> Self {
132 Self { client }
133 }
134
135 async fn convert_response(resp: reqwest::Response) -> Result<HttpResponse, SteamError> {
137 let status = resp.status().as_u16();
138 let mut headers = HashMap::new();
139
140 for (key, value) in resp.headers() {
141 if let Ok(v) = value.to_str() {
142 headers.insert(key.as_str().to_lowercase(), v.to_string());
143 }
144 }
145
146 let body = resp.bytes().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?.to_vec();
147
148 Ok(HttpResponse { status, body, headers })
149 }
150}
151
152impl Default for ReqwestHttpClient {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[async_trait]
159impl HttpClient for ReqwestHttpClient {
160 async fn get(&self, url: &str) -> Result<HttpResponse, SteamError> {
161 let resp = self.client.get(url).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
162
163 Self::convert_response(resp).await
164 }
165
166 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
167 let resp = self.client.get(url).query(query).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
168
169 Self::convert_response(resp).await
170 }
171
172 async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError> {
173 let resp = self.client.post(url).header("Content-Type", content_type).body(body).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
174
175 Self::convert_response(resp).await
176 }
177
178 async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
179 let resp = self.client.post(url).form(form).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
180
181 Self::convert_response(resp).await
182 }
183}
184
185#[async_trait]
186impl steam_cm_provider::HttpClient for dyn HttpClient {
187 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<steam_cm_provider::HttpResponse, steam_cm_provider::CmError> {
188 let resp = self.get_with_query(url, query).await.map_err(|e| steam_cm_provider::CmError::Network(e.to_string()))?;
189
190 Ok(steam_cm_provider::HttpResponse { status: resp.status, body: resp.body })
191 }
192}
193
194#[async_trait]
195impl steam_cm_provider::HttpClient for ReqwestHttpClient {
196 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<steam_cm_provider::HttpResponse, steam_cm_provider::CmError> {
197 let resp = crate::utils::http::HttpClient::get_with_query(self, url, query).await.map_err(|e| steam_cm_provider::CmError::Network(e.to_string()))?;
198
199 Ok(steam_cm_provider::HttpResponse { status: resp.status, body: resp.body })
200 }
201}
202
203pub struct MockHttpClient {
221 responses: Arc<Mutex<VecDeque<Result<HttpResponse, SteamError>>>>,
223 requests: Arc<Mutex<Vec<MockRequest>>>,
225}
226
227#[derive(Debug, Clone)]
229pub struct MockRequest {
230 pub method: String,
232 pub url: String,
234 pub query: Vec<(String, String)>,
236 pub body: Option<Vec<u8>>,
238 pub content_type: Option<String>,
240}
241
242impl MockHttpClient {
243 pub fn new() -> Self {
245 Self { responses: Arc::new(Mutex::new(VecDeque::new())), requests: Arc::new(Mutex::new(Vec::new())) }
246 }
247
248 pub fn queue_response(&self, response: HttpResponse) {
250 self.responses.lock().expect("failed to get mock value").push_back(Ok(response));
251 }
252
253 pub fn queue_error(&self, error: SteamError) {
255 self.responses.lock().expect("failed to get mock value").push_back(Err(error));
256 }
257
258 pub fn queue_responses(&self, responses: Vec<HttpResponse>) {
260 let mut queue = self.responses.lock().expect("failed to get mock value");
261 for resp in responses {
262 queue.push_back(Ok(resp));
263 }
264 }
265
266 pub fn requests(&self) -> Vec<MockRequest> {
268 self.requests.lock().expect("failed to get mock value").clone()
269 }
270
271 pub fn last_request(&self) -> Option<MockRequest> {
273 self.requests.lock().expect("failed to get mock value").last().cloned()
274 }
275
276 pub fn clear_requests(&self) {
278 self.requests.lock().expect("failed to get mock value").clear();
279 }
280
281 pub fn request_count(&self) -> usize {
283 self.requests.lock().expect("failed to get mock value").len()
284 }
285
286 fn pop_response(&self) -> Result<HttpResponse, SteamError> {
288 self.responses.lock().expect("failed to get mock value").pop_front().unwrap_or_else(|| Err(SteamError::Other("MockHttpClient: No response queued".to_string())))
289 }
290
291 fn record_request(&self, request: MockRequest) {
293 self.requests.lock().expect("failed to get mock value").push(request);
294 }
295}
296
297impl Default for MockHttpClient {
298 fn default() -> Self {
299 Self::new()
300 }
301}
302
303impl Clone for MockHttpClient {
304 fn clone(&self) -> Self {
305 Self { responses: Arc::clone(&self.responses), requests: Arc::clone(&self.requests) }
306 }
307}
308
309#[async_trait]
310impl HttpClient for MockHttpClient {
311 async fn get(&self, url: &str) -> Result<HttpResponse, SteamError> {
312 self.record_request(MockRequest { method: "GET".to_string(), url: url.to_string(), query: Vec::new(), body: None, content_type: None });
313 self.pop_response()
314 }
315
316 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
317 self.record_request(MockRequest {
318 method: "GET".to_string(),
319 url: url.to_string(),
320 query: query.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
321 body: None,
322 content_type: None,
323 });
324 self.pop_response()
325 }
326
327 async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError> {
328 self.record_request(MockRequest {
329 method: "POST".to_string(),
330 url: url.to_string(),
331 query: Vec::new(),
332 body: Some(body),
333 content_type: Some(content_type.to_string()),
334 });
335 self.pop_response()
336 }
337
338 async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
339 let form_body: String = form.iter().map(|(k, v)| format!("{}={}", k, v)).collect::<Vec<_>>().join("&");
341
342 self.record_request(MockRequest {
343 method: "POST".to_string(),
344 url: url.to_string(),
345 query: form.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
346 body: Some(form_body.into_bytes()),
347 content_type: Some("application/x-www-form-urlencoded".to_string()),
348 });
349 self.pop_response()
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[tokio::test]
358 async fn test_mock_http_client_get() {
359 let mock = MockHttpClient::new();
360 mock.queue_response(HttpResponse::ok(b"test response".to_vec()));
361
362 let response = mock.get("https://example.com/test").await.expect("failed to get mock value");
363
364 assert_eq!(response.status, 200);
365 assert_eq!(response.body, b"test response");
366 assert!(response.is_success());
367
368 let requests = mock.requests();
369 assert_eq!(requests.len(), 1);
370 assert_eq!(requests[0].method, "GET");
371 assert_eq!(requests[0].url, "https://example.com/test");
372 }
373
374 #[tokio::test]
375 async fn test_mock_http_client_get_with_query() {
376 let mock = MockHttpClient::new();
377 mock.queue_response(HttpResponse::ok(b"{}".to_vec()));
378
379 let query = [("key", "value"), ("foo", "bar")];
380 let response = mock.get_with_query("https://api.example.com", &query).await.expect("failed to get mock value");
381
382 assert!(response.is_success());
383
384 let request = mock.last_request().expect("failed to get mock value");
385 assert_eq!(request.query.len(), 2);
386 assert_eq!(request.query[0], ("key".to_string(), "value".to_string()));
387 }
388
389 #[tokio::test]
390 async fn test_mock_http_client_post() {
391 let mock = MockHttpClient::new();
392 mock.queue_response(HttpResponse::ok(b"created".to_vec()));
393
394 let body = b"request body".to_vec();
395 let response = mock.post("https://api.example.com/create", body.clone(), "text/plain").await.expect("failed to get mock value");
396
397 assert!(response.is_success());
398
399 let request = mock.last_request().expect("failed to get mock value");
400 assert_eq!(request.method, "POST");
401 assert_eq!(request.body, Some(body));
402 assert_eq!(request.content_type, Some("text/plain".to_string()));
403 }
404
405 #[tokio::test]
406 async fn test_mock_http_client_error() {
407 let mock = MockHttpClient::new();
408 mock.queue_error(SteamError::Timeout);
409
410 let result = mock.get("https://example.com").await;
411 assert!(result.is_err());
412 }
413
414 #[tokio::test]
415 async fn test_mock_http_client_no_response_queued() {
416 let mock = MockHttpClient::new();
417
418 let result = mock.get("https://example.com").await;
419 assert!(result.is_err());
420 }
421
422 #[tokio::test]
423 async fn test_http_response_json() {
424 let response = HttpResponse::ok(br#"{"key": "value"}"#.to_vec());
425 let json: serde_json::Value = response.json().expect("failed to get mock value");
426
427 assert_eq!(json["key"], "value");
428 }
429
430 #[tokio::test]
431 async fn test_http_response_is_success() {
432 assert!(HttpResponse::ok(vec![]).is_success());
433 assert!(HttpResponse { status: 201, body: vec![], headers: HashMap::new() }.is_success());
434 assert!(!HttpResponse::error(404, vec![]).is_success());
435 assert!(!HttpResponse::error(500, vec![]).is_success());
436 }
437}