#![allow(clippy::unwrap_used, clippy::expect_used)]
use saorsa_core::{AdaptiveDhtConfig, NodeConfig, P2PNode, PeerId, TrustEvent};
const NEUTRAL_TRUST: f64 = 0.5;
const SWAP_THRESHOLD: f64 = 0.35;
fn test_node_config() -> NodeConfig {
NodeConfig::builder()
.local(true)
.port(0)
.ipv6(false)
.build()
.expect("test config should be valid")
}
#[tokio::test]
async fn unknown_peer_starts_at_neutral() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
let score = node.peer_trust(&peer);
assert!(
(score - NEUTRAL_TRUST).abs() < f64::EPSILON,
"Expected neutral trust {NEUTRAL_TRUST}, got {score}"
);
}
#[tokio::test]
async fn successes_raise_trust_above_neutral() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
for _ in 0..20 {
node.report_trust_event(&peer, TrustEvent::ApplicationSuccess(1.0))
.await;
}
let score = node.peer_trust(&peer);
assert!(
score > NEUTRAL_TRUST,
"After 20 successes, trust {score} should exceed neutral {NEUTRAL_TRUST}"
);
}
#[tokio::test]
async fn failures_lower_trust_below_neutral() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
for _ in 0..20 {
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
}
let score = node.peer_trust(&peer);
assert!(
score < NEUTRAL_TRUST,
"After 20 failures, trust {score} should be below neutral {NEUTRAL_TRUST}"
);
}
#[tokio::test]
async fn all_trust_event_variants_affect_score() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let positive_events = [TrustEvent::ApplicationSuccess(1.0)];
let negative_events = [TrustEvent::ConnectionFailed, TrustEvent::ConnectionTimeout];
for event in positive_events {
let peer = PeerId::random();
node.report_trust_event(&peer, event).await;
let score = node.peer_trust(&peer);
assert!(
score > NEUTRAL_TRUST,
"Positive event {event:?} should raise score above neutral, got {score}"
);
}
for event in negative_events {
let peer = PeerId::random();
node.report_trust_event(&peer, event).await;
let score = node.peer_trust(&peer);
assert!(
score < NEUTRAL_TRUST,
"Negative event {event:?} should lower score below neutral, got {score}"
);
}
}
#[tokio::test]
async fn sustained_failures_drop_below_swap_threshold() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let bad_peer = PeerId::random();
for _ in 0..50 {
node.report_trust_event(&bad_peer, TrustEvent::ConnectionFailed)
.await;
}
let score = node.peer_trust(&bad_peer);
assert!(
score < SWAP_THRESHOLD,
"After 50 failures, trust {score} should be below swap threshold {SWAP_THRESHOLD}"
);
}
#[tokio::test]
async fn single_failure_does_not_cross_swap_threshold() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
let score = node.peer_trust(&peer);
assert!(
score >= SWAP_THRESHOLD,
"One failure from neutral should not cross threshold; score={score}, threshold={SWAP_THRESHOLD}"
);
}
#[tokio::test]
async fn trusted_peer_resilient_to_occasional_failures() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
for _ in 0..50 {
node.report_trust_event(&peer, TrustEvent::ApplicationSuccess(1.0))
.await;
}
let high_score = node.peer_trust(&peer);
for _ in 0..3 {
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
}
let score_after = node.peer_trust(&peer);
assert!(
score_after >= SWAP_THRESHOLD,
"3 failures after 50 successes should not block; score={score_after}"
);
assert!(
score_after < high_score,
"Score should have decreased from {high_score} to {score_after}"
);
}
#[tokio::test]
async fn trust_engine_arc_shares_state_with_node() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
node.report_trust_event(&peer, TrustEvent::ApplicationSuccess(1.0))
.await;
let engine = node.trust_engine();
let score = engine.score(&peer);
assert!(
score > NEUTRAL_TRUST,
"Engine should reflect the event reported through P2PNode; got {score}"
);
}
#[tokio::test]
async fn removing_peer_resets_to_neutral() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
for _ in 0..30 {
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
}
assert!(node.peer_trust(&peer) < NEUTRAL_TRUST);
node.trust_engine().remove_node(&peer);
let score = node.peer_trust(&peer);
assert!(
(score - NEUTRAL_TRUST).abs() < f64::EPSILON,
"Removed peer should return to neutral; got {score}"
);
}
#[tokio::test]
async fn peers_tracked_independently() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let good_peer = PeerId::random();
let bad_peer = PeerId::random();
let neutral_peer = PeerId::random();
for _ in 0..20 {
node.report_trust_event(&good_peer, TrustEvent::ApplicationSuccess(1.0))
.await;
node.report_trust_event(&bad_peer, TrustEvent::ConnectionFailed)
.await;
}
let good_score = node.peer_trust(&good_peer);
let bad_score = node.peer_trust(&bad_peer);
let neutral_score = node.peer_trust(&neutral_peer);
assert!(good_score > NEUTRAL_TRUST, "Good peer score: {good_score}");
assert!(bad_score < NEUTRAL_TRUST, "Bad peer score: {bad_score}");
assert!(
(neutral_score - NEUTRAL_TRUST).abs() < f64::EPSILON,
"Untouched peer should be neutral: {neutral_score}"
);
}
#[tokio::test]
async fn trust_scores_bounded() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
for _ in 0..500 {
node.report_trust_event(&peer, TrustEvent::ApplicationSuccess(1.0))
.await;
}
let high = node.peer_trust(&peer);
assert!((0.0..=1.0).contains(&high), "Score out of bounds: {high}");
for _ in 0..1000 {
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
}
let low = node.peer_trust(&peer);
assert!((0.0..=1.0).contains(&low), "Score out of bounds: {low}");
}
#[tokio::test]
async fn custom_swap_threshold_respected() {
let custom_threshold = 0.3;
let config = NodeConfig::builder()
.local(true)
.port(0)
.ipv6(false)
.adaptive_dht_config(AdaptiveDhtConfig {
swap_threshold: custom_threshold,
})
.build()
.unwrap();
let node = P2PNode::new(config).await.unwrap();
let threshold = node.adaptive_dht().config().swap_threshold;
assert!(
(threshold - custom_threshold).abs() < f64::EPSILON,
"Expected threshold {custom_threshold}, got {threshold}"
);
}
#[tokio::test]
async fn trust_enforcement_disabled_no_swap_eligibility() {
let config = NodeConfig::builder()
.local(true)
.port(0)
.ipv6(false)
.trust_enforcement(false)
.build()
.unwrap();
let node = P2PNode::new(config).await.unwrap();
let peer = PeerId::random();
for _ in 0..100 {
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
}
let score = node.peer_trust(&peer);
let threshold = node.adaptive_dht().config().swap_threshold;
assert!(
score >= threshold,
"With enforcement disabled (threshold={threshold}), score {score} should be >= threshold"
);
}
#[tokio::test]
async fn ema_blends_observations() {
let node = P2PNode::new(test_node_config()).await.unwrap();
let peer = PeerId::random();
node.report_trust_event(&peer, TrustEvent::ConnectionFailed)
.await;
let after_fail = node.peer_trust(&peer);
node.report_trust_event(&peer, TrustEvent::ApplicationSuccess(1.0))
.await;
let after_recovery = node.peer_trust(&peer);
assert!(
after_recovery > after_fail,
"Success after failure should raise score: {after_fail} -> {after_recovery}"
);
}
#[tokio::test]
async fn default_config_matches_expected_threshold() {
let config = AdaptiveDhtConfig::default();
assert!(
(config.swap_threshold - SWAP_THRESHOLD).abs() < f64::EPSILON,
"Default threshold {} != expected {}",
config.swap_threshold,
SWAP_THRESHOLD
);
}
#[tokio::test]
async fn invalid_swap_threshold_rejected() {
for bad_threshold in [f64::NAN, f64::NEG_INFINITY, -0.1, 1.1, f64::INFINITY] {
let config = NodeConfig::builder()
.local(true)
.port(0)
.ipv6(false)
.adaptive_dht_config(AdaptiveDhtConfig {
swap_threshold: bad_threshold,
})
.build();
match config {
Err(_) => {}
Ok(config) => {
let result = P2PNode::new(config).await;
assert!(
result.is_err(),
"Swap threshold {bad_threshold} should be rejected"
);
}
}
}
}