use serde::{Deserialize, Serialize}; use uuid::Uuid; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestContext { pub tenant_id: Uuid, pub user_id: Uuid, pub access_token: String, pub refresh_token: String, pub base_url: String, pub created_resources: HashMap>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreatedResource { pub id: Uuid, pub name: String, pub resource_type: String, } impl TestContext { pub fn new() -> Self { Self { tenant_id: Uuid::nil(), user_id: Uuid::nil(), access_token: String::new(), refresh_token: String::new(), base_url: std::env::var("ERP_API_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()), created_resources: HashMap::new(), } } pub fn store_resource(&mut self, resource_type: &str, id: Uuid, name: &str) { self.created_resources .entry(resource_type.to_string()) .or_insert_with(Vec::new) .push(CreatedResource { id, name: name.to_string(), resource_type: resource_type.to_string(), }); } pub fn get_resource(&self, resource_type: &str, name: &str) -> Option { self.created_resources .get(resource_type) .and_then(|resources| { resources .iter() .find(|r| r.name == name) .map(|r| r.id) }) } pub fn auth_header(&self) -> String { format!("Bearer {}", self.access_token) } } impl Default for TestContext { fn default() -> Self { Self::new() } } pub struct HttpClient { base_url: String, } impl HttpClient { pub fn new(base_url: &str) -> Self { Self { base_url: base_url.to_string(), } } pub async fn get(&self, path: &str, token: Option<&str>) -> TestResponse { let client = reqwest::Client::new(); let mut req = client.get(format!("{}{}", self.base_url, path)); if let Some(t) = token { req = req.header("Authorization", format!("Bearer {}", t)); } req.send().await.into() } pub async fn post(&self, path: &str, body: &T, token: Option<&str>) -> TestResponse { let client = reqwest::Client::new(); let mut req = client.post(format!("{}{}", self.base_url, path)); req = req.header("Content-Type", "application/json"); if let Some(t) = token { req = req.header("Authorization", format!("Bearer {}", t)); } req.json(body).send().await.into() } pub async fn put(&self, path: &str, body: &T, token: Option<&str>) -> TestResponse { let client = reqwest::Client::new(); let mut req = client.put(format!("{}{}", self.base_url, path)); req = req.header("Content-Type", "application/json"); if let Some(t) = token { req = req.header("Authorization", format!("Bearer {}", t)); } req.json(body).send().await.into() } pub async fn delete(&self, path: &str, token: Option<&str>) -> TestResponse { let client = reqwest::Client::new(); let mut req = client.delete(format!("{}{}", self.base_url, path)); if let Some(t) = token { req = req.header("Authorization", format!("Bearer {}", t)); } req.send().await.into() } } #[derive(Debug)] pub struct TestResponse { pub status: u16, pub body: serde_json::Value, pub elapsed_ms: u64, } impl From for TestResponse { fn from(resp: reqwest::Response) -> Self { let status = resp.status().as_u16(); let body = resp.json().unwrap_or(serde_json::json!({})); Self { status, body, elapsed_ms: 0, } } } pub async fn login(client: &HttpClient, username: &str, password: &str) -> Result<(String, String), String> { #[derive(Serialize)] struct LoginReq { username: String, password: String } #[derive(Deserialize)] struct LoginResp { data: LoginData } #[derive(Deserialize)] struct LoginData { access_token: String, refresh_token: String, } let resp = client.post("/api/v1/auth/login", &LoginReq { username: username.to_string(), password: password.to_string(), }, None).await; if resp.status == 200 { let data: LoginResp = serde_json::from_value(resp.body) .map_err(|e| format!("parse error: {}", e))?; Ok((data.data.access_token, data.data.refresh_token)) } else { Err(format!("login failed: {} - {:?}", resp.status, resp.body)) } } pub async fn refresh_token(client: &HttpClient, refresh_token: &str) -> Result<(String, String), String> { #[derive(Serialize)] struct RefreshReq { refresh_token: String } #[derive(Deserialize)] struct RefreshResp { data: RefreshData } #[derive(Deserialize)] struct RefreshData { access_token: String, refresh_token: String, } let resp = client.post("/api/v1/auth/refresh", &RefreshReq { refresh_token: refresh_token.to_string() }, None).await; if resp.status == 200 { let data: RefreshResp = serde_json::from_value(resp.body) .map_err(|e| format!("parse error: {}", e))?; Ok((data.data.access_token, data.data.refresh_token)) } else { Err(format!("refresh failed: {} - {:?}", resp.status, resp.body)) } } pub fn validate_uuid(id: &str) -> bool { Uuid::parse_str(id).is_ok() } pub fn validate_email(email: &str) -> bool { email.contains('@') && email.contains('.') } pub fn validate_pagination(body: &serde_json::Value) -> Option<(u64, u64, u64)> { let data = body.get("data")?; let total = data.get("total")?.as_u64()?; let page = data.get("page")?.as_u64()?; let page_size = data.get("page_size")?.as_u64()?; Some((total, page, page_size)) }