lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 77040d588430ba2ad3a35f7668e0f6d7b8020def
parent 73cb8d0fc3690b250210e34ba637a1baf9faafb3
Author: triesap <tyson@radroots.org>
Date:   Sun, 21 Jun 2026 19:56:36 +0000

events-codec: expand order coverage

- Cover order encoder error mapping and message-type context preconditions.

- Add order envelope parser coverage for invalid JSON, envelope/tag mismatches, typed parser mismatches, and binding errors.

- Exercise order parse error display/source and tag-error mapping branches.

- Validate full radroots_events_codec tests, check, diff check, and refreshed coverage run.

Diffstat:
Mcrates/events_codec/src/order/decode.rs | 495+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events_codec/src/order/encode.rs | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 672 insertions(+), 7 deletions(-)

diff --git a/crates/events_codec/src/order/decode.rs b/crates/events_codec/src/order/decode.rs @@ -408,9 +408,10 @@ fn validate_order_binding<T>( #[cfg(all(test, feature = "serde_json"))] mod tests { use super::{ - RadrootsOrderEnvelopeParseError, order_cancellation_from_event, order_decision_from_event, - order_envelope_from_event, order_request_from_event, order_revision_decision_from_event, - order_revision_proposal_from_event, + RadrootsOrderEnvelopeParseError, map_tag_parse_error_for_order_envelope, + order_cancellation_from_event, order_decision_from_event, order_envelope_from_event, + order_event_context_from_tags, order_request_from_event, + order_revision_decision_from_event, order_revision_proposal_from_event, }; use crate::order::encode::{ order_cancellation_event_build, order_decision_event_build, order_request_event_build, @@ -433,10 +434,10 @@ mod tests { order::{ RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, - RadrootsOrderEnvelope, RadrootsOrderEventType, RadrootsOrderInventoryCommitment, - RadrootsOrderItem, RadrootsOrderPayloadError, RadrootsOrderPricingBasis, - RadrootsOrderRequest, RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, - RadrootsOrderRevisionProposal, + RadrootsOrderEnvelope, RadrootsOrderEnvelopeError, RadrootsOrderEventType, + RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPayloadError, + RadrootsOrderPricingBasis, RadrootsOrderRequest, RadrootsOrderRevisionDecision, + RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, }, tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, }; @@ -620,6 +621,46 @@ mod tests { } } + fn order_request_tags() -> Vec<Vec<String>> { + vec![ + vec!["p".into(), seller_pubkey_wire()], + vec!["a".into(), listing_addr_wire()], + vec![TAG_D.into(), "order-1".into()], + vec![TAG_LISTING_EVENT.into(), event_id_wire('a')], + ] + } + + fn order_chain_tags(counterparty_pubkey: String) -> Vec<Vec<String>> { + vec![ + vec!["p".into(), counterparty_pubkey], + vec!["a".into(), listing_addr_wire()], + vec![TAG_D.into(), "order-1".into()], + vec![TAG_E_ROOT.into(), event_id_wire('1')], + vec![TAG_E_PREV.into(), event_id_wire('2')], + ] + } + + fn order_event_with_envelope<T: serde::Serialize>( + kind: u32, + author: String, + message_type: RadrootsOrderEventType, + listing_addr: impl Into<String>, + order_id: impl Into<String>, + payload: &T, + tags: Vec<Vec<String>>, + ) -> RadrootsNostrEvent { + let envelope = RadrootsOrderEnvelope::new(message_type, listing_addr, order_id, payload); + RadrootsNostrEvent { + id: event_id_wire('e'), + author, + created_at: 1, + kind, + tags, + content: serde_json::to_string(&envelope).unwrap(), + sig: "sig".into(), + } + } + #[test] fn listing_address_roundtrips() { let raw = format!("30402:{}:listing-1", seller_pubkey_wire()); @@ -955,6 +996,446 @@ mod tests { assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); } + #[cfg(feature = "std")] + #[test] + fn order_parse_error_display_and_source_cover_variants() { + use std::error::Error as _; + + let invalid_envelope = RadrootsOrderEnvelopeParseError::InvalidEnvelope( + RadrootsOrderEnvelopeError::MissingOrderId, + ); + let invalid_payload = RadrootsOrderEnvelopeParseError::InvalidPayload( + RadrootsOrderPayloadError::MissingItems, + ); + let invalid_listing_addr = RadrootsOrderEnvelopeParseError::InvalidListingAddr( + RadrootsListingAddress::parse("not-a-listing-address").unwrap_err(), + ); + let errors = [ + RadrootsOrderEnvelopeParseError::InvalidKind(3431), + RadrootsOrderEnvelopeParseError::InvalidJson, + invalid_envelope.clone(), + invalid_payload.clone(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: KIND_ORDER_REQUEST, + message_type: RadrootsOrderEventType::OrderDecision, + }, + RadrootsOrderEnvelopeParseError::MissingTag("a"), + RadrootsOrderEnvelopeParseError::InvalidTag("p"), + RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch, + RadrootsOrderEnvelopeParseError::OrderIdTagMismatch, + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("order_id"), + RadrootsOrderEnvelopeParseError::AuthorMismatch, + RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch, + invalid_listing_addr.clone(), + ]; + + for error in errors { + assert!(!error.to_string().is_empty()); + } + assert!(invalid_envelope.source().is_some()); + assert!(invalid_payload.source().is_some()); + assert!(invalid_listing_addr.source().is_some()); + assert!( + RadrootsOrderEnvelopeParseError::AuthorMismatch + .source() + .is_none() + ); + } + + #[test] + fn order_envelope_parse_rejects_content_tag_and_envelope_mismatches() { + let payload = serde_json::json!({}); + let invalid_json = RadrootsNostrEvent { + id: event_id_wire('e'), + author: buyer_pubkey_wire(), + created_at: 1, + kind: KIND_ORDER_REQUEST, + tags: Vec::new(), + content: "{".into(), + sig: "sig".into(), + }; + assert_eq!( + order_envelope_from_event::<serde_json::Value>(&invalid_json).unwrap_err(), + RadrootsOrderEnvelopeParseError::InvalidJson + ); + + let mut invalid_version_envelope = RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &payload, + ); + invalid_version_envelope.version = 99; + let invalid_version = RadrootsNostrEvent { + id: event_id_wire('e'), + author: buyer_pubkey_wire(), + created_at: 1, + kind: KIND_ORDER_REQUEST, + tags: order_request_tags(), + content: serde_json::to_string(&invalid_version_envelope).unwrap(), + sig: "sig".into(), + }; + assert!(matches!( + order_envelope_from_event::<serde_json::Value>(&invalid_version).unwrap_err(), + RadrootsOrderEnvelopeParseError::InvalidEnvelope( + RadrootsOrderEnvelopeError::InvalidVersion { .. } + ) + )); + + let message_type_mismatch = order_event_with_envelope( + KIND_ORDER_REQUEST, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderDecision, + listing_addr_wire(), + "order-1", + &payload, + Vec::new(), + ); + assert_eq!( + order_envelope_from_event::<serde_json::Value>(&message_type_mismatch).unwrap_err(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { + event_kind: KIND_ORDER_REQUEST, + message_type: RadrootsOrderEventType::OrderDecision + } + ); + + let listing_addr_mismatch = order_event_with_envelope( + KIND_ORDER_REQUEST, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &payload, + vec![ + vec!["a".into(), "30402:pubkey:AAAAAAAAAAAAAAAAAAAAAg".into()], + vec![TAG_D.into(), "order-1".into()], + ], + ); + assert_eq!( + order_envelope_from_event::<serde_json::Value>(&listing_addr_mismatch).unwrap_err(), + RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch + ); + + let order_id_mismatch = order_event_with_envelope( + KIND_ORDER_REQUEST, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &payload, + vec![ + vec!["a".into(), listing_addr_wire()], + vec![TAG_D.into(), "other-order".into()], + ], + ); + assert_eq!( + order_envelope_from_event::<serde_json::Value>(&order_id_mismatch).unwrap_err(), + RadrootsOrderEnvelopeParseError::OrderIdTagMismatch + ); + + for tags in [ + Vec::<Vec<String>>::new(), + vec![vec!["a".into()]], + vec![vec!["a".into(), " ".into()]], + ] { + let event = order_event_with_envelope( + KIND_ORDER_REQUEST, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &payload, + tags, + ); + let err = order_envelope_from_event::<serde_json::Value>(&event).unwrap_err(); + assert!(matches!( + err, + RadrootsOrderEnvelopeParseError::MissingTag("a") + | RadrootsOrderEnvelopeParseError::InvalidTag("a") + )); + } + + let invalid_listing_addr = order_event_with_envelope( + KIND_ORDER_REQUEST, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderRequested, + "not-a-listing-address", + "order-1", + &payload, + vec![ + vec!["a".into(), "not-a-listing-address".into()], + vec![TAG_D.into(), "order-1".into()], + ], + ); + assert!(matches!( + order_envelope_from_event::<serde_json::Value>(&invalid_listing_addr).unwrap_err(), + RadrootsOrderEnvelopeParseError::InvalidListingAddr(_) + )); + } + + #[test] + fn order_typed_parsers_reject_message_type_mismatches() { + let request_payload = order_request(); + let decision_payload = order_decision(); + let proposal_payload = order_revision_proposal(); + let revision_decision_payload = + order_revision_decision(RadrootsOrderRevisionOutcome::Accepted); + let cancellation_payload = order_cancelled(); + + let request_as_decision = order_event_with_envelope( + KIND_ORDER_DECISION, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderDecision, + listing_addr_wire(), + "order-1", + &request_payload, + order_chain_tags(seller_pubkey_wire()), + ); + assert!(matches!( + order_request_from_event(&request_as_decision).unwrap_err(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { .. } + )); + + let decision_as_request = order_event_with_envelope( + KIND_ORDER_REQUEST, + seller_pubkey_wire(), + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &decision_payload, + order_request_tags(), + ); + assert!(matches!( + order_decision_from_event(&decision_as_request).unwrap_err(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { .. } + )); + + let proposal_as_cancellation = order_event_with_envelope( + KIND_ORDER_CANCELLATION, + seller_pubkey_wire(), + RadrootsOrderEventType::OrderCancelled, + listing_addr_wire(), + "order-1", + &proposal_payload, + order_chain_tags(buyer_pubkey_wire()), + ); + assert!(matches!( + order_revision_proposal_from_event(&proposal_as_cancellation).unwrap_err(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { .. } + )); + + let revision_decision_as_cancellation = order_event_with_envelope( + KIND_ORDER_CANCELLATION, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderCancelled, + listing_addr_wire(), + "order-1", + &revision_decision_payload, + order_chain_tags(seller_pubkey_wire()), + ); + assert!(matches!( + order_revision_decision_from_event(&revision_decision_as_cancellation).unwrap_err(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { .. } + )); + + let cancellation_as_decision = order_event_with_envelope( + KIND_ORDER_DECISION, + buyer_pubkey_wire(), + RadrootsOrderEventType::OrderDecision, + listing_addr_wire(), + "order-1", + &cancellation_payload, + order_chain_tags(seller_pubkey_wire()), + ); + assert!(matches!( + order_cancellation_from_event(&cancellation_as_decision).unwrap_err(), + RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { .. } + )); + } + + #[test] + fn order_parse_rejects_payload_and_chain_binding_mismatches() { + let mut request_payload = order_request(); + request_payload.order_id = order_id("other-order"); + let request_built = + order_request_event_build(&listing_event_ptr(), &order_request()).unwrap(); + let mut request_event = RadrootsNostrEvent { + id: event_id_wire('e'), + author: buyer_pubkey_wire(), + created_at: 1, + kind: request_built.kind, + tags: request_built.tags.clone(), + content: serde_json::to_string(&RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &request_payload, + )) + .unwrap(), + sig: "sig".into(), + }; + assert_eq!( + order_request_from_event(&request_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("order_id") + ); + + request_payload = order_request(); + request_payload.listing_addr = + format!("30402:{}:BBBBBBBBBBBBBBBBBBBBBA", seller_pubkey_wire()) + .parse() + .unwrap(); + request_event.content = serde_json::to_string(&RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &request_payload, + )) + .unwrap(); + assert_eq!( + order_request_from_event(&request_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("listing_addr") + ); + + let proposal_payload = order_revision_proposal(); + let proposal_built = order_revision_proposal_event_build( + &proposal_payload.root_event_id, + &proposal_payload.prev_event_id, + &proposal_payload, + ) + .unwrap(); + let mut proposal_event = RadrootsNostrEvent { + id: event_id_wire('e'), + author: seller_pubkey_wire(), + created_at: 1, + kind: proposal_built.kind, + tags: proposal_built.tags.clone(), + content: proposal_built.content.clone(), + sig: "sig".into(), + }; + proposal_event + .tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) + .unwrap()[1] = event_id_wire('4'); + assert_eq!( + order_revision_proposal_from_event(&proposal_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("root_event_id") + ); + + proposal_event.tags = proposal_built.tags; + proposal_event + .tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_PREV)) + .unwrap()[1] = event_id_wire('5'); + assert_eq!( + order_revision_proposal_from_event(&proposal_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("prev_event_id") + ); + + let revision_decision_payload = + order_revision_decision(RadrootsOrderRevisionOutcome::Accepted); + let revision_decision_built = order_revision_decision_event_build( + &revision_decision_payload.root_event_id, + &revision_decision_payload.prev_event_id, + &revision_decision_payload, + ) + .unwrap(); + let mut revision_decision_event = RadrootsNostrEvent { + id: event_id_wire('e'), + author: buyer_pubkey_wire(), + created_at: 1, + kind: revision_decision_built.kind, + tags: revision_decision_built.tags.clone(), + content: revision_decision_built.content, + sig: "sig".into(), + }; + revision_decision_event + .tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) + .unwrap()[1] = event_id_wire('6'); + assert_eq!( + order_revision_decision_from_event(&revision_decision_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("root_event_id") + ); + + revision_decision_event.tags = revision_decision_built.tags; + revision_decision_event + .tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_PREV)) + .unwrap()[1] = event_id_wire('7'); + assert_eq!( + order_revision_decision_from_event(&revision_decision_event).unwrap_err(), + RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("prev_event_id") + ); + } + + #[test] + fn order_event_context_and_parse_error_mapping_cover_missing_context() { + let err = order_event_context_from_tags( + RadrootsOrderEventType::OrderRequested, + &[vec!["p".into(), seller_pubkey_wire()]], + ) + .unwrap_err(); + assert_eq!( + err, + RadrootsOrderEnvelopeParseError::MissingTag(TAG_LISTING_EVENT) + ); + + let err = order_event_context_from_tags( + RadrootsOrderEventType::OrderDecision, + &[ + vec!["p".into(), buyer_pubkey_wire()], + vec![TAG_E_PREV.into(), event_id_wire('2')], + ], + ) + .unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::MissingTag(TAG_E_ROOT)); + + let err = order_event_context_from_tags( + RadrootsOrderEventType::OrderDecision, + &[ + vec!["p".into(), buyer_pubkey_wire()], + vec![TAG_E_ROOT.into(), event_id_wire('1')], + vec![TAG_E_PREV.into(), "not-an-event-id".into()], + ], + ) + .unwrap_err(); + assert_eq!(err, RadrootsOrderEnvelopeParseError::InvalidTag(TAG_E_PREV)); + + let invalid_number = "x".parse::<u32>().unwrap_err(); + assert_eq!( + map_tag_parse_error_for_order_envelope(crate::error::EventParseError::MissingTag("p")), + RadrootsOrderEnvelopeParseError::MissingTag("p") + ); + assert_eq!( + map_tag_parse_error_for_order_envelope(crate::error::EventParseError::InvalidTag("p")), + RadrootsOrderEnvelopeParseError::InvalidTag("p") + ); + assert_eq!( + map_tag_parse_error_for_order_envelope(crate::error::EventParseError::InvalidKind { + expected: "1", + got: 2, + }), + RadrootsOrderEnvelopeParseError::InvalidKind(2) + ); + assert_eq!( + map_tag_parse_error_for_order_envelope(crate::error::EventParseError::InvalidNumber( + "n", + invalid_number, + )), + RadrootsOrderEnvelopeParseError::InvalidTag("n") + ); + assert_eq!( + map_tag_parse_error_for_order_envelope(crate::error::EventParseError::InvalidJson( + "json", + )), + RadrootsOrderEnvelopeParseError::InvalidTag("json") + ); + } + #[test] fn order_revision_kinds_parse_with_chain_tags() { for (kind, message_type) in [ diff --git a/crates/events_codec/src/order/encode.rs b/crates/events_codec/src/order/encode.rs @@ -230,3 +230,187 @@ pub fn order_cancellation_event_build( payload, }) } + +#[cfg(all(test, feature = "serde_json"))] +mod tests { + use super::{ + OrderEnvelopeEventBuildParts, map_order_envelope_error, map_order_payload_error, + order_envelope_event_build, + }; + use crate::error::EventEncodeError; + use radroots_events::{ + ids::RadrootsEventId, + order::{RadrootsOrderEnvelopeError, RadrootsOrderEventType, RadrootsOrderPayloadError}, + }; + + fn event_id(character: char) -> RadrootsEventId { + core::iter::repeat_n(character, 64) + .collect::<String>() + .parse() + .unwrap() + } + + fn payload() -> serde_json::Value { + serde_json::json!({}) + } + + #[test] + fn order_encode_error_mappers_cover_envelope_and_payload_variants() { + assert_empty_required( + map_order_envelope_error(RadrootsOrderEnvelopeError::MissingOrderId), + "order_id", + ); + assert_empty_required( + map_order_envelope_error(RadrootsOrderEnvelopeError::MissingListingAddr), + "listing_addr", + ); + assert_invalid_field( + map_order_envelope_error(RadrootsOrderEnvelopeError::InvalidVersion { + expected: 1, + got: 2, + }), + "version", + ); + assert_empty_required( + map_order_payload_error(RadrootsOrderPayloadError::EmptyField("buyer_pubkey")), + "buyer_pubkey", + ); + assert_empty_required( + map_order_payload_error(RadrootsOrderPayloadError::MissingItems), + "items", + ); + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidItemBinCount { index: 0 }), + "items.bin_count", + ); + assert_empty_required( + map_order_payload_error(RadrootsOrderPayloadError::MissingEconomicItems), + "economics.items", + ); + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemBinCount { + index: 0, + }), + "economics.items.bin_count", + ); + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemQuantity { + index: 0, + }), + "economics.items.quantity_amount", + ); + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemPrice { + index: 0, + }), + "economics.items.unit_price_amount", + ); + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidEconomicItemSubtotal { + index: 0, + }), + "economics.items.line_subtotal", + ); + for error in [ + RadrootsOrderPayloadError::InvalidEconomicLineAmount { + field: "adjustments.amount", + index: 0, + }, + RadrootsOrderPayloadError::InvalidEconomicLineKind { + field: "discounts.kind", + index: 0, + }, + RadrootsOrderPayloadError::InvalidEconomicLineEffect { + field: "discounts.effect", + index: 0, + }, + RadrootsOrderPayloadError::InvalidEconomicCurrency { + field: "subtotal.currency", + }, + RadrootsOrderPayloadError::InvalidEconomicOrdering { + field: "adjustments", + }, + RadrootsOrderPayloadError::InvalidEconomicTotal { field: "total" }, + RadrootsOrderPayloadError::InvalidOrderEconomicsBinding { field: "items" }, + ] { + assert!(matches!( + map_order_payload_error(error), + EventEncodeError::InvalidField(_) + )); + } + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidQuoteVersion), + "economics.quote_version", + ); + assert_empty_required( + map_order_payload_error(RadrootsOrderPayloadError::MissingInventoryCommitments), + "inventory_commitments", + ); + assert_invalid_field( + map_order_payload_error(RadrootsOrderPayloadError::InvalidInventoryCommitmentCount { + index: 0, + }), + "inventory_commitments.bin_count", + ); + } + + #[test] + fn order_envelope_event_build_requires_context_tags_by_message_type() { + let payload = payload(); + let root_event_id = event_id('1'); + let prev_event_id = event_id('2'); + + let missing_listing_event = order_envelope_event_build(OrderEnvelopeEventBuildParts { + recipient_pubkey: "recipient", + message_type: RadrootsOrderEventType::OrderRequested, + listing_addr: "listing-address", + order_id: "order-1", + listing_event: None, + root_event_id: None, + prev_event_id: None, + payload: &payload, + }) + .unwrap_err(); + assert_empty_required(missing_listing_event, "listing_event.id"); + + let missing_root = order_envelope_event_build(OrderEnvelopeEventBuildParts { + recipient_pubkey: "recipient", + message_type: RadrootsOrderEventType::OrderDecision, + listing_addr: "listing-address", + order_id: "order-1", + listing_event: None, + root_event_id: None, + prev_event_id: Some(&prev_event_id), + payload: &payload, + }) + .unwrap_err(); + assert_empty_required(missing_root, "root_event_id"); + + let missing_prev = order_envelope_event_build(OrderEnvelopeEventBuildParts { + recipient_pubkey: "recipient", + message_type: RadrootsOrderEventType::OrderDecision, + listing_addr: "listing-address", + order_id: "order-1", + listing_event: None, + root_event_id: Some(&root_event_id), + prev_event_id: None, + payload: &payload, + }) + .unwrap_err(); + assert_empty_required(missing_prev, "prev_event_id"); + } + + fn assert_empty_required(error: EventEncodeError, field: &'static str) { + assert!(matches!( + error, + EventEncodeError::EmptyRequiredField(found) if found == field + )); + } + + fn assert_invalid_field(error: EventEncodeError, field: &'static str) { + assert!(matches!( + error, + EventEncodeError::InvalidField(found) if found == field + )); + } +}