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>
218 lines
6.4 KiB
Rust
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)?)
|
|
}
|