249 lines
8.1 KiB
Rust
249 lines
8.1 KiB
Rust
//! 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<String>,
|
|
}
|
|
|
|
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<ApiResponse, Box<dyn std::error::Error>> {
|
|
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<ApiResponse, Box<dyn std::error::Error>> {
|
|
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<ApiResponse, Box<dyn std::error::Error>> {
|
|
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<ApiResponse, Box<dyn std::error::Error>> {
|
|
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<AuthStatus, Box<dyn std::error::Error>> {
|
|
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<CartState, Box<dyn std::error::Error>> {
|
|
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<WalletBalance, Box<dyn std::error::Error>> {
|
|
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<Value, Box<dyn std::error::Error>> {
|
|
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<Vec<Value>, Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<String, String>,
|
|
pub body: Value,
|
|
}
|
|
|
|
/// Authentication status
|
|
#[derive(Debug)]
|
|
pub struct AuthStatus {
|
|
pub authenticated: bool,
|
|
pub user_email: Option<String>,
|
|
}
|
|
|
|
/// 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,
|
|
} |