init projectmycelium

This commit is contained in:
mik-tf
2025-09-01 21:37:01 -04:00
commit b41efb0e99
319 changed files with 128160 additions and 0 deletions

View 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,
}

View 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?
}
}

View 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(())
}
}

View 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()))
}
}

View 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);
}
}
}