stemedb/crates/stemedb-ontology/tests/consumer_health_uat_lib/setup.rs
jordan 41c676a78e feat: Aphoria enterprise features + ontology SDK + file length compliance
Enterprise Features:
- Hosted mode with remote sync for team pattern aggregation
- Community sharing with privacy-preserving anonymization
- LLM-based semantic claim extraction with Gemini integration
- Pattern learning with promotion to declarative extractors
- High-entropy secrets extractor with configurable thresholds
- Auth bypass and insecure cookies extractors

Module Refactoring:
- Split oversized files to comply with 500-line limit
- Config split: types/core.rs, types/extractors.rs, types/hosted.rs, etc.
- Handlers split: scan.rs, policy.rs, report.rs modules
- Extractors split: declarative/, high_entropy_secrets/, insecure_cookies/
- Learning split: store modules with metrics and persistence

SDK & Ontology:
- stemedb-ontology SDK with fluent builders and StemeDB client
- Pharma domain extractors for FDA Orange Book data
- Consumer health UAT test infrastructure

Code Quality:
- Fixed clippy warnings (needless_borrows_for_generic_args)
- Added KVStore trait imports where needed
- Fixed utoipa path re-exports for OpenAPI docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:55:29 -07:00

218 lines
6.4 KiB
Rust

//! 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<String, Box<dyn std::error::Error>> {
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<Vec<SignatureDto>>,
}
#[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<u8>,
}
#[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<ClaimSummary>,
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<TierResolution>,
pub overall_winner: Option<serde_json::Value>,
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<serde_json::Value>,
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<String, Box<dyn std::error::Error>> {
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<SkepticResponse, Box<dyn std::error::Error>> {
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<LayeredQueryResponse, Box<dyn std::error::Error>> {
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)?)
}