//! Test setup, DTOs, and helper functions for Consumer Health UAT scenarios. use ed25519_dalek::{Signer, SigningKey}; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use std::env; // ==================== Test Isolation ==================== /// Generate a unique subject prefix for test isolation. /// Each test run gets unique subjects to avoid pollution from previous runs. pub fn unique_prefix() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0); format!("test_{}", nanos) } // ==================== API Client Setup ==================== /// Base URL for StemeDB API - defaults to localhost:18180 pub fn api_url() -> String { env::var("STEMEDB_API_URL").unwrap_or_else(|_| "http://localhost:18180".to_string()) } /// HTTP client for API calls pub fn client() -> reqwest::blocking::Client { reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .expect("failed to create HTTP client") } /// Check HTTP response and extract body, returning a descriptive error on failure. pub fn check_response( response: reqwest::blocking::Response, context: &str, ) -> Result> { if response.status().is_success() { Ok(response.text()?) } else { let status = response.status(); let body = response.text().unwrap_or_default(); Err(format!("{} failed with {}: {}", context, status, body).into()) } } // ==================== DTOs ==================== // NOTE: These DTOs intentionally duplicate structures from stemedb-api. // Integration tests should not depend on internal crate types to maintain // a clean API boundary. This ensures tests validate the actual wire format. #[derive(Debug, Serialize, Deserialize)] pub struct CreateAssertionRequest { pub subject: String, pub predicate: String, pub object: ObjectValue, pub confidence: f32, pub source_class: String, pub source_hash: String, #[serde(skip_serializing_if = "Option::is_none")] pub signatures: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct SignatureDto { pub agent_id: String, pub signature: String, pub timestamp: u64, #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "value")] pub enum ObjectValue { Boolean(bool), Number(f64), Text(String), } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct CreateResponse { pub hash: String, pub status: String, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct SkepticResponse { pub subject: String, pub predicate: String, pub status: String, pub conflict_score: f32, pub claims: Vec, pub candidates_count: usize, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct ClaimSummary { pub value: serde_json::Value, pub weight_share: f32, pub assertion_count: usize, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct LayeredQueryResponse { pub subject: String, pub predicate: String, pub tiers: Vec, pub overall_winner: Option, pub overall_conflict_score: f32, pub total_candidates: usize, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct TierResolution { pub tier: u8, pub source_class: String, pub winner: Option, pub candidates_count: usize, pub conflict_score: f32, pub resolution_confidence: f32, } // ==================== Signing Helpers ==================== /// Test signing key - generated once per test run pub fn get_signing_key() -> SigningKey { SigningKey::generate(&mut OsRng) } /// Sign a message using v1 (legacy) format: "{subject}:{predicate}" pub fn sign_v1(signing_key: &SigningKey, subject: &str, predicate: &str) -> SignatureDto { let message = format!("{}:{}", subject, predicate); let signature = signing_key.sign(message.as_bytes()); let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); SignatureDto { agent_id: hex::encode(signing_key.verifying_key().as_bytes()), signature: hex::encode(signature.to_bytes()), timestamp, version: Some(1), } } // ==================== API Helper Functions ==================== /// POST /v1/assert with automatic signature generation pub fn create_assertion( signing_key: &SigningKey, subject: &str, predicate: &str, object: ObjectValue, confidence: f32, source_class: &str, source_hash: &str, ) -> Result> { let url = format!("{}/v1/assert", api_url()); let signature = sign_v1(signing_key, subject, predicate); let request = CreateAssertionRequest { subject: subject.to_string(), predicate: predicate.to_string(), object, confidence, source_class: source_class.to_string(), source_hash: source_hash.to_string(), signatures: Some(vec![signature]), }; let response = client().post(&url).json(&request).send()?; let body = check_response(response, "POST /v1/assert")?; let create_response: CreateResponse = serde_json::from_str(&body)?; Ok(create_response.hash) } /// GET /v1/skeptic?subject=...&predicate=... pub fn query_skeptic( subject: &str, predicate: &str, ) -> Result> { let url = format!( "{}/v1/skeptic?subject={}&predicate={}", api_url(), urlencoding::encode(subject), urlencoding::encode(predicate) ); let response = client().get(&url).send()?; let body = check_response(response, "GET /v1/skeptic")?; Ok(serde_json::from_str(&body)?) } /// GET /v1/layered?subject=...&predicate=... pub fn query_layered( subject: &str, predicate: &str, ) -> Result> { let url = format!( "{}/v1/layered?subject={}&predicate={}", api_url(), urlencoding::encode(subject), urlencoding::encode(predicate) ); let response = client().get(&url).send()?; let body = check_response(response, "GET /v1/layered")?; Ok(serde_json::from_str(&body)?) }