init projectmycelium
This commit is contained in:
249
tests/tests_archive/ux_suite/environment/api_client.rs
Normal file
249
tests/tests_archive/ux_suite/environment/api_client.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
//! 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,
|
||||
}
|
230
tests/tests_archive/ux_suite/environment/browser_manager.rs
Normal file
230
tests/tests_archive/ux_suite/environment/browser_manager.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
//! Browser Automation Manager
|
||||
//!
|
||||
//! Handles browser lifecycle, navigation, and interaction for UX testing
|
||||
|
||||
use thirtyfour::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Supported browser types for testing
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BrowserType {
|
||||
Chrome,
|
||||
Firefox,
|
||||
Safari,
|
||||
Edge,
|
||||
}
|
||||
|
||||
/// Browser manager for UX testing
|
||||
pub struct BrowserManager {
|
||||
driver: WebDriver,
|
||||
base_url: String,
|
||||
screenshot_dir: PathBuf,
|
||||
config: super::UXTestConfig,
|
||||
}
|
||||
|
||||
impl Clone for BrowserManager {
|
||||
fn clone(&self) -> Self {
|
||||
// Note: WebDriver cannot be cloned, so this creates a reference to the same driver
|
||||
// In practice, we should avoid cloning BrowserManager and use references instead
|
||||
panic!("BrowserManager cannot be cloned due to WebDriver limitations. Use references instead.");
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserManager {
|
||||
/// Create a new browser manager
|
||||
pub async fn new(config: &super::UXTestConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let capabilities = match config.browser_type {
|
||||
BrowserType::Chrome => {
|
||||
let mut caps = DesiredCapabilities::chrome();
|
||||
if config.headless {
|
||||
caps.add_chrome_arg("--headless")?;
|
||||
}
|
||||
caps.add_chrome_arg("--no-sandbox")?;
|
||||
caps.add_chrome_arg("--disable-dev-shm-usage")?;
|
||||
caps.add_chrome_arg("--disable-gpu")?;
|
||||
caps.add_chrome_arg("--window-size=1920,1080")?;
|
||||
caps
|
||||
}
|
||||
BrowserType::Firefox => {
|
||||
let mut caps = DesiredCapabilities::firefox();
|
||||
if config.headless {
|
||||
caps.add_firefox_arg("--headless")?;
|
||||
}
|
||||
caps
|
||||
}
|
||||
_ => {
|
||||
return Err("Browser type not yet implemented".into());
|
||||
}
|
||||
};
|
||||
|
||||
// Try to connect to existing WebDriver or start one
|
||||
let driver = match WebDriver::new("http://localhost:4444", capabilities.clone()).await {
|
||||
Ok(driver) => driver,
|
||||
Err(_) => {
|
||||
// If selenium server is not running, try local driver
|
||||
WebDriver::new("http://localhost:9515", capabilities).await?
|
||||
}
|
||||
};
|
||||
|
||||
// Configure browser
|
||||
driver.set_window_size(1920, 1080).await?;
|
||||
driver.implicitly_wait(Duration::from_secs(10)).await?;
|
||||
|
||||
Ok(Self {
|
||||
driver,
|
||||
base_url: format!("http://localhost:{}", config.test_port),
|
||||
screenshot_dir: config.screenshot_dir.clone(),
|
||||
config: config.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Navigate to a path on the test server
|
||||
pub async fn navigate_to(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let url = if path.starts_with("http") {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("{}{}", self.base_url, path)
|
||||
};
|
||||
|
||||
log::info!("Navigating to: {}", url);
|
||||
|
||||
timeout(
|
||||
Duration::from_secs(self.config.timeout_seconds),
|
||||
self.driver.goto(&url)
|
||||
).await??;
|
||||
|
||||
// Wait for page to load
|
||||
self.wait_for_page_load().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for page to be fully loaded
|
||||
pub async fn wait_for_page_load(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Wait for document ready state
|
||||
self.driver.execute("return document.readyState", vec![]).await?;
|
||||
|
||||
// Additional wait for any dynamic content
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Take a screenshot with the given name
|
||||
pub async fn take_screenshot(&self, name: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let screenshot = self.driver.screenshot_as_png().await?;
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
||||
let filename = format!("{}_{}.png", name, timestamp);
|
||||
let path = self.screenshot_dir.join(filename);
|
||||
|
||||
std::fs::create_dir_all(&self.screenshot_dir)?;
|
||||
std::fs::write(&path, screenshot)?;
|
||||
|
||||
log::info!("Screenshot saved: {:?}", path);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Find element by CSS selector
|
||||
pub async fn find_element(&self, selector: &str) -> Result<WebElement, Box<dyn std::error::Error>> {
|
||||
Ok(self.driver.find(By::Css(selector)).await?)
|
||||
}
|
||||
|
||||
/// Find elements by CSS selector
|
||||
pub async fn find_elements(&self, selector: &str) -> Result<Vec<WebElement>, Box<dyn std::error::Error>> {
|
||||
Ok(self.driver.find_all(By::Css(selector)).await?)
|
||||
}
|
||||
|
||||
/// Click element by CSS selector
|
||||
pub async fn click(&self, selector: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let element = self.find_element(selector).await?;
|
||||
element.scroll_into_view().await?;
|
||||
element.click().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Type text into element
|
||||
pub async fn type_text(&self, selector: &str, text: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let element = self.find_element(selector).await?;
|
||||
element.clear().await?;
|
||||
element.send_keys(text).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get text from element
|
||||
pub async fn get_text(&self, selector: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let element = self.find_element(selector).await?;
|
||||
Ok(element.text().await?)
|
||||
}
|
||||
|
||||
/// Check if element exists
|
||||
pub async fn element_exists(&self, selector: &str) -> bool {
|
||||
self.driver.find(By::Css(selector)).await.is_ok()
|
||||
}
|
||||
|
||||
/// Wait for element to be visible
|
||||
pub async fn wait_for_element(&self, selector: &str) -> Result<WebElement, Box<dyn std::error::Error>> {
|
||||
let timeout_duration = Duration::from_secs(self.config.timeout_seconds);
|
||||
|
||||
timeout(timeout_duration, async {
|
||||
loop {
|
||||
if let Ok(element) = self.driver.find(By::Css(selector)).await {
|
||||
if element.is_displayed().await.unwrap_or(false) {
|
||||
return Ok(element);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}).await?
|
||||
}
|
||||
|
||||
/// Get current page title
|
||||
pub async fn get_title(&self) -> Result<String, Box<dyn std::error::Error>> {
|
||||
Ok(self.driver.title().await?)
|
||||
}
|
||||
|
||||
/// Get current URL
|
||||
pub async fn get_current_url(&self) -> Result<String, Box<dyn std::error::Error>> {
|
||||
Ok(self.driver.current_url().await?)
|
||||
}
|
||||
|
||||
/// Execute JavaScript
|
||||
pub async fn execute_script(&self, script: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
Ok(self.driver.execute(script, vec![]).await?)
|
||||
}
|
||||
|
||||
/// Quit the browser
|
||||
pub async fn quit(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.driver.quit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get page source for debugging
|
||||
pub async fn get_page_source(&self) -> Result<String, Box<dyn std::error::Error>> {
|
||||
Ok(self.driver.source().await?)
|
||||
}
|
||||
|
||||
/// Scroll to element
|
||||
pub async fn scroll_to_element(&self, selector: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let element = self.find_element(selector).await?;
|
||||
element.scroll_into_view().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for text to appear in element
|
||||
pub async fn wait_for_text(&self, selector: &str, expected_text: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let timeout_duration = Duration::from_secs(self.config.timeout_seconds);
|
||||
|
||||
timeout(timeout_duration, async {
|
||||
loop {
|
||||
if let Ok(element) = self.driver.find(By::Css(selector)).await {
|
||||
if let Ok(text) = element.text().await {
|
||||
if text.contains(expected_text) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}).await?
|
||||
}
|
||||
}
|
115
tests/tests_archive/ux_suite/environment/mod.rs
Normal file
115
tests/tests_archive/ux_suite/environment/mod.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
//! Test Environment Management
|
||||
//!
|
||||
//! Handles isolated test environment setup including:
|
||||
//! - Test server instance
|
||||
//! - Browser automation
|
||||
//! - Test data management
|
||||
//! - API client for validation
|
||||
|
||||
pub mod browser_manager;
|
||||
pub mod test_server;
|
||||
pub mod test_data_manager;
|
||||
pub mod api_client;
|
||||
|
||||
pub use browser_manager::*;
|
||||
pub use test_server::*;
|
||||
pub use test_data_manager::*;
|
||||
pub use api_client::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tokio::time::Duration;
|
||||
|
||||
/// Configuration for UX test environment
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UXTestConfig {
|
||||
pub test_port: u16,
|
||||
pub headless: bool,
|
||||
pub timeout_seconds: u64,
|
||||
pub screenshot_on_failure: bool,
|
||||
pub browser_type: BrowserType,
|
||||
pub test_data_dir: PathBuf,
|
||||
pub screenshot_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for UXTestConfig {
|
||||
fn default() -> Self {
|
||||
let test_mode = std::env::var("UX_TEST_MODE").unwrap_or_else(|_| "dev".to_string());
|
||||
|
||||
match test_mode.as_str() {
|
||||
"ci" => Self {
|
||||
test_port: 8081,
|
||||
headless: true,
|
||||
timeout_seconds: 30,
|
||||
screenshot_on_failure: true,
|
||||
browser_type: BrowserType::Chrome,
|
||||
test_data_dir: PathBuf::from("user_data_test"),
|
||||
screenshot_dir: PathBuf::from("tests/ux_suite/reports/screenshots"),
|
||||
},
|
||||
"dev" => Self {
|
||||
test_port: 8081,
|
||||
headless: false,
|
||||
timeout_seconds: 60,
|
||||
screenshot_on_failure: true,
|
||||
browser_type: BrowserType::Chrome,
|
||||
test_data_dir: PathBuf::from("user_data_test"),
|
||||
screenshot_dir: PathBuf::from("tests/ux_suite/reports/screenshots"),
|
||||
},
|
||||
_ => Self {
|
||||
test_port: 8081,
|
||||
headless: false,
|
||||
timeout_seconds: 60,
|
||||
screenshot_on_failure: true,
|
||||
browser_type: BrowserType::Chrome,
|
||||
test_data_dir: PathBuf::from("user_data_test"),
|
||||
screenshot_dir: PathBuf::from("tests/ux_suite/reports/screenshots"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete UX test environment
|
||||
pub struct UXTestEnvironment {
|
||||
pub config: UXTestConfig,
|
||||
pub browser: BrowserManager,
|
||||
pub server: TestServer,
|
||||
pub data_manager: TestDataManager,
|
||||
pub api_client: APITestClient,
|
||||
}
|
||||
|
||||
impl UXTestEnvironment {
|
||||
/// Initialize a new test environment
|
||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let config = UXTestConfig::default();
|
||||
|
||||
// Create directories
|
||||
std::fs::create_dir_all(&config.test_data_dir)?;
|
||||
std::fs::create_dir_all(&config.screenshot_dir)?;
|
||||
|
||||
// Initialize components
|
||||
let data_manager = TestDataManager::new(&config.test_data_dir)?;
|
||||
let server = TestServer::start(config.test_port).await?;
|
||||
let browser = BrowserManager::new(&config).await?;
|
||||
let api_client = APITestClient::new(config.test_port);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
browser,
|
||||
server,
|
||||
data_manager,
|
||||
api_client,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a UX test helper for this environment
|
||||
pub fn ux_helper(&self) -> UXTestHelper {
|
||||
UXTestHelper::new(self)
|
||||
}
|
||||
|
||||
/// Clean up test environment
|
||||
pub async fn cleanup(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.browser.quit().await?;
|
||||
self.server.stop().await?;
|
||||
self.data_manager.cleanup()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
314
tests/tests_archive/ux_suite/environment/test_data_manager.rs
Normal file
314
tests/tests_archive/ux_suite/environment/test_data_manager.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
//! Test Data Management
|
||||
//!
|
||||
//! Manages test fixtures, user personas, and data isolation for UX testing
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Test user persona for different user types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestPersona {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub name: String,
|
||||
pub role: UserRole,
|
||||
pub profile: UserProfile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum UserRole {
|
||||
Consumer,
|
||||
Farmer,
|
||||
AppProvider,
|
||||
ServiceProvider,
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserProfile {
|
||||
pub country: String,
|
||||
pub timezone: String,
|
||||
pub currency_preference: String,
|
||||
pub wallet_balance: f64,
|
||||
}
|
||||
|
||||
/// Test marketplace data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestMarketplaceData {
|
||||
pub products: Vec<TestProduct>,
|
||||
pub services: Vec<TestService>,
|
||||
pub nodes: Vec<TestNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestProduct {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
pub price: f64,
|
||||
pub currency: String,
|
||||
pub description: String,
|
||||
pub provider_email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestService {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub price: f64,
|
||||
pub provider_email: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestNode {
|
||||
pub id: String,
|
||||
pub farmer_email: String,
|
||||
pub location: String,
|
||||
pub specs: NodeSpecs,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeSpecs {
|
||||
pub cpu_cores: u32,
|
||||
pub ram_gb: u32,
|
||||
pub storage_gb: u32,
|
||||
pub price_per_hour: f64,
|
||||
}
|
||||
|
||||
/// Test data manager for UX testing
|
||||
#[derive(Clone)]
|
||||
pub struct TestDataManager {
|
||||
test_data_dir: PathBuf,
|
||||
personas: HashMap<UserRole, TestPersona>,
|
||||
marketplace_data: TestMarketplaceData,
|
||||
}
|
||||
|
||||
impl TestDataManager {
|
||||
/// Create a new test data manager
|
||||
pub fn new(test_data_dir: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let personas = Self::create_test_personas();
|
||||
let marketplace_data = Self::create_test_marketplace_data(&personas);
|
||||
|
||||
let manager = Self {
|
||||
test_data_dir: test_data_dir.clone(),
|
||||
personas,
|
||||
marketplace_data,
|
||||
};
|
||||
|
||||
manager.setup_test_data()?;
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// Create test user personas
|
||||
fn create_test_personas() -> HashMap<UserRole, TestPersona> {
|
||||
let mut personas = HashMap::new();
|
||||
|
||||
personas.insert(UserRole::Consumer, TestPersona {
|
||||
email: "user1@example.com".to_string(),
|
||||
password: "testpass123".to_string(),
|
||||
name: "Test Consumer".to_string(),
|
||||
role: UserRole::Consumer,
|
||||
profile: UserProfile {
|
||||
country: "United States".to_string(),
|
||||
timezone: "America/New_York".to_string(),
|
||||
currency_preference: "USD".to_string(),
|
||||
wallet_balance: 100.0,
|
||||
},
|
||||
});
|
||||
|
||||
personas.insert(UserRole::Farmer, TestPersona {
|
||||
email: "farmer1@example.com".to_string(),
|
||||
password: "testpass123".to_string(),
|
||||
name: "Test Farmer".to_string(),
|
||||
role: UserRole::Farmer,
|
||||
profile: UserProfile {
|
||||
country: "Canada".to_string(),
|
||||
timezone: "America/Toronto".to_string(),
|
||||
currency_preference: "CAD".to_string(),
|
||||
wallet_balance: 500.0,
|
||||
},
|
||||
});
|
||||
|
||||
personas.insert(UserRole::AppProvider, TestPersona {
|
||||
email: "appdev1@example.com".to_string(),
|
||||
password: "testpass123".to_string(),
|
||||
name: "Test App Developer".to_string(),
|
||||
role: UserRole::AppProvider,
|
||||
profile: UserProfile {
|
||||
country: "Germany".to_string(),
|
||||
timezone: "Europe/Berlin".to_string(),
|
||||
currency_preference: "EUR".to_string(),
|
||||
wallet_balance: 200.0,
|
||||
},
|
||||
});
|
||||
|
||||
personas.insert(UserRole::ServiceProvider, TestPersona {
|
||||
email: "service1@example.com".to_string(),
|
||||
password: "testpass123".to_string(),
|
||||
name: "Test Service Provider".to_string(),
|
||||
role: UserRole::ServiceProvider,
|
||||
profile: UserProfile {
|
||||
country: "United Kingdom".to_string(),
|
||||
timezone: "Europe/London".to_string(),
|
||||
currency_preference: "TFC".to_string(),
|
||||
wallet_balance: 300.0,
|
||||
},
|
||||
});
|
||||
|
||||
personas
|
||||
}
|
||||
|
||||
/// Create test marketplace data
|
||||
fn create_test_marketplace_data(personas: &HashMap<UserRole, TestPersona>) -> TestMarketplaceData {
|
||||
let farmer_email = personas.get(&UserRole::Farmer).unwrap().email.clone();
|
||||
let app_provider_email = personas.get(&UserRole::AppProvider).unwrap().email.clone();
|
||||
let service_provider_email = personas.get(&UserRole::ServiceProvider).unwrap().email.clone();
|
||||
|
||||
TestMarketplaceData {
|
||||
products: vec![
|
||||
TestProduct {
|
||||
id: "test-vm-1".to_string(),
|
||||
name: "Test VM Small".to_string(),
|
||||
category: "compute".to_string(),
|
||||
price: 10.0,
|
||||
currency: "TFC".to_string(),
|
||||
description: "Small virtual machine for testing".to_string(),
|
||||
provider_email: farmer_email.clone(),
|
||||
},
|
||||
TestProduct {
|
||||
id: "test-app-1".to_string(),
|
||||
name: "Test Application".to_string(),
|
||||
category: "applications".to_string(),
|
||||
price: 25.0,
|
||||
currency: "TFC".to_string(),
|
||||
description: "Test application for UX testing".to_string(),
|
||||
provider_email: app_provider_email.clone(),
|
||||
},
|
||||
],
|
||||
services: vec![
|
||||
TestService {
|
||||
id: "test-service-1".to_string(),
|
||||
name: "Test Consulting Service".to_string(),
|
||||
description: "Professional consulting service for testing".to_string(),
|
||||
price: 100.0,
|
||||
provider_email: service_provider_email.clone(),
|
||||
status: "available".to_string(),
|
||||
},
|
||||
],
|
||||
nodes: vec![
|
||||
TestNode {
|
||||
id: "test-node-1".to_string(),
|
||||
farmer_email: farmer_email.clone(),
|
||||
location: "New York, USA".to_string(),
|
||||
specs: NodeSpecs {
|
||||
cpu_cores: 8,
|
||||
ram_gb: 16,
|
||||
storage_gb: 500,
|
||||
price_per_hour: 2.0,
|
||||
},
|
||||
available: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup test data files
|
||||
fn setup_test_data(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create test data directory
|
||||
std::fs::create_dir_all(&self.test_data_dir)?;
|
||||
|
||||
// Create user data files for each persona
|
||||
for persona in self.personas.values() {
|
||||
self.create_user_data_file(persona)?;
|
||||
}
|
||||
|
||||
// Save marketplace data
|
||||
let marketplace_file = self.test_data_dir.join("marketplace_data.json");
|
||||
let marketplace_json = serde_json::to_string_pretty(&self.marketplace_data)?;
|
||||
std::fs::write(marketplace_file, marketplace_json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create user data file for a persona
|
||||
fn create_user_data_file(&self, persona: &TestPersona) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let encoded_email = persona.email.replace("@", "_at_").replace(".", "_dot_");
|
||||
let user_file = self.test_data_dir.join(format!("{}.json", encoded_email));
|
||||
|
||||
let user_data = serde_json::json!({
|
||||
"email": persona.email,
|
||||
"name": persona.name,
|
||||
"profile": persona.profile,
|
||||
"role": persona.role,
|
||||
"wallet": {
|
||||
"balance": persona.profile.wallet_balance,
|
||||
"currency": persona.profile.currency_preference,
|
||||
"transactions": []
|
||||
},
|
||||
"cart": {
|
||||
"items": [],
|
||||
"total": 0.0
|
||||
},
|
||||
"orders": [],
|
||||
"settings": {
|
||||
"notifications": {
|
||||
"security_alerts": true,
|
||||
"billing_notifications": true,
|
||||
"system_alerts": true,
|
||||
"newsletter": false,
|
||||
"dashboard_notifications": true
|
||||
},
|
||||
"ssh_keys": []
|
||||
}
|
||||
});
|
||||
|
||||
let user_json = serde_json::to_string_pretty(&user_data)?;
|
||||
std::fs::write(user_file, user_json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get test persona by role
|
||||
pub fn get_persona(&self, role: &UserRole) -> Option<&TestPersona> {
|
||||
self.personas.get(role)
|
||||
}
|
||||
|
||||
/// Get all test personas
|
||||
pub fn get_all_personas(&self) -> &HashMap<UserRole, TestPersona> {
|
||||
&self.personas
|
||||
}
|
||||
|
||||
/// Get test marketplace data
|
||||
pub fn get_marketplace_data(&self) -> &TestMarketplaceData {
|
||||
&self.marketplace_data
|
||||
}
|
||||
|
||||
/// Reset test data to clean state
|
||||
pub fn reset_test_data(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Remove all user data files
|
||||
if self.test_data_dir.exists() {
|
||||
std::fs::remove_dir_all(&self.test_data_dir)?;
|
||||
}
|
||||
|
||||
// Recreate test data
|
||||
self.setup_test_data()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cleanup test data
|
||||
pub fn cleanup(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.test_data_dir.exists() {
|
||||
std::fs::remove_dir_all(&self.test_data_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get test user credentials
|
||||
pub fn get_test_credentials(&self, role: &UserRole) -> Option<(String, String)> {
|
||||
self.personas.get(role).map(|p| (p.email.clone(), p.password.clone()))
|
||||
}
|
||||
}
|
116
tests/tests_archive/ux_suite/environment/test_server.rs
Normal file
116
tests/tests_archive/ux_suite/environment/test_server.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
//! Test Server Management
|
||||
//!
|
||||
//! Manages isolated test server instance for UX testing
|
||||
|
||||
use actix_web::{web, App, HttpServer, middleware::Logger};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test server instance
|
||||
pub struct TestServer {
|
||||
port: u16,
|
||||
server_handle: Option<actix_web::dev::ServerHandle>,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
/// Start a new test server instance
|
||||
pub async fn start(port: u16) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// Set environment variables for test mode
|
||||
std::env::set_var("TEST_MODE", "true");
|
||||
std::env::set_var("TEST_PORT", port.to_string());
|
||||
std::env::set_var("TEST_DATA_DIR", "user_data_test");
|
||||
|
||||
// Import the main app configuration
|
||||
let config = threefold_marketplace::config::get_config();
|
||||
|
||||
log::info!("Starting test server on port {}", port);
|
||||
|
||||
// Create test server with the same configuration as main app
|
||||
let server = HttpServer::new(move || {
|
||||
// Initialize Tera templates
|
||||
let mut tera = match tera::Tera::new(&format!("{}/**/*.html", config.templates.dir)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("Tera initialization error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Register custom Tera functions
|
||||
threefold_marketplace::utils::register_tera_functions(&mut tera);
|
||||
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(threefold_marketplace::middleware::RequestTimer)
|
||||
.wrap(threefold_marketplace::middleware::SecurityHeaders)
|
||||
.service(actix_files::Files::new("/static", "./src/static"))
|
||||
.app_data(web::Data::new(tera))
|
||||
.configure(threefold_marketplace::routes::configure_routes)
|
||||
})
|
||||
.workers(1) // Single worker for testing
|
||||
.bind(format!("127.0.0.1:{}", port))?;
|
||||
|
||||
let server_handle = server.handle();
|
||||
|
||||
// Start server in background
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = server.run().await {
|
||||
eprintln!("Test server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for server to start
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Verify server is running
|
||||
let client = reqwest::Client::new();
|
||||
let health_check_url = format!("http://127.0.0.1:{}/", port);
|
||||
|
||||
for attempt in 1..=10 {
|
||||
match client.get(&health_check_url).send().await {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
log::info!("Test server started successfully on port {}", port);
|
||||
return Ok(Self {
|
||||
port,
|
||||
server_handle: Some(server_handle),
|
||||
});
|
||||
}
|
||||
Ok(_) | Err(_) => {
|
||||
if attempt == 10 {
|
||||
return Err("Failed to start test server after 10 attempts".into());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
port,
|
||||
server_handle: Some(server_handle),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the server URL
|
||||
pub fn url(&self) -> String {
|
||||
format!("http://127.0.0.1:{}", self.port)
|
||||
}
|
||||
|
||||
/// Stop the test server
|
||||
pub async fn stop(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(handle) = self.server_handle.take() {
|
||||
handle.stop(true).await;
|
||||
log::info!("Test server stopped");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(handle) = self.server_handle.take() {
|
||||
// Best effort cleanup
|
||||
let _ = handle.stop(false);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user