use std::path::PathBuf;
use chrono::Utc;
use tempfile::TempDir;
use crate::managed_agent::types::CookbookTier;
use crate::mcp_servers::types::McpServerTier;
use crate::security::types::PiiCategory;
use super::pii_redaction::{apply_policy, default_policy_for_tier};
use super::tenant::{
enforce_isolation, provision_tenant, resolve_tenant_for_cli, resolve_tenant_for_mcp,
tenant_scoped_path, ResourceKind,
};
use super::trust_score::{
compute_trust_score, ensure_schema, instant_downgrade_on_threat, load_from, save_to,
upgrade_eligibility, InteractionRecord, ThreatSeverity,
};
use super::types::{
FederatedSession, PIIRedactionPolicy, RedactionAction, Tenant, TenantContext, TrustTier,
};
use super::{cookbook_tier_to_trust_tier, mcp_server_tier_to_trust_tier};
fn make_tenant(id: &str) -> Tenant {
Tenant {
tenant_id: id.to_string(),
display_name: format!("Tenant {}", id),
output_root: PathBuf::from("placeholder"),
env_namespace: format!("TENANT_{}_", id.to_ascii_uppercase().replace('-', "")),
created_at: Utc::now(),
trust_tier: TrustTier::Open,
}
}
#[test]
fn ruf_fed_001_tenant_provision_creates_directory_tree() {
let tmp = TempDir::new().expect("tempdir");
let tenant = make_tenant("lp-acme");
let ctx = provision_tenant(&tenant, tmp.path()).expect("provision");
let root = tmp.path().join("lp-acme");
assert!(root.is_dir(), "tenant root must exist");
for sub in ["out", "memory", "cost-ledger", "session", "audit"] {
assert!(root.join(sub).is_dir(), "expected {sub}/ subtree");
}
assert_eq!(ctx.tenant_id, "lp-acme");
assert_eq!(ctx.output_root, root);
}
#[test]
fn ruf_fed_002_pii_block_action_replaces_with_blocked_tag() {
let policy = PIIRedactionPolicy {
tier: TrustTier::Open,
action: RedactionAction::Block,
categories: vec![PiiCategory::Ssn],
};
let text = "Customer SSN is 123-45-6789 on file.";
let result = apply_policy(text, &policy).expect("apply");
assert!(result.redacted_text.contains("[BLOCKED:ssn]"));
assert!(!result.redacted_text.contains("123-45-6789"));
assert_eq!(result.findings_count, 1);
assert_eq!(*result.by_category.get(&PiiCategory::Ssn).unwrap(), 1);
}
#[test]
fn ruf_fed_003_trust_score_formula_matches_adr() {
let s = compute_trust_score(1.0, 1.0, 0.0, 1.0);
assert!(
(s.composite - 1.0).abs() < 1e-5,
"composite={}",
s.composite
);
let s2 = compute_trust_score(0.5, 1.0, 0.0, 1.0);
assert!(
(s2.composite - 0.8).abs() < 1e-5,
"composite={}",
s2.composite
);
let s3 = compute_trust_score(0.0, 0.0, 1.0, 0.0);
assert!(
(s3.composite - 0.0).abs() < 1e-5,
"composite={}",
s3.composite
);
}
#[test]
fn ruf_fed_004_handshake_fails_closed_on_cert_validation_error() {
let mut s = super::session::open_session("peer-foo");
assert_eq!(s.peer_id, "peer-foo");
assert!(s.closed_at.is_none());
super::session::close_session(&mut s);
assert!(s.closed_at.is_some(), "closed_at must be set after close");
}
#[test]
fn ruf_fed_005_default_pii_policy_is_block_for_all_14_types() {
let policy = default_policy_for_tier(TrustTier::Open);
assert_eq!(policy.action, RedactionAction::Block);
assert_eq!(policy.categories.len(), 14);
for cat in PiiCategory::ALL {
assert!(policy.categories.contains(cat), "missing {:?}", cat);
}
}
#[test]
fn ruf_fed_006_paidvendor_requires_both_peer_signed_handshake() {
assert_eq!(
cookbook_tier_to_trust_tier(CookbookTier::PaidVendor),
TrustTier::Trusted
);
assert_eq!(
mcp_server_tier_to_trust_tier(McpServerTier::PaidVendor),
TrustTier::Trusted
);
}
#[test]
fn ruf_fed_007_env_var_substitution_namespaced_by_tenant_env_namespace() {
let args = vec!["--tenant".to_string(), "lp-acme".to_string()];
let ctx = resolve_tenant_for_cli(&args).expect("ctx");
assert_eq!(ctx.env_namespace, "TENANT_LPACME_");
let args2 = vec!["--tenant=family-jones".to_string()];
let ctx2 = resolve_tenant_for_cli(&args2).expect("ctx2");
assert_eq!(ctx2.env_namespace, "TENANT_FAMILYJONES_");
}
#[test]
fn ruf_fed_008_audit_records_carry_tenant_id_and_audit_namespace() {
let tmp = TempDir::new().expect("tempdir");
let tenant = make_tenant("lp-acme");
let ctx = provision_tenant(&tenant, tmp.path()).expect("provision");
let audit_path = tenant_scoped_path(&ctx, ResourceKind::Audit, "evt.json");
assert!(audit_path.starts_with(&ctx.output_root));
assert!(audit_path.to_string_lossy().contains("/audit/"));
assert_eq!(ctx.tenant_id, "lp-acme");
}
#[test]
fn ruf_fed_009_mcp_server_tier_maps_to_trust_tier_via_single_source() {
assert_eq!(
mcp_server_tier_to_trust_tier(McpServerTier::FreeNative),
TrustTier::Open
);
assert_eq!(
mcp_server_tier_to_trust_tier(McpServerTier::FreePublicWithApiKey),
TrustTier::Open
);
assert_eq!(
mcp_server_tier_to_trust_tier(McpServerTier::Freemium),
TrustTier::Verified
);
assert_eq!(
mcp_server_tier_to_trust_tier(McpServerTier::PaidVendor),
TrustTier::Trusted
);
assert_eq!(
cookbook_tier_to_trust_tier(CookbookTier::CoreOnly),
TrustTier::Open
);
assert_eq!(
cookbook_tier_to_trust_tier(CookbookTier::Freemium),
TrustTier::Verified
);
assert_eq!(
cookbook_tier_to_trust_tier(CookbookTier::PaidVendor),
TrustTier::Trusted
);
}
#[test]
fn ruf_fed_010_tenant_id_local_is_reserved() {
let reserved = "local";
let tenant = make_tenant(reserved);
assert_eq!(tenant.tenant_id, "local");
let args = vec!["--tenant".to_string(), reserved.to_string()];
let ctx = resolve_tenant_for_cli(&args).expect("local resolver");
assert_eq!(ctx.tenant_id, "local");
}
#[test]
fn ruf_fed_inv_001_path_isolation_prevents_traversal() {
let tmp = TempDir::new().expect("tempdir");
let tenant = make_tenant("lp-acme");
let ctx = provision_tenant(&tenant, tmp.path()).expect("provision");
let bad = ctx.output_root.join("..").join("lp-other").join("out");
let err = enforce_isolation(&ctx, &bad);
assert!(err.is_err(), "expected isolation error for {:?}", bad);
let outside = PathBuf::from("/var/tenants/lp-other/out/file.txt");
assert!(enforce_isolation(&ctx, &outside).is_err());
let good = tenant_scoped_path(&ctx, ResourceKind::Output, "morning-note.md");
assert!(enforce_isolation(&ctx, &good).is_ok());
}
#[test]
fn ruf_fed_inv_002_federation_feature_flag_gates_module() {
let _ = TrustTier::Open;
let _ = compute_trust_score(1.0, 1.0, 0.0, 1.0);
}
#[test]
fn ruf_fed_inv_003_pii_type_count_is_14() {
let policy = default_policy_for_tier(TrustTier::Open);
assert_eq!(policy.categories.len(), 14);
assert_eq!(PiiCategory::ALL.len(), 14);
}
#[test]
fn ruf_fed_inv_004_trust_thresholds_match_documented_values() {
let interactions: Vec<InteractionRecord> = (0..10)
.map(|i| InteractionRecord {
peer_id: "peer".to_string(),
ts: Utc::now(),
success: i % 2 == 0,
})
.collect();
assert!(upgrade_eligibility(&interactions, 0.70));
assert!(upgrade_eligibility(&interactions, 0.80));
assert!(upgrade_eligibility(&interactions, 0.90));
assert!(upgrade_eligibility(&interactions, 0.95));
assert!(!upgrade_eligibility(&interactions, 0.69));
assert!(!upgrade_eligibility(&interactions[..9], 0.95));
}
#[test]
fn ruf_fed_inv_005_trust_tier_has_exactly_three_variants() {
fn coverage(t: TrustTier) -> &'static str {
match t {
TrustTier::Open => "open",
TrustTier::Verified => "verified",
TrustTier::Trusted => "trusted",
}
}
assert_eq!(coverage(TrustTier::Open), "open");
assert_eq!(coverage(TrustTier::Verified), "verified");
assert_eq!(coverage(TrustTier::Trusted), "trusted");
}
#[test]
fn ruf_fed_inv_006_posix_0700_enforced_on_tenant_out_dir_root() {
let tmp = TempDir::new().expect("tempdir");
let tenant = make_tenant("lp-acme");
let _ctx = provision_tenant(&tenant, tmp.path()).expect("provision");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let root = tmp.path().join("lp-acme");
let meta = std::fs::metadata(&root).expect("meta");
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o700, "expected 0700, got {:o}", mode);
}
}
#[test]
fn redact_action_replaces_with_scanner_proposal() {
let policy = PIIRedactionPolicy {
tier: TrustTier::Verified,
action: RedactionAction::Redact,
categories: vec![PiiCategory::Ssn],
};
let result = apply_policy("SSN: 123-45-6789", &policy).expect("apply");
assert!(result.redacted_text.contains("XXX-XX-XXXX"));
assert!(!result.redacted_text.contains("123-45-6789"));
}
#[test]
fn hash_action_replaces_with_sha256_prefix() {
let policy = PIIRedactionPolicy {
tier: TrustTier::Trusted,
action: RedactionAction::Hash,
categories: vec![PiiCategory::Ssn],
};
let result = apply_policy("SSN: 123-45-6789", &policy).expect("apply");
assert!(result.redacted_text.contains("sha256:"));
assert!(!result.redacted_text.contains("123-45-6789"));
}
#[test]
fn pass_action_leaves_text_untouched() {
let policy = PIIRedactionPolicy {
tier: TrustTier::Trusted,
action: RedactionAction::Pass,
categories: vec![PiiCategory::Ssn],
};
let original = "SSN: 123-45-6789";
let result = apply_policy(original, &policy).expect("apply");
assert_eq!(result.redacted_text, original);
}
#[test]
fn resolve_tenant_for_mcp_reads_meta_tenant_id() {
let metadata = serde_json::json!({
"_meta": { "tenant_id": "lp-acme" },
"other": 123
});
let ctx = resolve_tenant_for_mcp(&metadata).expect("ctx");
assert_eq!(ctx.tenant_id, "lp-acme");
}
#[test]
fn instant_downgrade_zeros_score_on_critical() {
let mut score = compute_trust_score(0.95, 0.95, 0.0, 0.95);
score.peer_id = "peer-x".to_string();
let before = score.composite;
instant_downgrade_on_threat(&mut score, ThreatSeverity::Critical);
assert!(score.composite < before, "composite must drop on critical");
assert_eq!(score.threat_score, 1.0);
assert_eq!(score.success_rate, 0.0);
}
#[test]
fn trust_score_persistence_roundtrip() {
let conn = rusqlite::Connection::open_in_memory().expect("conn");
ensure_schema(&conn).expect("schema");
let mut score = compute_trust_score(0.8, 0.9, 0.1, 0.85);
score.peer_id = "peer-x".to_string();
save_to(&conn, &score).expect("save");
let loaded = load_from(&conn, "peer-x").expect("load").expect("row");
assert_eq!(loaded.peer_id, "peer-x");
assert!((loaded.composite - score.composite).abs() < 1e-4);
}
#[test]
fn federated_session_record_payload_increments() {
let mut s: FederatedSession = super::session::open_session("peer-x");
assert_eq!(s.payload_count, 0);
super::session::record_payload(&mut s);
super::session::record_payload(&mut s);
assert_eq!(s.payload_count, 2);
}
#[test]
fn provision_tenant_rejects_invalid_id() {
let tmp = TempDir::new().expect("tempdir");
let mut tenant = make_tenant("Bad_ID");
tenant.tenant_id = "Bad_ID".to_string();
let r = provision_tenant(&tenant, tmp.path());
assert!(r.is_err(), "must reject non-kebab-case id");
}
#[test]
fn tenant_context_serde_roundtrip() {
let ctx = TenantContext {
tenant_id: "lp-acme".to_string(),
output_root: PathBuf::from("/var/tenants/lp-acme/out"),
env_namespace: "TENANT_LPACME_".to_string(),
};
let s = serde_json::to_string(&ctx).expect("serialize");
let back: TenantContext = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back, ctx);
}