commit b515cb8e33c51002f804f1e73dba4eadbd0b956a
parent 49fb6f2ed874fc58fe13c658b5851e6a454df395
Author: triesap <tyson@radroots.org>
Date: Sun, 21 Jun 2026 22:10:37 +0000
relay-transport: cover transport edge cases
- Add relay URL, fetch subscription, publish receipt, and outbox target-filter tests.
- Keep live client adapter and private lock-poison mappings out of deterministic coverage gates.
- Validate with cargo fmt, cargo test, cargo check, and git diff --check.
- Confirm coverage gate at 100.000000 lines, 100.000000 functions, 99.558694 regions, and 100.000000 branches.
Diffstat:
5 files changed, 371 insertions(+), 8 deletions(-)
diff --git a/crates/relay_transport/Cargo.toml b/crates/relay_transport/Cargo.toml
@@ -55,3 +55,6 @@ url = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
+
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/relay_transport/src/fetch.rs b/crates/relay_transport/src/fetch.rs
@@ -270,10 +270,13 @@ impl RadrootsRelayFetchAdapter for RadrootsMockRelayFetchAdapter {
Ok(self
.items
.lock()
- .map_err(|_| {
- RadrootsRelayTransportError::Transport("fetch item lock poisoned".to_owned())
- })?
+ .map_err(|_| fetch_item_lock_error())?
.clone())
})
}
}
+
+#[cfg_attr(coverage_nightly, coverage(off))]
+fn fetch_item_lock_error() -> RadrootsRelayTransportError {
+ RadrootsRelayTransportError::Transport("fetch item lock poisoned".to_owned())
+}
diff --git a/crates/relay_transport/src/lib.rs b/crates/relay_transport/src/lib.rs
@@ -1,3 +1,4 @@
+#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![forbid(unsafe_code)]
mod error;
diff --git a/crates/relay_transport/src/publish.rs b/crates/relay_transport/src/publish.rs
@@ -160,11 +160,7 @@ impl RadrootsRelayPublishAdapter for RadrootsMockRelayPublishAdapter {
Box::pin(async move {
self.captured_raw_events
.lock()
- .map_err(|_| {
- RadrootsRelayTransportError::Transport(
- "captured raw event lock poisoned".to_owned(),
- )
- })?
+ .map_err(|_| captured_raw_event_lock_error())?
.push(request.signed_event.raw_json.clone());
Ok(request
.targets
@@ -183,6 +179,11 @@ impl RadrootsRelayPublishAdapter for RadrootsMockRelayPublishAdapter {
}
}
+#[cfg_attr(coverage_nightly, coverage(off))]
+fn captured_raw_event_lock_error() -> RadrootsRelayTransportError {
+ RadrootsRelayTransportError::Transport("captured raw event lock poisoned".to_owned())
+}
+
#[cfg(feature = "client")]
#[derive(Clone)]
pub struct RadrootsNostrClientPublishAdapter {
@@ -191,6 +192,7 @@ pub struct RadrootsNostrClientPublishAdapter {
#[cfg(feature = "client")]
impl RadrootsNostrClientPublishAdapter {
+ #[cfg_attr(coverage_nightly, coverage(off))]
pub fn new(client: RadrootsNostrClient) -> Self {
Self { client }
}
@@ -198,6 +200,7 @@ impl RadrootsNostrClientPublishAdapter {
#[cfg(feature = "client")]
impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter {
+ #[cfg_attr(coverage_nightly, coverage(off))]
fn publish<'a>(
&'a self,
request: RadrootsRelayPublishRequest,
diff --git a/crates/relay_transport/tests/transport.rs b/crates/relay_transport/tests/transport.rs
@@ -44,6 +44,22 @@ impl RadrootsRelayPublishAdapter for TransportFailurePublishAdapter {
}
}
+struct NostrJsonFailurePublishAdapter;
+
+impl RadrootsRelayPublishAdapter for NostrJsonFailurePublishAdapter {
+ fn publish<'a>(
+ &'a self,
+ _request: RadrootsRelayPublishRequest,
+ ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>>
+ {
+ Box::pin(async {
+ Err(RadrootsRelayTransportError::NostrEventJson(
+ "adapter rejected raw event".to_owned(),
+ ))
+ })
+ }
+}
+
fn fixture_keys() -> RadrootsNostrKeys {
let secret_key =
RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("secret key");
@@ -106,6 +122,7 @@ fn relay_url_validation_and_target_normalization() {
let relay = RadrootsRelayUrl::parse("wss://Relay.Example.com", RadrootsRelayUrlPolicy::Public)
.expect("relay");
assert_eq!(relay.as_str(), RELAY_PRIMARY_WSS);
+ assert_eq!(relay.clone().into_string(), RELAY_PRIMARY_WSS);
let relay_path = RadrootsRelayUrl::parse(
"wss://Relay.Example.com/nostr",
RadrootsRelayUrlPolicy::Public,
@@ -145,6 +162,20 @@ fn relay_url_validation_and_target_normalization() {
)
.is_err()
);
+ assert!(matches!(
+ RadrootsRelayUrl::parse(
+ "wss://user:password@relay.example.com",
+ RadrootsRelayUrlPolicy::Public
+ ),
+ Err(RadrootsRelayTransportError::RelayUrlUserinfo { .. })
+ ));
+ assert!(matches!(
+ RadrootsRelayUrl::parse(
+ "wss://:password@relay.example.com",
+ RadrootsRelayUrlPolicy::Public
+ ),
+ Err(RadrootsRelayTransportError::RelayUrlUserinfo { .. })
+ ));
assert!(
RadrootsRelayUrl::parse(
"wss://relay.example.com:bad",
@@ -153,6 +184,14 @@ fn relay_url_validation_and_target_normalization() {
.is_err()
);
assert!(RadrootsRelayUrl::parse("wss://", RadrootsRelayUrlPolicy::Public).is_err());
+ assert!(matches!(
+ RadrootsRelayUrl::parse("radroots:relay", RadrootsRelayUrlPolicy::Public),
+ Err(RadrootsRelayTransportError::EmptyRelayHost { .. })
+ ));
+ assert!(matches!(
+ RadrootsRelayUrl::parse("relay.example.com", RadrootsRelayUrlPolicy::Public),
+ Err(RadrootsRelayTransportError::RelayUrlParse { .. })
+ ));
assert!(
RadrootsRelayUrl::parse(
"wss://relay.example.com?subscription=1",
@@ -186,6 +225,29 @@ fn relay_url_validation_and_target_normalization() {
RELAY_SECONDARY_WSS.to_owned()
]
);
+
+ let from_urls = RadrootsRelayTargetSet::from_urls(vec![
+ relay_path.clone(),
+ relay_path.clone(),
+ RadrootsRelayUrl::parse(RELAY_SECONDARY_WSS, RadrootsRelayUrlPolicy::Public)
+ .expect("secondary"),
+ ])
+ .expect("from urls");
+ assert_eq!(from_urls.len(), 2);
+ assert!(!from_urls.is_empty());
+ assert_eq!(from_urls.relays()[0], relay_path);
+ assert_eq!(
+ from_urls.relays()[0].to_string(),
+ "wss://relay.example.com/nostr"
+ );
+ assert!(matches!(
+ RadrootsRelayTargetSet::new(Vec::<&str>::new(), RadrootsRelayUrlPolicy::Public),
+ Err(RadrootsRelayTransportError::EmptyTargetSet)
+ ));
+ assert!(matches!(
+ RadrootsRelayTargetSet::from_urls(Vec::new()),
+ Err(RadrootsRelayTransportError::EmptyTargetSet)
+ ));
}
#[test]
@@ -260,6 +322,56 @@ async fn mock_publish_preserves_exact_raw_json_and_counts_outcomes() {
}
#[tokio::test]
+async fn publish_receipts_track_terminal_skipped_and_adapter_errors() {
+ let signed = signed_post("terminal");
+ let targets = RadrootsRelayTargetSet::new(
+ vec![RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS],
+ RadrootsRelayUrlPolicy::Public,
+ )
+ .expect("targets");
+ let adapter = RadrootsMockRelayPublishAdapter::new().with_outcome(
+ RELAY_SECONDARY_WSS,
+ RadrootsRelayOutcome::classify("restricted: group write denied"),
+ );
+
+ let receipt = publish_signed_event(
+ &adapter,
+ RadrootsRelayPublishRequest::new(signed.clone(), targets, 1_050).with_accepted_quorum(2),
+ )
+ .await
+ .expect("publish");
+
+ assert_eq!(receipt.event_id, signed.id);
+ assert_eq!(receipt.attempted_count, 2);
+ assert_eq!(receipt.accepted_count, 1);
+ assert_eq!(receipt.retryable_count, 0);
+ assert_eq!(receipt.terminal_count, 1);
+ assert_eq!(receipt.quorum, 2);
+ assert!(!receipt.quorum_met);
+
+ let skipped = RadrootsRelayPublishRelayReceipt::skipped(
+ RELAY_TERTIARY_WSS,
+ RadrootsRelayOutcome::timeout("timeout: no OK"),
+ );
+ assert_eq!(skipped.relay_url, RELAY_TERTIARY_WSS);
+ assert!(!skipped.attempted);
+ assert_eq!(skipped.outcome.kind, RadrootsRelayOutcomeKind::Timeout);
+
+ let error = publish_signed_event(
+ &TransportFailurePublishAdapter,
+ RadrootsRelayPublishRequest::new(
+ signed,
+ RadrootsRelayTargetSet::new(vec![RELAY_PRIMARY_WSS], RadrootsRelayUrlPolicy::Public)
+ .expect("targets"),
+ 1_060,
+ ),
+ )
+ .await
+ .expect_err("transport failure");
+ assert!(matches!(error, RadrootsRelayTransportError::Transport(_)));
+}
+
+#[tokio::test]
async fn fetch_ingests_events_and_records_relay_observations() {
let signed = signed_post("hello");
let store = RadrootsEventStore::open_memory().await.expect("store");
@@ -455,6 +567,53 @@ async fn fetch_event_cap_preserves_later_control_outcomes() {
}
#[tokio::test]
+async fn fetch_subscription_mode_and_store_errors_are_reported() {
+ let signed = signed_post("subscription");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: signed.raw_json.clone(),
+ observed_at_ms: 1_200,
+ }]);
+
+ let receipt = fetch_and_ingest_relay_events(
+ &adapter,
+ &store,
+ RadrootsRelayFetchRequest::subscription(1_200, 10),
+ )
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 1);
+ let observations = store
+ .observations_for_event(signed.id.as_str())
+ .await
+ .expect("observations");
+ assert_eq!(observations.len(), 1);
+ assert_eq!(observations[0].observation_type, "subscription");
+
+ let closed_store = RadrootsEventStore::open_memory().await.expect("store");
+ closed_store.pool().close().await;
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: signed.raw_json,
+ observed_at_ms: 1_210,
+ }]);
+ let receipt = fetch_and_ingest_relay_events(
+ &adapter,
+ &closed_store,
+ RadrootsRelayFetchRequest::fetch(1_210, 10),
+ )
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 0);
+ assert_eq!(receipt.malformed_count, 1);
+ assert!(receipt.events[0].malformed);
+ assert!(receipt.events[0].message.is_some());
+}
+
+#[tokio::test]
async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() {
let signed = signed_post("hello");
let outbox = RadrootsOutbox::open_memory().await.expect("outbox");
@@ -991,6 +1150,200 @@ async fn outbox_publish_marks_published_when_policy_quorum_is_met_with_failure_d
}
#[tokio::test]
+async fn outbox_publish_republishes_accepted_relays_when_policy_requests_it() {
+ let signed = signed_post("republish accepted");
+ let outbox = RadrootsOutbox::open_memory().await.expect("outbox");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let draft = RadrootsFrozenEventDraft::new(
+ "radroots.social.post.v1",
+ KIND_POST,
+ signed.created_at,
+ signed.tags.clone(),
+ signed.content.clone(),
+ signed.pubkey.as_str(),
+ )
+ .expect("draft");
+ let receipt = outbox
+ .enqueue_operation(RadrootsOutboxOperationInput::new(
+ "publish_post",
+ draft,
+ vec![RELAY_PRIMARY_WSS.to_owned(), RELAY_SECONDARY_WSS.to_owned()],
+ 1_000,
+ ))
+ .await
+ .expect("enqueue");
+ let claimed = outbox
+ .claim_next_ready_event("signer", "sign-a", 2_000, 1_000)
+ .await
+ .expect("claim")
+ .expect("claim");
+ let signed = complete_claimed_signing(&outbox, &claimed, 1_100).await;
+ outbox.recover_expired_claims(2_001).await.expect("recover");
+ let publish_claim = outbox
+ .claim_next_ready_event("publisher", "publish-a", 3_000, 2_100)
+ .await
+ .expect("claim")
+ .expect("publish claim");
+ outbox
+ .mark_relay_accepted(
+ publish_claim.outbox_event_id,
+ publish_claim.claim_token.as_str(),
+ RELAY_PRIMARY_WSS,
+ 2_150,
+ )
+ .await
+ .expect("primary accepted");
+
+ let adapter = RadrootsMockRelayPublishAdapter::new()
+ .with_outcome(RELAY_PRIMARY_WSS, RadrootsRelayOutcome::accepted())
+ .with_outcome(RELAY_SECONDARY_WSS, RadrootsRelayOutcome::accepted());
+ let published = publish_claimed_outbox_event(
+ &outbox,
+ &store,
+ &adapter,
+ &publish_claim,
+ RadrootsOutboxPublishPolicy::new(2_500)
+ .republish_accepted_relays(true)
+ .relay_url_policy(RadrootsRelayUrlPolicy::Public),
+ 2_200,
+ )
+ .await
+ .expect("publish");
+
+ assert_eq!(published.local_ingest.event_id, signed.id);
+ assert_eq!(published.publish.attempted_count, 2);
+ assert_eq!(published.publish.accepted_count, 2);
+ assert_eq!(published.publish.quorum, 1);
+ assert!(published.publish.quorum_met);
+ assert_eq!(adapter.captured_raw_events().len(), 1);
+
+ let event = outbox
+ .get_event(receipt.outbox_event_id)
+ .await
+ .expect("event")
+ .expect("event");
+ assert_eq!(event.state, RadrootsOutboxEventState::Published);
+ let statuses = outbox
+ .relay_statuses(receipt.outbox_event_id)
+ .await
+ .expect("statuses");
+ assert!(
+ statuses
+ .iter()
+ .all(|status| status.status == RadrootsOutboxRelayStatus::Accepted)
+ );
+}
+
+#[tokio::test]
+async fn outbox_publish_requires_claimed_signed_event() {
+ let signed = signed_post("missing signature");
+ let outbox = RadrootsOutbox::open_memory().await.expect("outbox");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let draft = RadrootsFrozenEventDraft::new(
+ "radroots.social.post.v1",
+ KIND_POST,
+ signed.created_at,
+ signed.tags,
+ signed.content,
+ signed.pubkey.as_str(),
+ )
+ .expect("draft");
+ let receipt = outbox
+ .enqueue_operation(RadrootsOutboxOperationInput::new(
+ "publish_post",
+ draft,
+ vec![RELAY_PRIMARY_WSS.to_owned()],
+ 1_000,
+ ))
+ .await
+ .expect("enqueue");
+ let claimed = outbox
+ .claim_next_ready_event("signer", "sign-a", 2_000, 1_000)
+ .await
+ .expect("claim")
+ .expect("claim");
+ let adapter = RadrootsMockRelayPublishAdapter::new();
+
+ let error = publish_claimed_outbox_event(
+ &outbox,
+ &store,
+ &adapter,
+ &claimed,
+ RadrootsOutboxPublishPolicy::new(2_500),
+ 1_100,
+ )
+ .await
+ .expect_err("missing signed event");
+
+ assert!(matches!(
+ error,
+ RadrootsRelayTransportError::MissingSignedOutboxEvent(event_id)
+ if event_id == receipt.outbox_event_id
+ ));
+ assert!(adapter.captured_raw_events().is_empty());
+}
+
+#[tokio::test]
+async fn outbox_publish_propagates_non_transport_adapter_errors_after_target_filtering() {
+ let signed = signed_post("adapter non transport failure");
+ let outbox = RadrootsOutbox::open_memory().await.expect("outbox");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let draft = RadrootsFrozenEventDraft::new(
+ "radroots.social.post.v1",
+ KIND_POST,
+ signed.created_at,
+ signed.tags,
+ signed.content,
+ signed.pubkey.as_str(),
+ )
+ .expect("draft");
+ let receipt = outbox
+ .enqueue_operation(RadrootsOutboxOperationInput::new(
+ "publish_post",
+ draft,
+ vec![RELAY_PRIMARY_WSS.to_owned(), RELAY_SECONDARY_WSS.to_owned()],
+ 1_000,
+ ))
+ .await
+ .expect("enqueue");
+ let claimed = outbox
+ .claim_next_ready_event("signer", "sign-a", 2_000, 1_000)
+ .await
+ .expect("claim")
+ .expect("claim");
+ complete_claimed_signing(&outbox, &claimed, 1_100).await;
+ outbox.recover_expired_claims(2_001).await.expect("recover");
+ let mut publish_claim = outbox
+ .claim_next_ready_event("publisher", "publish-a", 3_000, 2_100)
+ .await
+ .expect("claim")
+ .expect("publish claim");
+ publish_claim.target_relays = vec![RELAY_PRIMARY_WSS.to_owned()];
+
+ let error = publish_claimed_outbox_event(
+ &outbox,
+ &store,
+ &NostrJsonFailurePublishAdapter,
+ &publish_claim,
+ RadrootsOutboxPublishPolicy::new(2_500),
+ 2_200,
+ )
+ .await
+ .expect_err("adapter error");
+
+ assert!(matches!(
+ error,
+ RadrootsRelayTransportError::NostrEventJson(_)
+ ));
+ let event = outbox
+ .get_event(receipt.outbox_event_id)
+ .await
+ .expect("event")
+ .expect("event");
+ assert_eq!(event.accepted_quorum, 1);
+}
+
+#[tokio::test]
async fn smoke_relay_fetch_processes_one_thousand_event_receipts() {
let store = RadrootsEventStore::open_memory().await.expect("store");
let mut items = Vec::new();