use std::{
collections::{HashMap, VecDeque},
sync::{Arc, Mutex},
};
use async_trait::async_trait;
use crate::error::SteamError;
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub body: Vec<u8>,
pub headers: HashMap<String, String>,
}
impl HttpResponse {
pub fn ok(body: Vec<u8>) -> Self {
Self { status: 200, body, headers: HashMap::new() }
}
pub fn error(status: u16, body: Vec<u8>) -> Self {
Self { status, body, headers: HashMap::new() }
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, SteamError> {
serde_json::from_slice(&self.body).map_err(|e| SteamError::ProtocolError(format!("Failed to parse JSON: {}", e)))
}
pub fn text(&self) -> Result<String, SteamError> {
String::from_utf8(self.body.clone()).map_err(|e| SteamError::ProtocolError(format!("Invalid UTF-8: {}", e)))
}
}
#[async_trait]
pub trait HttpClient: Send + Sync {
async fn get(&self, url: &str) -> Result<HttpResponse, SteamError>;
async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError>;
async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError>;
async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError>;
}
#[derive(Clone)]
pub struct ReqwestHttpClient {
client: reqwest::Client,
}
impl ReqwestHttpClient {
pub fn new() -> Self {
use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, ACCEPT_CHARSET, USER_AGENT};
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("Valve/Steam HTTP Client 1.0"));
headers.insert(ACCEPT, HeaderValue::from_static("text/html,*/*;q=0.9"));
headers.insert(ACCEPT_CHARSET, HeaderValue::from_static("ISO-8859-1,utf-8,*;q=0.7"));
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());
Self { client }
}
pub fn with_client(client: reqwest::Client) -> Self {
Self { client }
}
async fn convert_response(resp: reqwest::Response) -> Result<HttpResponse, SteamError> {
let status = resp.status().as_u16();
let mut headers = HashMap::new();
for (key, value) in resp.headers() {
if let Ok(v) = value.to_str() {
headers.insert(key.as_str().to_lowercase(), v.to_string());
}
}
let body = resp.bytes().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?.to_vec();
Ok(HttpResponse { status, body, headers })
}
}
impl Default for ReqwestHttpClient {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl HttpClient for ReqwestHttpClient {
async fn get(&self, url: &str) -> Result<HttpResponse, SteamError> {
let resp = self.client.get(url).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
Self::convert_response(resp).await
}
async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
let resp = self.client.get(url).query(query).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
Self::convert_response(resp).await
}
async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError> {
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)))?;
Self::convert_response(resp).await
}
async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
let resp = self.client.post(url).form(form).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
Self::convert_response(resp).await
}
}
#[async_trait]
impl steam_cm_provider::HttpClient for dyn HttpClient {
async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<steam_cm_provider::HttpResponse, steam_cm_provider::CmError> {
let resp = self.get_with_query(url, query).await.map_err(|e| steam_cm_provider::CmError::Network(e.to_string()))?;
Ok(steam_cm_provider::HttpResponse { status: resp.status, body: resp.body })
}
}
#[async_trait]
impl steam_cm_provider::HttpClient for ReqwestHttpClient {
async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<steam_cm_provider::HttpResponse, steam_cm_provider::CmError> {
let resp = crate::utils::http::HttpClient::get_with_query(self, url, query).await.map_err(|e| steam_cm_provider::CmError::Network(e.to_string()))?;
Ok(steam_cm_provider::HttpResponse { status: resp.status, body: resp.body })
}
}
pub struct MockHttpClient {
responses: Arc<Mutex<VecDeque<Result<HttpResponse, SteamError>>>>,
requests: Arc<Mutex<Vec<MockRequest>>>,
}
#[derive(Debug, Clone)]
pub struct MockRequest {
pub method: String,
pub url: String,
pub query: Vec<(String, String)>,
pub body: Option<Vec<u8>>,
pub content_type: Option<String>,
}
impl MockHttpClient {
pub fn new() -> Self {
Self { responses: Arc::new(Mutex::new(VecDeque::new())), requests: Arc::new(Mutex::new(Vec::new())) }
}
pub fn queue_response(&self, response: HttpResponse) {
self.responses.lock().expect("failed to get mock value").push_back(Ok(response));
}
pub fn queue_error(&self, error: SteamError) {
self.responses.lock().expect("failed to get mock value").push_back(Err(error));
}
pub fn queue_responses(&self, responses: Vec<HttpResponse>) {
let mut queue = self.responses.lock().expect("failed to get mock value");
for resp in responses {
queue.push_back(Ok(resp));
}
}
pub fn requests(&self) -> Vec<MockRequest> {
self.requests.lock().expect("failed to get mock value").clone()
}
pub fn last_request(&self) -> Option<MockRequest> {
self.requests.lock().expect("failed to get mock value").last().cloned()
}
pub fn clear_requests(&self) {
self.requests.lock().expect("failed to get mock value").clear();
}
pub fn request_count(&self) -> usize {
self.requests.lock().expect("failed to get mock value").len()
}
fn pop_response(&self) -> Result<HttpResponse, SteamError> {
self.responses.lock().expect("failed to get mock value").pop_front().unwrap_or_else(|| Err(SteamError::Other("MockHttpClient: No response queued".to_string())))
}
fn record_request(&self, request: MockRequest) {
self.requests.lock().expect("failed to get mock value").push(request);
}
}
impl Default for MockHttpClient {
fn default() -> Self {
Self::new()
}
}
impl Clone for MockHttpClient {
fn clone(&self) -> Self {
Self { responses: Arc::clone(&self.responses), requests: Arc::clone(&self.requests) }
}
}
#[async_trait]
impl HttpClient for MockHttpClient {
async fn get(&self, url: &str) -> Result<HttpResponse, SteamError> {
self.record_request(MockRequest { method: "GET".to_string(), url: url.to_string(), query: Vec::new(), body: None, content_type: None });
self.pop_response()
}
async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
self.record_request(MockRequest {
method: "GET".to_string(),
url: url.to_string(),
query: query.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
body: None,
content_type: None,
});
self.pop_response()
}
async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError> {
self.record_request(MockRequest {
method: "POST".to_string(),
url: url.to_string(),
query: Vec::new(),
body: Some(body),
content_type: Some(content_type.to_string()),
});
self.pop_response()
}
async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
let form_body: String = form.iter().map(|(k, v)| format!("{}={}", k, v)).collect::<Vec<_>>().join("&");
self.record_request(MockRequest {
method: "POST".to_string(),
url: url.to_string(),
query: form.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
body: Some(form_body.into_bytes()),
content_type: Some("application/x-www-form-urlencoded".to_string()),
});
self.pop_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_http_client_get() {
let mock = MockHttpClient::new();
mock.queue_response(HttpResponse::ok(b"test response".to_vec()));
let response = mock.get("https://example.com/test").await.expect("failed to get mock value");
assert_eq!(response.status, 200);
assert_eq!(response.body, b"test response");
assert!(response.is_success());
let requests = mock.requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].method, "GET");
assert_eq!(requests[0].url, "https://example.com/test");
}
#[tokio::test]
async fn test_mock_http_client_get_with_query() {
let mock = MockHttpClient::new();
mock.queue_response(HttpResponse::ok(b"{}".to_vec()));
let query = [("key", "value"), ("foo", "bar")];
let response = mock.get_with_query("https://api.example.com", &query).await.expect("failed to get mock value");
assert!(response.is_success());
let request = mock.last_request().expect("failed to get mock value");
assert_eq!(request.query.len(), 2);
assert_eq!(request.query[0], ("key".to_string(), "value".to_string()));
}
#[tokio::test]
async fn test_mock_http_client_post() {
let mock = MockHttpClient::new();
mock.queue_response(HttpResponse::ok(b"created".to_vec()));
let body = b"request body".to_vec();
let response = mock.post("https://api.example.com/create", body.clone(), "text/plain").await.expect("failed to get mock value");
assert!(response.is_success());
let request = mock.last_request().expect("failed to get mock value");
assert_eq!(request.method, "POST");
assert_eq!(request.body, Some(body));
assert_eq!(request.content_type, Some("text/plain".to_string()));
}
#[tokio::test]
async fn test_mock_http_client_error() {
let mock = MockHttpClient::new();
mock.queue_error(SteamError::Timeout);
let result = mock.get("https://example.com").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_mock_http_client_no_response_queued() {
let mock = MockHttpClient::new();
let result = mock.get("https://example.com").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_http_response_json() {
let response = HttpResponse::ok(br#"{"key": "value"}"#.to_vec());
let json: serde_json::Value = response.json().expect("failed to get mock value");
assert_eq!(json["key"], "value");
}
#[tokio::test]
async fn test_http_response_is_success() {
assert!(HttpResponse::ok(vec![]).is_success());
assert!(HttpResponse { status: 201, body: vec![], headers: HashMap::new() }.is_success());
assert!(!HttpResponse::error(404, vec![]).is_success());
assert!(!HttpResponse::error(500, vec![]).is_success());
}
}