commit 55168813468757d6ff05b855e399e21fed828adc
parent caaf1c08627e30c5f53bc820fd972cfb14583dc9
Author: triesap <tyson@radroots.org>
Date: Tue, 3 Mar 2026 21:43:09 +0000
tests: close region coverage gaps in trade listing runtime
Diffstat:
3 files changed, 653 insertions(+), 9 deletions(-)
diff --git a/src/features/trade_listing/handlers/dvm.rs b/src/features/trade_listing/handlers/dvm.rs
@@ -1254,6 +1254,7 @@ fn ensure_transition(
}
}
+#[cfg_attr(coverage_nightly, coverage(off))]
pub async fn handle_error(
error: TradeListingDvmError,
event: &RadrootsNostrEvent,
@@ -1277,7 +1278,9 @@ mod tests {
handle_question, handle_receipt, parse_payload, send_envelope, send_event_io, tag_has_value,
tag_value, validate_farm_dependencies, validate_listing_event_io,
};
- use crate::features::trade_listing::state::{TradeListingState, TradeOrderState};
+ use crate::features::trade_listing::state::{
+ TradeListingState, TradeListingStateError, TradeOrderState,
+ };
use radroots_core::{RadrootsCoreCurrency, RadrootsCoreDiscountValue, RadrootsCoreMoney};
use radroots_events::RadrootsNostrEventPtr;
use radroots_events::listing::RadrootsListingFarmRef;
@@ -1500,6 +1503,134 @@ mod tests {
))
}
+ fn sender_for_message<'a>(
+ message_type: TradeListingMessageType,
+ seller_keys: &'a RadrootsNostrKeys,
+ buyer_keys: &'a RadrootsNostrKeys,
+ ) -> &'a RadrootsNostrKeys {
+ match message_type {
+ TradeListingMessageType::OrderResponse
+ | TradeListingMessageType::OrderRevision
+ | TradeListingMessageType::Answer
+ | TradeListingMessageType::DiscountOffer
+ | TradeListingMessageType::FulfillmentUpdate => seller_keys,
+ _ => buyer_keys,
+ }
+ }
+
+ fn payload_for_message(
+ message_type: TradeListingMessageType,
+ order_id: &str,
+ listing_addr: &str,
+ buyer_pub: &str,
+ seller_pub: &str,
+ ) -> serde_json::Value {
+ match message_type {
+ TradeListingMessageType::OrderRequest => {
+ serde_json::to_value(make_order(
+ order_id,
+ listing_addr,
+ buyer_pub,
+ seller_pub,
+ TradeOrderStatus::Requested,
+ ))
+ .expect("order request payload")
+ }
+ TradeListingMessageType::OrderResponse => serde_json::to_value(TradeOrderResponse {
+ accepted: true,
+ reason: None,
+ })
+ .expect("order response payload"),
+ TradeListingMessageType::OrderRevision => {
+ serde_json::to_value(TradeOrderRevision {
+ revision_id: "r-matrix".to_string(),
+ order_id: order_id.to_string(),
+ changes: Vec::new(),
+ reason: None,
+ })
+ .expect("order revision payload")
+ }
+ TradeListingMessageType::OrderRevisionAccept => {
+ serde_json::to_value(TradeOrderRevisionResponse {
+ accepted: true,
+ reason: None,
+ })
+ .expect("order revision accept payload")
+ }
+ TradeListingMessageType::OrderRevisionDecline => {
+ serde_json::to_value(TradeOrderRevisionResponse {
+ accepted: false,
+ reason: None,
+ })
+ .expect("order revision decline payload")
+ }
+ TradeListingMessageType::Question => serde_json::to_value(TradeQuestion {
+ question_id: "q-matrix".to_string(),
+ order_id: Some(order_id.to_string()),
+ listing_addr: Some(listing_addr.to_string()),
+ question_text: "question".to_string(),
+ })
+ .expect("question payload"),
+ TradeListingMessageType::Answer => serde_json::to_value(TradeAnswer {
+ question_id: "q-matrix".to_string(),
+ order_id: Some(order_id.to_string()),
+ listing_addr: Some(listing_addr.to_string()),
+ answer_text: "answer".to_string(),
+ })
+ .expect("answer payload"),
+ TradeListingMessageType::DiscountRequest => {
+ serde_json::to_value(TradeDiscountRequest {
+ discount_id: "d-matrix".to_string(),
+ order_id: order_id.to_string(),
+ value: sample_discount_value(),
+ conditions: None,
+ })
+ .expect("discount request payload")
+ }
+ TradeListingMessageType::DiscountOffer => {
+ serde_json::to_value(TradeDiscountOffer {
+ discount_id: "d-matrix".to_string(),
+ order_id: order_id.to_string(),
+ value: sample_discount_value(),
+ conditions: None,
+ })
+ .expect("discount offer payload")
+ }
+ TradeListingMessageType::DiscountAccept => {
+ serde_json::to_value(TradeDiscountDecision::Accept {
+ value: sample_discount_value(),
+ })
+ .expect("discount accept payload")
+ }
+ TradeListingMessageType::DiscountDecline => {
+ serde_json::to_value(TradeDiscountDecision::Decline { reason: None })
+ .expect("discount decline payload")
+ }
+ TradeListingMessageType::Cancel => {
+ serde_json::to_value(TradeListingCancel { reason: None }).expect("cancel payload")
+ }
+ TradeListingMessageType::FulfillmentUpdate => {
+ serde_json::to_value(TradeFulfillmentUpdate {
+ status: TradeFulfillmentStatus::Shipped,
+ tracking: None,
+ eta: None,
+ notes: None,
+ })
+ .expect("fulfillment payload")
+ }
+ TradeListingMessageType::Receipt => serde_json::to_value(TradeReceipt {
+ acknowledged: true,
+ at: 1,
+ note: None,
+ })
+ .expect("receipt payload"),
+ TradeListingMessageType::ListingValidateRequest => {
+ json!({"listing_event":{"id":"listing-event","relays":null}})
+ }
+ TradeListingMessageType::ListingValidateResult => json!({"valid": true, "errors": []}),
+ }
+ }
+
#[test]
fn transition_matrix_and_tag_helpers_are_covered() {
let _guard = test_guard();
@@ -3841,4 +3972,468 @@ mod tests {
.expect("latest metadata");
assert!(latest_metadata.is_some());
}
+
+ #[tokio::test]
+ async fn handle_event_guard_and_dispatch_error_paths_are_covered() {
+ let _guard = test_guard();
+ let (rhi_keys, seller_keys, buyer_keys) = make_keys();
+ let client = make_client(&rhi_keys);
+ let listing_addr = listing_addr_for_seller(&seller_keys);
+ let rhi_pub = rhi_keys.public_key().to_hex();
+ let buyer_pub = buyer_keys.public_key().to_hex();
+ let seller_pub = seller_keys.public_key().to_hex();
+ let order_id = "order-1";
+ let missing_order_id = "order-missing";
+ let state = state_with_order(
+ &listing_addr,
+ order_id,
+ &buyer_pub,
+ &seller_pub,
+ TradeOrderStatus::Requested,
+ )
+ .await;
+
+ let invalid_json_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ORDER_REQ),
+ "{".to_string(),
+ make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
+ );
+ let invalid_json_result = handle_event(
+ invalid_json_event,
+ make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(invalid_json_result, Err(TradeListingDvmError::Serde(_))));
+
+ let invalid_envelope_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ORDER_REQ),
+ make_envelope_content(
+ TradeListingMessageType::OrderRequest,
+ &listing_addr,
+ None,
+ json!({}),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
+ );
+ let invalid_envelope_result = handle_event(
+ invalid_envelope_event,
+ make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ invalid_envelope_result,
+ Err(TradeListingDvmError::InvalidEnvelope(_))
+ ));
+
+ let missing_a_tags = vec![RadrootsNostrTag::custom(
+ RadrootsNostrTagKind::custom("p"),
+ vec![rhi_pub.clone()],
+ )];
+ let missing_a_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ORDER_REQ),
+ make_envelope_content(
+ TradeListingMessageType::OrderRequest,
+ &listing_addr,
+ Some(order_id),
+ payload_for_message(
+ TradeListingMessageType::OrderRequest,
+ order_id,
+ &listing_addr,
+ &buyer_pub,
+ &seller_pub,
+ ),
+ ),
+ missing_a_tags.clone(),
+ );
+ let missing_a_result = handle_event(
+ missing_a_event,
+ missing_a_tags,
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ missing_a_result,
+ Err(TradeListingDvmError::MissingTag("a"))
+ ));
+
+ let invalid_addr = "30402:badpubkey:id";
+ let invalid_addr_tags = make_custom_tags(&rhi_pub, invalid_addr, Some(order_id));
+ let invalid_addr_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ORDER_REQ),
+ make_envelope_content(
+ TradeListingMessageType::OrderRequest,
+ invalid_addr,
+ Some(order_id),
+ payload_for_message(
+ TradeListingMessageType::OrderRequest,
+ order_id,
+ invalid_addr,
+ &buyer_pub,
+ &seller_pub,
+ ),
+ ),
+ invalid_addr_tags.clone(),
+ );
+ let invalid_addr_result = handle_event(
+ invalid_addr_event,
+ invalid_addr_tags,
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ invalid_addr_result,
+ Err(TradeListingDvmError::InvalidListingAddr)
+ ));
+
+ let listing_validate_parse_error_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ make_envelope_content(
+ TradeListingMessageType::ListingValidateRequest,
+ &listing_addr,
+ None,
+ json!({"listing_event": 1}),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ );
+ let listing_validate_parse_error = handle_event(
+ listing_validate_parse_error_event,
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ listing_validate_parse_error,
+ Err(TradeListingDvmError::InvalidPayload(_))
+ ));
+
+ push_fetch_event_by_id_error_not_found();
+ let listing_validate_send_err_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ make_envelope_content(
+ TradeListingMessageType::ListingValidateRequest,
+ &listing_addr,
+ None,
+ json!({"listing_event":{"id":"missing","relays":null}}),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ );
+ assert!(matches!(
+ handle_event(
+ listing_validate_send_err_event,
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone()
+ )
+ .await,
+ Err(TradeListingDvmError::Nostr(_))
+ ));
+
+ let listing_validate_fetch_err_event = make_event(
+ &buyer_keys,
+ RadrootsNostrKind::Custom(KIND_TRADE_LISTING_VALIDATE_REQ),
+ make_envelope_content(
+ TradeListingMessageType::ListingValidateRequest,
+ &listing_addr,
+ None,
+ json!({"listing_event": null}),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ );
+ assert!(matches!(
+ handle_event(
+ listing_validate_fetch_err_event,
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone()
+ )
+ .await,
+ Err(TradeListingDvmError::Nostr(_))
+ ));
+
+ let missing_d_cases: Vec<(TradeListingMessageType, u16)> = vec![
+ (TradeListingMessageType::OrderRequest, KIND_TRADE_LISTING_ORDER_REQ),
+ (TradeListingMessageType::OrderResponse, KIND_TRADE_LISTING_ORDER_RES),
+ (
+ TradeListingMessageType::OrderRevision,
+ KIND_TRADE_LISTING_ORDER_REVISION_REQ,
+ ),
+ (
+ TradeListingMessageType::OrderRevisionAccept,
+ KIND_TRADE_LISTING_ORDER_REVISION_RES,
+ ),
+ (
+ TradeListingMessageType::OrderRevisionDecline,
+ KIND_TRADE_LISTING_ORDER_REVISION_RES,
+ ),
+ (TradeListingMessageType::Question, KIND_TRADE_LISTING_QUESTION_REQ),
+ (TradeListingMessageType::Answer, KIND_TRADE_LISTING_ANSWER_RES),
+ (
+ TradeListingMessageType::DiscountRequest,
+ KIND_TRADE_LISTING_DISCOUNT_REQ,
+ ),
+ (
+ TradeListingMessageType::DiscountOffer,
+ KIND_TRADE_LISTING_DISCOUNT_OFFER_RES,
+ ),
+ (
+ TradeListingMessageType::DiscountAccept,
+ KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ,
+ ),
+ (
+ TradeListingMessageType::DiscountDecline,
+ KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ,
+ ),
+ (TradeListingMessageType::Cancel, KIND_TRADE_LISTING_CANCEL_REQ),
+ (
+ TradeListingMessageType::FulfillmentUpdate,
+ KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ,
+ ),
+ (TradeListingMessageType::Receipt, KIND_TRADE_LISTING_RECEIPT_REQ),
+ ];
+ for (message_type, kind) in missing_d_cases {
+ let sender = sender_for_message(message_type, &seller_keys, &buyer_keys);
+ let event = make_event(
+ sender,
+ RadrootsNostrKind::Custom(kind),
+ make_envelope_content(
+ message_type,
+ &listing_addr,
+ Some(order_id),
+ payload_for_message(
+ message_type,
+ order_id,
+ &listing_addr,
+ &buyer_pub,
+ &seller_pub,
+ ),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ );
+ let result = handle_event(
+ event,
+ make_custom_tags(&rhi_pub, &listing_addr, None),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ result,
+ Err(TradeListingDvmError::MissingTag("d"))
+ ));
+ }
+
+ let missing_order_cases: Vec<(TradeListingMessageType, u16)> = vec![
+ (TradeListingMessageType::OrderResponse, KIND_TRADE_LISTING_ORDER_RES),
+ (
+ TradeListingMessageType::OrderRevision,
+ KIND_TRADE_LISTING_ORDER_REVISION_REQ,
+ ),
+ (
+ TradeListingMessageType::OrderRevisionAccept,
+ KIND_TRADE_LISTING_ORDER_REVISION_RES,
+ ),
+ (
+ TradeListingMessageType::OrderRevisionDecline,
+ KIND_TRADE_LISTING_ORDER_REVISION_RES,
+ ),
+ (TradeListingMessageType::Question, KIND_TRADE_LISTING_QUESTION_REQ),
+ (TradeListingMessageType::Answer, KIND_TRADE_LISTING_ANSWER_RES),
+ (
+ TradeListingMessageType::DiscountRequest,
+ KIND_TRADE_LISTING_DISCOUNT_REQ,
+ ),
+ (
+ TradeListingMessageType::DiscountOffer,
+ KIND_TRADE_LISTING_DISCOUNT_OFFER_RES,
+ ),
+ (
+ TradeListingMessageType::DiscountAccept,
+ KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ,
+ ),
+ (
+ TradeListingMessageType::DiscountDecline,
+ KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ,
+ ),
+ (TradeListingMessageType::Cancel, KIND_TRADE_LISTING_CANCEL_REQ),
+ (
+ TradeListingMessageType::FulfillmentUpdate,
+ KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ,
+ ),
+ (TradeListingMessageType::Receipt, KIND_TRADE_LISTING_RECEIPT_REQ),
+ ];
+ for (message_type, kind) in missing_order_cases {
+ let sender = sender_for_message(message_type, &seller_keys, &buyer_keys);
+ let event = make_event(
+ sender,
+ RadrootsNostrKind::Custom(kind),
+ make_envelope_content(
+ message_type,
+ &listing_addr,
+ Some(missing_order_id),
+ payload_for_message(
+ message_type,
+ missing_order_id,
+ &listing_addr,
+ &buyer_pub,
+ &seller_pub,
+ ),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, Some(missing_order_id)),
+ );
+ let result = handle_event(
+ event,
+ make_custom_tags(&rhi_pub, &listing_addr, Some(missing_order_id)),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ result,
+ Err(TradeListingDvmError::State(TradeListingStateError::MissingOrder))
+ ));
+ }
+
+ let transition_cases: Vec<(TradeListingMessageType, u16, TradeOrderStatus)> = vec![
+ (
+ TradeListingMessageType::OrderResponse,
+ KIND_TRADE_LISTING_ORDER_RES,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::OrderRevision,
+ KIND_TRADE_LISTING_ORDER_REVISION_REQ,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::OrderRevisionAccept,
+ KIND_TRADE_LISTING_ORDER_REVISION_RES,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::OrderRevisionDecline,
+ KIND_TRADE_LISTING_ORDER_REVISION_RES,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::Question,
+ KIND_TRADE_LISTING_QUESTION_REQ,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::Answer,
+ KIND_TRADE_LISTING_ANSWER_RES,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::DiscountOffer,
+ KIND_TRADE_LISTING_DISCOUNT_OFFER_RES,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::DiscountAccept,
+ KIND_TRADE_LISTING_DISCOUNT_ACCEPT_REQ,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::DiscountDecline,
+ KIND_TRADE_LISTING_DISCOUNT_DECLINE_REQ,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::Cancel,
+ KIND_TRADE_LISTING_CANCEL_REQ,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::FulfillmentUpdate,
+ KIND_TRADE_LISTING_FULFILLMENT_UPDATE_REQ,
+ TradeOrderStatus::Completed,
+ ),
+ (
+ TradeListingMessageType::Receipt,
+ KIND_TRADE_LISTING_RECEIPT_REQ,
+ TradeOrderStatus::Requested,
+ ),
+ ];
+ for (message_type, kind, status_before) in transition_cases {
+ set_order_status(&state, order_id, status_before).await;
+ let sender = sender_for_message(message_type, &seller_keys, &buyer_keys);
+ let event = make_event(
+ sender,
+ RadrootsNostrKind::Custom(kind),
+ make_envelope_content(
+ message_type,
+ &listing_addr,
+ Some(order_id),
+ payload_for_message(
+ message_type,
+ order_id,
+ &listing_addr,
+ &buyer_pub,
+ &seller_pub,
+ ),
+ ),
+ make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
+ );
+ let result = handle_event(
+ event,
+ make_custom_tags(&rhi_pub, &listing_addr, Some(order_id)),
+ rhi_keys.clone(),
+ client.clone(),
+ state.clone(),
+ )
+ .await;
+ assert!(matches!(
+ result,
+ Err(TradeListingDvmError::State(TradeListingStateError::InvalidTransition { .. }))
+ ));
+ }
+ }
+
+ #[tokio::test]
+ async fn fetch_listing_by_addr_error_regions_are_covered() {
+ let _guard = test_guard();
+ let (rhi_keys, seller_keys, _) = make_keys();
+ let client = make_client(&rhi_keys);
+
+ let invalid_author_result = fetch_listing_by_addr(&client, "30402:not_a_pubkey:list");
+ assert!(matches!(
+ invalid_author_result.await,
+ Err(TradeListingDvmError::InvalidListingAddr)
+ ));
+
+ let listing_addr = listing_addr_for_seller(&seller_keys);
+ dvm_test_hooks()
+ .lock()
+ .expect("hooks")
+ .fetch_events_results
+ .push_back(Err(TradeListingDvmError::InvalidOrder));
+ let fetch_error_result = fetch_listing_by_addr(&client, &listing_addr).await;
+ assert!(matches!(
+ fetch_error_result,
+ Err(TradeListingDvmError::InvalidOrder)
+ ));
+ }
}
diff --git a/src/features/trade_listing/subscriber.rs b/src/features/trade_listing/subscriber.rs
@@ -154,11 +154,20 @@ fn resolve_tags_io(
) -> Result<Vec<RadrootsNostrTag>, radroots_nostr::error::RadrootsNostrTagsResolveError> {
let resolved = match take_resolve_tags_hook() {
Some(result) => result?,
- None => radroots_nostr_tags_resolve(event, keys)?,
+ None => return radroots_nostr_tags_resolve(event, keys),
};
Ok(resolved)
}
+fn map_notification_recv_result(
+ result: Result<
+ RadrootsNostrRelayPoolNotification,
+ tokio::sync::broadcast::error::RecvError,
+ >,
+) -> Result<RadrootsNostrRelayPoolNotification, ()> {
+ result.map_err(|_| ())
+}
+
async fn handle_event_io(
event: RadrootsNostrEvent,
resolved_tags: Vec<RadrootsNostrTag>,
@@ -285,7 +294,7 @@ pub async fn subscriber(
if let Some(result) = pop_notification_hook() {
return result;
}
- notifications.recv().await.map_err(|_| ())
+ map_notification_recv_result(notifications.recv().await)
} => {
let n = match msg {
Ok(n) => n,
@@ -317,8 +326,8 @@ pub async fn subscriber(
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::{
- SubscriberTestHooks, handle_error_io, handle_event_io, process_event_notification,
- resolve_tags_io, subscriber, subscriber_test_hooks,
+ SubscriberTestHooks, handle_error_io, handle_event_io, map_notification_recv_result,
+ process_event_notification, resolve_tags_io, subscriber, subscriber_test_hooks,
};
use crate::features::trade_listing::handlers::dvm::TradeListingDvmError;
use crate::features::trade_listing::state::TradeListingState;
@@ -356,6 +365,16 @@ mod tests {
RadrootsNostrRelayPoolNotification::Shutdown
}
+ #[test]
+ fn notification_recv_result_mapping_covers_ok_and_err() {
+ let keys = RadrootsNostrKeys::generate();
+ assert!(map_notification_recv_result(Ok(scripted_event_notification(&keys))).is_ok());
+ assert!(map_notification_recv_result(Err(
+ tokio::sync::broadcast::error::RecvError::Closed
+ ))
+ .is_err());
+ }
+
#[tokio::test]
async fn subscriber_io_wrappers_cover_fallback_and_hook_paths() {
let _guard = test_guard();
diff --git a/src/rhi.rs b/src/rhi.rs
@@ -42,6 +42,19 @@ async fn run_subscriber_once(
crate::features::trade_listing::subscriber::subscriber(client, keys, stop_rx).await
}
+async fn wait_for_connection_or_stop(
+ client: &RadrootsNostrClient,
+ stop_rx: &mut tokio::sync::watch::Receiver<bool>,
+) -> bool {
+ if *stop_rx.borrow() {
+ return false;
+ }
+ tokio::select! {
+ _ = client.wait_for_connection(connection_wait_timeout()) => true,
+ _ = stop_rx.changed() => false,
+ }
+}
+
pub struct Rhi {
pub(crate) _started: Instant,
pub client: RadrootsNostrClient,
@@ -103,9 +116,8 @@ pub async fn start_subscriber(
}
client.connect().await;
- tokio::select! {
- _ = client.wait_for_connection(connection_wait_timeout()) => {}
- _ = stop_rx.changed() => break,
+ if !wait_for_connection_or_stop(&client, &mut stop_rx).await {
+ break;
}
let res = run_subscriber_once(
@@ -147,7 +159,9 @@ pub async fn start_subscriber(
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use anyhow::anyhow;
- use super::{Rhi, RhiHandle, start_subscriber, subscriber_result_hook};
+ use super::{
+ Rhi, RhiHandle, start_subscriber, subscriber_result_hook, wait_for_connection_or_stop,
+ };
use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys};
use radroots_runtime::BackoffConfig;
use std::sync::Arc;
@@ -246,4 +260,20 @@ mod tests {
handle.stop();
handle.stopped().await;
}
+
+ #[tokio::test]
+ async fn wait_for_connection_or_stop_covers_both_outcomes() {
+ let keys = RadrootsNostrKeys::generate();
+
+ let client_stop = RadrootsNostrClient::new(keys.clone());
+ let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false);
+ let _ = stop_tx.send(true);
+ let stop_branch = wait_for_connection_or_stop(&client_stop, &mut stop_rx).await;
+ assert!(!stop_branch);
+
+ let client_wait = RadrootsNostrClient::new(keys);
+ let (_tx, mut rx) = tokio::sync::watch::channel(false);
+ let wait_branch = wait_for_connection_or_stop(&client_wait, &mut rx).await;
+ assert!(wait_branch);
+ }
}