use super::*;
use crate::discovery::nostr::{NostrAdvertIngestOutcome, OverlayTransportKind, TraversalAddress};
#[test]
fn event_channel_capacity_tracks_open_and_inbound_limits() {
let mut config = NostrDiscoveryConfig {
open_discovery_max_pending: 8,
max_concurrent_incoming_offers: 16,
..Default::default()
};
assert_eq!(event_channel_capacity(&config), 64);
config.open_discovery_max_pending = 32;
config.max_concurrent_incoming_offers = 4;
assert_eq!(event_channel_capacity(&config), 128);
config.open_discovery_max_pending = 0;
config.max_concurrent_incoming_offers = 0;
assert_eq!(event_channel_capacity(&config), 64);
config.open_discovery_max_pending = 5000;
config.max_concurrent_incoming_offers = 1;
assert_eq!(event_channel_capacity(&config), 4096);
}
#[test]
fn advert_publish_retry_delay_backs_off_to_short_cap() {
assert_eq!(
next_advert_publish_retry_delay(ADVERT_PUBLISH_RETRY_INITIAL),
Duration::from_secs(4)
);
assert_eq!(
next_advert_publish_retry_delay(Duration::from_secs(16)),
Duration::from_secs(30)
);
assert_eq!(
next_advert_publish_retry_delay(Duration::from_secs(30)),
ADVERT_PUBLISH_RETRY_MAX
);
}
#[test]
fn signal_answer_wait_is_bounded_by_attempt_timeout() {
let config = NostrDiscoveryConfig {
signal_ttl_secs: 120,
attempt_timeout_secs: 10,
..Default::default()
};
assert_eq!(signal_answer_timeout(&config), Duration::from_secs(10));
let config = NostrDiscoveryConfig {
signal_ttl_secs: 5,
attempt_timeout_secs: 10,
..Default::default()
};
assert_eq!(signal_answer_timeout(&config), Duration::from_secs(5));
}
#[test]
fn mesh_signaled_initiators_use_direct_refresh_admission() {
let discovery = NostrDiscovery::new_for_test();
discovery.set_outbound_admission(false);
discovery.set_direct_refresh_admission(true);
assert!(
!discovery.traversal_initiator_admission_allowed(false),
"ordinary Nostr traversal should still obey peer-slot capacity"
);
assert!(
discovery.traversal_initiator_admission_allowed(true),
"mesh-signaled direct refresh should bypass only the peer-slot cap"
);
discovery.set_direct_refresh_admission(false);
assert!(
!discovery.traversal_initiator_admission_allowed(true),
"mesh-signaled direct refresh should still obey connection/link capacity"
);
}
#[tokio::test]
async fn traversal_replay_cache_is_scoped_by_signal_path() {
let discovery = NostrDiscovery::new_for_test();
discovery
.mark_session_seen("session", TraversalSignalPath::Mesh)
.await
.expect("first mesh copy should be accepted");
discovery
.mark_session_seen("session", TraversalSignalPath::Nostr)
.await
.expect("Nostr fallback copy of the same offer should still be accepted");
let duplicate_mesh = discovery
.mark_session_seen("session", TraversalSignalPath::Mesh)
.await
.expect_err("duplicate mesh copy should still be rejected");
assert!(matches!(duplicate_mesh, BootstrapError::Replay(_)));
let duplicate_nostr = discovery
.mark_session_seen("session", TraversalSignalPath::Nostr)
.await
.expect_err("duplicate Nostr copy should still be rejected");
assert!(matches!(duplicate_nostr, BootstrapError::Replay(_)));
}
#[test]
fn ambient_advert_subscription_is_open_policy_only() {
let discovery = NostrDiscovery::new_for_test();
assert!(!discovery.should_subscribe_ambient_adverts());
let open = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
policy: crate::config::NostrDiscoveryPolicy::Open,
..Default::default()
});
assert!(open.should_subscribe_ambient_adverts());
let disabled = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
policy: crate::config::NostrDiscoveryPolicy::Disabled,
..Default::default()
});
assert!(!disabled.should_subscribe_ambient_adverts());
}
#[test]
fn rating_fact_subscription_is_enabled_by_trust_config() {
let discovery = NostrDiscovery::new_for_test();
assert!(!discovery.should_subscribe_rating_facts());
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
..Default::default()
});
assert!(discovery.should_subscribe_rating_facts());
let filter = serde_json::to_value(discovery.rating_fact_filter()).unwrap();
assert_eq!(
filter["kinds"],
serde_json::json!([ratings::RATING_FACT_KIND])
);
assert_eq!(filter["#i"], serde_json::json!(["fips.peer"]));
assert_eq!(filter["limit"], 500);
assert!(filter["since"].as_u64().is_some());
}
#[tokio::test]
async fn trusted_rating_fact_updates_peer_trust_score() {
let author = nostr::Keys::generate();
let author_npub = author.public_key().to_bech32().expect("author npub");
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
open_discovery_trusted_rating_authors: vec![author_npub],
..Default::default()
});
let event = signed_rating_fact_event(&author, &subject_npub, "fips.peer", 80, 42);
assert!(discovery.process_rating_fact_event(&event).await);
let scores = discovery
.trust_scores_for_npubs(std::slice::from_ref(&subject_npub))
.await;
assert_eq!(scores.get(&subject_npub), Some(&60));
}
#[tokio::test]
async fn trusted_rating_fact_signer_can_differ_from_rater() {
let crawler = nostr::Keys::generate();
let crawler_npub = crawler.public_key().to_bech32().expect("crawler npub");
let rater = nostr::Keys::generate();
let rater_npub = rater.public_key().to_bech32().expect("rater npub");
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
open_discovery_trusted_rating_authors: vec![crawler_npub],
..Default::default()
});
let event = signed_rating_fact_event_with_rater(
&crawler,
&rater_npub,
&subject_npub,
"fips.peer",
75,
43,
);
assert_ne!(event.pubkey, rater.public_key());
assert!(discovery.process_rating_fact_event(&event).await);
let scores = discovery
.trust_scores_for_npubs(std::slice::from_ref(&subject_npub))
.await;
assert_eq!(scores.get(&subject_npub), Some(&50));
}
#[tokio::test]
async fn peer_trust_snapshot_uses_newest_rating_per_peer() {
let author = nostr::Keys::generate();
let author_npub = author.public_key().to_bech32().expect("author npub");
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
open_discovery_trusted_rating_authors: vec![author_npub],
..Default::default()
});
assert!(
discovery
.process_rating_fact_event(&signed_rating_fact_event(
&author,
&subject_npub,
"fips.peer",
80,
42,
))
.await
);
assert!(
discovery
.process_rating_fact_event(&signed_rating_fact_event(
&author,
&subject_npub,
"fips.peer",
0,
41,
))
.await
);
assert!(
discovery
.process_rating_fact_event(&signed_rating_fact_event(
&author,
&subject_npub,
"fips.peer",
100,
43,
))
.await
);
let snapshot = discovery
.peer_trust_score_snapshot()
.expect("trust cache snapshot");
assert_eq!(snapshot.len(), 1);
assert_eq!(snapshot[0].npub, subject_npub);
assert_eq!(snapshot[0].score, 100);
assert_eq!(snapshot[0].updated_at_secs, 43);
}
#[tokio::test]
async fn configured_rating_fact_file_updates_peer_trust_score() {
let author = nostr::Keys::generate();
let author_npub = author.public_key().to_bech32().expect("author npub");
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let event = signed_rating_fact_event(&author, &subject_npub, "fips.peer", 90, 43);
let tempdir = tempfile::tempdir().expect("tempdir");
let path = tempdir.path().join("ratings.json");
std::fs::write(
&path,
serde_json::to_vec_pretty(&serde_json::json!({ "events": [event] }))
.expect("encode rating events"),
)
.expect("write rating event file");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
open_discovery_trusted_rating_authors: vec![author_npub],
open_discovery_rating_event_files: vec![path],
..Default::default()
});
let report = discovery.load_rating_fact_events_from_files().await;
assert_eq!(report.files, 1);
assert_eq!(report.events, 1);
assert_eq!(report.accepted, 1);
let scores = discovery
.trust_scores_for_npubs(std::slice::from_ref(&subject_npub))
.await;
assert_eq!(scores.get(&subject_npub), Some(&80));
}
#[tokio::test]
async fn hashtree_query_output_rating_file_updates_peer_trust_score() {
let author = nostr::Keys::generate();
let author_npub = author.public_key().to_bech32().expect("author npub");
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let event = signed_rating_fact_event(&author, &subject_npub, "fips.peer", 95, 44);
let tempdir = tempfile::tempdir().expect("tempdir");
let path = tempdir.path().join("hashtree-query.json");
std::fs::write(
&path,
serde_json::to_vec_pretty(&serde_json::json!({
"root": "nhash1testfixture",
"count": 1,
"events": [event],
}))
.expect("encode hashtree query output"),
)
.expect("write rating event file");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
open_discovery_trusted_rating_authors: vec![author_npub],
open_discovery_rating_event_files: vec![path],
..Default::default()
});
let report = discovery.load_rating_fact_events_from_files().await;
assert_eq!(report.files, 1);
assert_eq!(report.events, 1);
assert_eq!(report.accepted, 1);
let scores = discovery
.trust_scores_for_npubs(std::slice::from_ref(&subject_npub))
.await;
assert_eq!(scores.get(&subject_npub), Some(&90));
}
#[tokio::test]
async fn untrusted_rating_fact_is_ignored() {
let author = nostr::Keys::generate();
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
..Default::default()
});
let event = signed_rating_fact_event(&author, &subject_npub, "fips.peer", 80, 42);
assert!(!discovery.process_rating_fact_event(&event).await);
let scores = discovery
.trust_scores_for_npubs(std::slice::from_ref(&subject_npub))
.await;
assert!(!scores.contains_key(&subject_npub));
}
#[tokio::test]
async fn rating_fact_scope_must_match_configured_scope() {
let author = nostr::Keys::generate();
let author_npub = author.public_key().to_bech32().expect("author npub");
let subject = nostr::Keys::generate();
let subject_npub = subject.public_key().to_bech32().expect("subject npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
open_discovery_trust_ratings_enabled: true,
open_discovery_trusted_rating_authors: vec![author_npub],
..Default::default()
});
let event = signed_rating_fact_event(&author, &subject_npub, "other.scope", 80, 42);
assert!(!discovery.process_rating_fact_event(&event).await);
let scores = discovery
.trust_scores_for_npubs(std::slice::from_ref(&subject_npub))
.await;
assert!(!scores.contains_key(&subject_npub));
}
#[tokio::test]
async fn duplicate_connect_request_reports_already_active() {
let discovery = Arc::new(NostrDiscovery::new_for_test());
let peer_npub = nostr::Keys::generate()
.public_key()
.to_bech32()
.expect("peer npub");
let peer_config = PeerConfig::new(peer_npub, "udp", "nat");
assert!(
discovery
.request_connect_with_mesh_signaling(peer_config.clone(), true)
.await,
"first request should spawn an initiator"
);
assert!(
!discovery
.request_connect_with_mesh_signaling(peer_config, true)
.await,
"second request for the same peer should be deduped"
);
assert_eq!(discovery.active_initiator_count_for_test().await, 1);
}
fn signed_rating_fact_event(
keys: &nostr::Keys,
subject_npub: &str,
scope: &str,
rating: i64,
created_at: u64,
) -> Event {
let rater_npub = keys.public_key().to_bech32().expect("rater npub");
signed_rating_fact_event_with_rater(keys, &rater_npub, subject_npub, scope, rating, created_at)
}
fn signed_rating_fact_event_with_rater(
keys: &nostr::Keys,
rater_npub: &str,
subject_npub: &str,
scope: &str,
rating: i64,
created_at: u64,
) -> Event {
let created_at_string = created_at.to_string();
let rating_string = rating.to_string();
let rater_index = rater_npub.to_lowercase();
let subject_index = subject_npub.to_lowercase();
let scope_index = scope.to_lowercase();
let tags = vec![
rating_fact_tag(["i", "550e8400-e29b-41d4-a716-446655440000", "subject"]),
rating_fact_tag(["i", &rater_index]),
rating_fact_tag(["i", &subject_index]),
rating_fact_tag(["i", &scope_index]),
rating_fact_tag(["type", "rating"]),
rating_fact_tag(["schema", "1"]),
rating_fact_tag(["created_at", &created_at_string]),
rating_fact_tag(["rater", rater_npub]),
rating_fact_tag(["subject", subject_npub]),
rating_fact_tag(["scope", scope]),
rating_fact_tag(["rating", &rating_string]),
rating_fact_tag(["min_rating", "0"]),
rating_fact_tag(["max_rating", "100"]),
];
EventBuilder::new(Kind::Custom(ratings::RATING_FACT_KIND), "")
.tags(tags)
.custom_created_at(Timestamp::from(created_at))
.sign_with_keys(keys)
.unwrap()
}
fn rating_fact_tag<const N: usize>(parts: [&str; N]) -> Tag {
Tag::parse(parts).unwrap()
}
#[tokio::test]
async fn duplicate_connect_request_canonicalizes_hex_and_npub() {
let discovery = Arc::new(NostrDiscovery::new_for_test());
let peer_pubkey = nostr::Keys::generate().public_key();
let peer_npub = peer_pubkey.to_bech32().expect("peer npub");
let peer_hex = peer_pubkey.to_hex();
assert!(
discovery
.request_connect_with_mesh_signaling(PeerConfig::new(peer_npub, "udp", "nat"), true)
.await,
"first request should spawn an initiator"
);
assert!(
!discovery
.request_connect_with_mesh_signaling(PeerConfig::new(peer_hex, "udp", "nat"), true)
.await,
"same pubkey with a different edge spelling should be deduped"
);
assert_eq!(discovery.active_initiator_count_for_test().await, 1);
}
#[tokio::test]
async fn advert_cache_lookup_canonicalizes_hex_and_npub() {
let discovery = NostrDiscovery::new_for_test();
let peer_pubkey = nostr::Keys::generate().public_key();
let peer_npub = peer_pubkey.to_bech32().expect("peer npub");
let peer_hex = peer_pubkey.to_hex();
let endpoint = OverlayEndpointAdvert {
transport: OverlayTransportKind::Udp,
addr: "nat".to_string(),
};
let advert = NostrDiscovery::cached_advert_for_test(peer_npub.clone(), endpoint.clone(), 42);
discovery.insert_advert_for_test(peer_npub, advert).await;
assert_eq!(
discovery.cached_advert_endpoints_for_peer(&peer_hex).await,
Some(vec![endpoint])
);
}
#[test]
fn ambient_advert_filter_targets_normal_nostr_adverts_for_app() {
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
app: "fips-test".to_string(),
..Default::default()
});
let filter = serde_json::to_value(discovery.ambient_advert_filter()).unwrap();
assert_eq!(filter["kinds"], serde_json::json!([ADVERT_KIND]));
assert_eq!(filter["#d"], serde_json::json!(["fips-test"]));
}
#[test]
fn advert_retention_policy_matches_app_scoped_pubsub_adverts() {
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
app: "fips-test".to_string(),
advert_cache_max_entries: 17,
..Default::default()
});
let peer = nostr::Keys::generate();
let created_at_secs = Timestamp::now().as_secs();
let matching = signed_runtime_overlay_advert_event(
&peer,
"fips-test",
OverlayTransportKind::Tcp,
"8.8.8.8:443",
created_at_secs,
);
let other_app = signed_runtime_overlay_advert_event(
&peer,
"other-app",
OverlayTransportKind::Tcp,
"8.8.4.4:443",
created_at_secs,
);
let policy = discovery.advert_retention_policy();
assert_eq!(policy.max_events, 17);
assert!(policy.accepts(
&nostr_pubsub::VerifiedEvent::try_from(matching).expect("matching pubsub advert")
));
assert!(!policy.accepts(
&nostr_pubsub::VerifiedEvent::try_from(other_app).expect("other app pubsub advert")
));
}
#[tokio::test]
async fn pubsub_advert_ingest_populates_peer_cache() {
let peer = nostr::Keys::generate();
let peer_npub = peer.public_key().to_bech32().expect("peer npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
app: "fips-test".to_string(),
..Default::default()
});
let event = signed_runtime_overlay_advert_event(
&peer,
"fips-test",
OverlayTransportKind::Tcp,
"8.8.8.8:443",
Timestamp::now().as_secs(),
);
assert_eq!(
discovery
.ingest_pubsub_advert_event(
nostr_pubsub::VerifiedEvent::try_from(event).expect("verified pubsub advert"),
)
.await,
NostrAdvertIngestOutcome::Cached
);
let endpoints = discovery
.cached_advert_endpoints_for_peer(&peer_npub)
.await
.expect("pubsub advert should populate cache");
assert_eq!(
endpoints,
vec![OverlayEndpointAdvert {
transport: OverlayTransportKind::Tcp,
addr: "8.8.8.8:443".to_string(),
}]
);
}
#[tokio::test]
async fn pubsub_cache_resolves_before_relay_fallback() {
let peer = nostr::Keys::generate();
let peer_npub = peer.public_key().to_bech32().expect("peer npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
app: "fips-test".to_string(),
advert_relays: Vec::new(),
..Default::default()
});
let event = signed_runtime_overlay_advert_event(
&peer,
"fips-test",
OverlayTransportKind::Tcp,
"8.8.4.4:443",
Timestamp::now().as_secs(),
);
assert!(discovery.ingest_advert_event(&event).await.cached());
let endpoints = discovery
.advert_endpoints_for_peer(&peer_npub)
.await
.expect("cache should resolve before relay fallback");
assert_eq!(
endpoints,
vec![OverlayEndpointAdvert {
transport: OverlayTransportKind::Tcp,
addr: "8.8.4.4:443".to_string(),
}]
);
}
#[tokio::test]
async fn stale_pubsub_advert_does_not_replace_newer_cache_entry() {
let peer = nostr::Keys::generate();
let peer_npub = peer.public_key().to_bech32().expect("peer npub");
let discovery = NostrDiscovery::new_for_test_with_config(NostrDiscoveryConfig {
app: "fips-test".to_string(),
..Default::default()
});
let now_secs = Timestamp::now().as_secs();
let newer = signed_runtime_overlay_advert_event(
&peer,
"fips-test",
OverlayTransportKind::Tcp,
"8.8.8.8:443",
now_secs,
);
let older = signed_runtime_overlay_advert_event(
&peer,
"fips-test",
OverlayTransportKind::Tcp,
"8.8.4.4:443",
now_secs.saturating_sub(1),
);
assert_eq!(
discovery.ingest_advert_event(&newer).await,
NostrAdvertIngestOutcome::Cached
);
assert_eq!(
discovery.ingest_advert_event(&older).await,
NostrAdvertIngestOutcome::Stale
);
let endpoints = discovery
.cached_advert_endpoints_for_peer(&peer_npub)
.await
.expect("newer advert should remain cached");
assert_eq!(endpoints[0].addr, "8.8.8.8:443");
}
fn signed_runtime_overlay_advert_event(
keys: &nostr::Keys,
app: &str,
transport: OverlayTransportKind,
addr: &str,
created_at_secs: u64,
) -> Event {
let advert = OverlayAdvert {
identifier: ADVERT_IDENTIFIER.to_string(),
version: ADVERT_VERSION,
endpoints: vec![OverlayEndpointAdvert {
transport,
addr: addr.to_string(),
}],
signal_relays: None,
stun_servers: None,
};
EventBuilder::new(
Kind::Custom(ADVERT_KIND),
serde_json::to_string(&advert).unwrap(),
)
.tags([
Tag::identifier(app),
Tag::custom(TagKind::custom("protocol"), [app.to_string()]),
Tag::custom(TagKind::custom("version"), [PROTOCOL_VERSION.to_string()]),
Tag::expiration(Timestamp::from(created_at_secs.saturating_add(3_600))),
])
.custom_created_at(Timestamp::from(created_at_secs))
.sign_with_keys(keys)
.unwrap()
}
#[tokio::test]
async fn mesh_signal_channel_roundtrips_offer() {
let discovery = NostrDiscovery::new_for_test();
let offer = TraversalOffer {
message_type: "offer".to_string(),
session_id: "session".to_string(),
issued_at: 1,
expires_at: 2,
nonce: "nonce".to_string(),
sender_npub: discovery.npub.clone(),
recipient_npub: "npub1peer".to_string(),
reflexive_address: None,
local_addresses: Vec::new(),
stun_server: None,
};
assert!(
discovery
.emit_mesh_signal(MeshTraversalSignal::Offer {
peer_npub: "npub1peer".to_string(),
offer: offer.clone(),
})
.await
);
let signals = discovery.drain_mesh_signals().await;
assert_eq!(signals.len(), 1);
match &signals[0] {
MeshTraversalSignal::Offer {
peer_npub,
offer: got,
} => {
assert_eq!(peer_npub, "npub1peer");
assert_eq!(got, &offer);
}
MeshTraversalSignal::Answer { .. } => panic!("expected mesh offer"),
}
}
#[tokio::test]
async fn mesh_answer_resolves_pending_offer_without_nostr_event() {
let discovery = NostrDiscovery::new_for_test();
let (tx, rx) = oneshot::channel();
discovery
.pending_answers
.lock()
.await
.insert("offer-nonce".to_string(), tx);
let answer = TraversalAnswer {
message_type: "answer".to_string(),
session_id: "session".to_string(),
issued_at: 1,
expires_at: 2,
nonce: "answer-nonce".to_string(),
sender_npub: "npub1peer".to_string(),
recipient_npub: discovery.npub.clone(),
in_reply_to: "offer-nonce".to_string(),
accepted: true,
reflexive_address: None,
local_addresses: vec![TraversalAddress {
protocol: "udp".to_string(),
ip: "127.0.0.1".to_string(),
port: 51820,
}],
stun_server: None,
punch: None,
reason: None,
offer_received_at: None,
};
discovery
.receive_mesh_traversal_answer(answer.clone(), "npub1peer".to_string())
.await;
let envelope = rx.await.expect("pending answer should resolve");
assert_eq!(envelope.payload, answer);
assert!(envelope.event_id.is_none());
assert_eq!(envelope.sender_npub, "npub1peer");
}