//! API Test Client //! //! Validates API responses alongside UX interactions use reqwest; use serde_json::Value; use std::collections::HashMap; /// API test client for validating backend responses #[derive(Clone)] pub struct APITestClient { client: reqwest::Client, base_url: String, session_cookies: Option, } impl APITestClient { /// Create a new API test client pub fn new(test_port: u16) -> Self { Self { client: reqwest::Client::builder() .cookie_store(true) .build() .expect("Failed to create HTTP client"), base_url: format!("http://localhost:{}", test_port), session_cookies: None, } } /// Make a GET request to an API endpoint pub async fn get(&self, path: &str) -> Result> { let url = format!("{}{}", self.base_url, path); log::info!("API GET: {}", url); let mut request = self.client.get(&url); if let Some(cookies) = &self.session_cookies { request = request.header("Cookie", cookies); } let response = request.send().await?; let status = response.status(); let headers = response.headers().clone(); let body: Value = response.json().await?; Ok(ApiResponse { status: status.as_u16(), headers: headers.iter().map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())).collect(), body, }) } /// Make a POST request to an API endpoint pub async fn post(&self, path: &str, data: Value) -> Result> { let url = format!("{}{}", self.base_url, path); log::info!("API POST: {}", url); let mut request = self.client.post(&url).json(&data); if let Some(cookies) = &self.session_cookies { request = request.header("Cookie", cookies); } let response = request.send().await?; let status = response.status(); let headers = response.headers().clone(); let body: Value = response.json().await?; Ok(ApiResponse { status: status.as_u16(), headers: headers.iter().map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())).collect(), body, }) } /// Make a PUT request to an API endpoint pub async fn put(&self, path: &str, data: Value) -> Result> { let url = format!("{}{}", self.base_url, path); log::info!("API PUT: {}", url); let mut request = self.client.put(&url).json(&data); if let Some(cookies) = &self.session_cookies { request = request.header("Cookie", cookies); } let response = request.send().await?; let status = response.status(); let headers = response.headers().clone(); let body: Value = response.json().await?; Ok(ApiResponse { status: status.as_u16(), headers: headers.iter().map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())).collect(), body, }) } /// Make a DELETE request to an API endpoint pub async fn delete(&self, path: &str) -> Result> { let url = format!("{}{}", self.base_url, path); log::info!("API DELETE: {}", url); let mut request = self.client.delete(&url); if let Some(cookies) = &self.session_cookies { request = request.header("Cookie", cookies); } let response = request.send().await?; let status = response.status(); let headers = response.headers().clone(); let body: Value = response.json().await?; Ok(ApiResponse { status: status.as_u16(), headers: headers.iter().map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())).collect(), body, }) } /// Validate authentication status pub async fn validate_auth_status(&self) -> Result> { let response = self.get("/api/auth/status").await?; // Validate ResponseBuilder envelope format self.assert_response_envelope(&response.body)?; let authenticated = response.body["data"]["authenticated"].as_bool().unwrap_or(false); let user_email = response.body["data"]["user"]["email"].as_str().map(|s| s.to_string()); Ok(AuthStatus { authenticated, user_email, }) } /// Validate cart state pub async fn validate_cart_state(&self) -> Result> { let response = self.get("/api/cart").await?; self.assert_response_envelope(&response.body)?; let items = response.body["data"]["items"].as_array() .map(|arr| arr.len()) .unwrap_or(0); let total = response.body["data"]["total"]["amount"].as_f64().unwrap_or(0.0); Ok(CartState { item_count: items, total_amount: total, }) } /// Validate wallet balance pub async fn get_wallet_balance(&self) -> Result> { let response = self.get("/api/wallet/balance").await?; self.assert_response_envelope(&response.body)?; let balance = response.body["data"]["balance"]["amount"].as_f64().unwrap_or(0.0); let currency = response.body["data"]["balance"]["currency"].as_str().unwrap_or("TFC").to_string(); Ok(WalletBalance { balance, currency, }) } /// Get user dashboard data pub async fn get_dashboard_data(&self, dashboard_type: &str) -> Result> { let path = format!("/api/dashboard/{}-data", dashboard_type); let response = self.get(&path).await?; self.assert_response_envelope(&response.body)?; Ok(response.body["data"].clone()) } /// Validate products API pub async fn get_products(&self, category: Option<&str>) -> Result, Box> { let path = if let Some(cat) = category { format!("/api/products?category={}", cat) } else { "/api/products".to_string() }; let response = self.get(&path).await?; self.assert_response_envelope(&response.body)?; Ok(response.body["data"]["products"].as_array().unwrap_or(&vec![]).clone()) } /// Assert ResponseBuilder envelope format fn assert_response_envelope(&self, response: &Value) -> Result<(), Box> { if !response.get("success").is_some() { return Err("Missing 'success' field in response envelope".into()); } let success = response["success"].as_bool().unwrap_or(false); if success { if !response.get("data").is_some() { return Err("Missing 'data' field in successful response".into()); } } else { if !response.get("error").is_some() { return Err("Missing 'error' field in failed response".into()); } } Ok(()) } /// Set session cookies for authenticated requests pub fn set_session_cookies(&mut self, cookies: String) { self.session_cookies = Some(cookies); } } /// API response structure #[derive(Debug)] pub struct ApiResponse { pub status: u16, pub headers: HashMap, pub body: Value, } /// Authentication status #[derive(Debug)] pub struct AuthStatus { pub authenticated: bool, pub user_email: Option, } /// Cart state #[derive(Debug)] pub struct CartState { pub item_count: usize, pub total_amount: f64, } /// Wallet balance #[derive(Debug)] pub struct WalletBalance { pub balance: f64, pub currency: String, }