commit 6bab64bdbb98ddcde1b66b8cc7a45b9800d2973b
parent bbab2a137b050748b66055c067cd785977d30090
Author: triesap <tyson@radroots.org>
Date: Thu, 5 Mar 2026 02:53:27 +0000
trade: close strict coverage gaps in listing parsers
- align codec reject-path assertions with parser error tags
- refactor display-tag and image-size parsing to remove hidden region misses
- consolidate dvm envelope serialization tests to cover both serde_json paths in one instantiation
- run radroots-trade check, tests, and strict xtask coverage gate at 100/100/100/100
Diffstat:
4 files changed, 461 insertions(+), 90 deletions(-)
diff --git a/crates/trade/src/listing/codec.rs b/crates/trade/src/listing/codec.rs
@@ -325,19 +325,23 @@ fn listing_from_tags(
}
bin.quantity = Some(RadrootsCoreQuantity::new(amount, unit));
- if tag.len() >= 5 {
- if tag.len() < 6 {
+ match tag.as_slice() {
+ [_, _, _, _, display_amount_raw, display_unit_raw]
+ | [_, _, _, _, display_amount_raw, display_unit_raw, _] => {
+ let display_amount = parse_decimal(display_amount_raw, TAG_RADROOTS_BIN)?;
+ let display_unit = parse_unit(display_unit_raw)?;
+ bin.display_amount = Some(display_amount);
+ bin.display_unit = Some(display_unit);
+ if let [_, _, _, _, _, _, label] = tag.as_slice() {
+ bin.display_label = clean_value(label);
+ }
+ }
+ [_, _, _, _, _] => {
return Err(TradeListingParseError::InvalidTag(
TAG_RADROOTS_BIN.to_string(),
));
}
- let display_amount = parse_decimal(&tag[4], TAG_RADROOTS_BIN)?;
- let display_unit = parse_unit(&tag[5])?;
- bin.display_amount = Some(display_amount);
- bin.display_unit = Some(display_unit);
- if tag.len() == 7 {
- bin.display_label = clean_value(&tag[6]);
- }
+ _ => {}
}
}
TAG_RADROOTS_PRICE => {
@@ -375,16 +379,19 @@ fn listing_from_tags(
}
bin.price_per_canonical_unit = Some(price_per_canonical_unit);
- if tag.len() == 7 {
- return Err(TradeListingParseError::InvalidTag(
- TAG_RADROOTS_PRICE.to_string(),
- ));
- }
- if tag.len() == 8 {
- let display_price = parse_decimal(&tag[6], TAG_RADROOTS_PRICE)?;
- let display_unit = parse_unit(&tag[7])?;
- bin.display_price = Some(RadrootsCoreMoney::new(display_price, currency));
- bin.display_price_unit = Some(display_unit);
+ match tag.as_slice() {
+ [_, _, _, _, _, _, _] => {
+ return Err(TradeListingParseError::InvalidTag(
+ TAG_RADROOTS_PRICE.to_string(),
+ ));
+ }
+ [_, _, _, _, _, _, display_price_raw, display_unit_raw] => {
+ let display_price = parse_decimal(display_price_raw, TAG_RADROOTS_PRICE)?;
+ let display_unit = parse_unit(display_unit_raw)?;
+ bin.display_price = Some(RadrootsCoreMoney::new(display_price, currency));
+ bin.display_price_unit = Some(display_unit);
+ }
+ _ => {}
}
}
TAG_RADROOTS_DISCOUNT => {
@@ -655,9 +662,7 @@ mod tests {
RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney,
RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
- use radroots_events::listing::{
- RadrootsListing, RadrootsListingFarmRef, RadrootsListingStatus,
- };
+ use radroots_events::listing::{RadrootsListing, RadrootsListingFarmRef};
fn farm_ref() -> RadrootsListingFarmRef {
RadrootsListingFarmRef {
@@ -1021,6 +1026,27 @@ mod tests {
}
#[test]
+ fn listing_from_event_parts_rejects_missing_reference_tags() {
+ let mut missing_farm_ref = base_event_tags();
+ missing_farm_ref.extend(base_trade_tags());
+ missing_farm_ref.retain(|tag| tag.first().map(|v| v.as_str()) != Some(TAG_A));
+ let err = listing_from_event_parts(&missing_farm_ref, "").unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_A.to_string());
+
+ let mut missing_farm_pubkey = base_event_tags();
+ missing_farm_pubkey.extend(base_trade_tags());
+ missing_farm_pubkey.retain(|tag| tag.first().map(|v| v.as_str()) != Some(TAG_P));
+ let err = listing_from_event_parts(&missing_farm_pubkey, "").unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_P.to_string());
+
+ let mut invalid_resource_area = base_event_tags();
+ invalid_resource_area.extend(base_trade_tags());
+ invalid_resource_area.push(vec![TAG_RADROOTS_RESOURCE_AREA.into(), "bad".into()]);
+ let err = listing_from_event_parts(&invalid_resource_area, "").unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_RESOURCE_AREA.to_string());
+ }
+
+ #[test]
fn listing_tags_build_and_error_mapping_cover_paths() {
let listing = parse_base_listing_from_tags();
let built = listing_tags_build(&listing).expect("build tags");
@@ -1045,6 +1071,10 @@ mod tests {
let mut tags = base_trade_tags();
tags.push(Vec::new());
tags.push(vec![TAG_PRICE.into(), "ignored".into()]);
+ tags.push(vec!["process".into(), "washed".into()]);
+ tags.push(vec!["lot".into(), "lot-7".into()]);
+ tags.push(vec!["profile".into(), "fruity".into()]);
+ tags.push(vec!["year".into(), "2024".into()]);
tags.push(vec![TAG_RADROOTS_PRIMARY_BIN.into(), "bin-1".into()]);
tags.push(vec![
TAG_LOCATION.into(),
@@ -1101,6 +1131,10 @@ mod tests {
listing.location.as_ref().unwrap().geohash.as_deref(),
Some("u6se")
);
+ assert_eq!(listing.product.process.as_deref(), Some("washed"));
+ assert_eq!(listing.product.lot.as_deref(), Some("lot-7"));
+ assert_eq!(listing.product.profile.as_deref(), Some("fruity"));
+ assert_eq!(listing.product.year.as_deref(), Some("2024"));
assert_eq!(listing.images.as_ref().unwrap().len(), 1);
assert_eq!(listing.discounts.as_ref().unwrap().len(), 1);
}
@@ -1130,6 +1164,38 @@ mod tests {
}
#[test]
+ fn listing_from_tags_parses_delivery_enum_variants() {
+ let mut local_delivery = base_trade_tags();
+ local_delivery.push(vec![TAG_DELIVERY.into(), "local_delivery".into()]);
+ let listing = listing_from_tags(
+ &local_delivery,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .expect("local delivery parse");
+ assert_eq!(
+ format!("{:?}", listing.delivery_method),
+ "Some(LocalDelivery)"
+ );
+
+ let mut shipping = base_trade_tags();
+ shipping.push(vec![TAG_DELIVERY.into(), "shipping".into()]);
+ let listing = listing_from_tags(
+ &shipping,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .expect("shipping parse");
+ assert_eq!(format!("{:?}", listing.delivery_method), "Some(Shipping)");
+ }
+
+ #[test]
fn listing_from_tags_rejects_empty_structured_location_primary() {
let mut tags = base_trade_tags();
tags.push(vec![
@@ -1522,6 +1588,219 @@ mod tests {
)
.unwrap_err();
assert_eq!(parse_error_tag(err), TAG_IMAGE.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![TAG_INVENTORY.into(), "bad".into()]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_INVENTORY.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_BIN.into(),
+ "bin-1".into(),
+ "bad".into(),
+ "g".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_BIN.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_BIN.into(),
+ "bin-1".into(),
+ "500".into(),
+ "bad".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), "unit".to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_BIN.into(),
+ "bin-2".into(),
+ "500".into(),
+ "g".into(),
+ "bad".into(),
+ "g".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_BIN.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_BIN.into(),
+ "bin-2".into(),
+ "500".into(),
+ "g".into(),
+ "1".into(),
+ "bad".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), "unit".to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_PRICE.into(),
+ "bin-1".into(),
+ "bad".into(),
+ "USD".into(),
+ "1".into(),
+ "g".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_PRICE.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_PRICE.into(),
+ "bin-1".into(),
+ "10".into(),
+ "US".into(),
+ "1".into(),
+ "g".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), "currency".to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_PRICE.into(),
+ "bin-1".into(),
+ "10".into(),
+ "USD".into(),
+ "bad".into(),
+ "g".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_PRICE.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_PRICE.into(),
+ "bin-1".into(),
+ "10".into(),
+ "USD".into(),
+ "1".into(),
+ "bad".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), "unit".to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_PRICE.into(),
+ "bin-2".into(),
+ "10".into(),
+ "USD".into(),
+ "1".into(),
+ "g".into(),
+ "bad".into(),
+ "g".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_PRICE.to_string());
+
+ let mut tags = base_trade_tags();
+ tags.push(vec![
+ TAG_RADROOTS_PRICE.into(),
+ "bin-2".into(),
+ "10".into(),
+ "USD".into(),
+ "1".into(),
+ "g".into(),
+ "12".into(),
+ "bad".into(),
+ ]);
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), "unit".to_string());
}
#[test]
@@ -1635,6 +1914,32 @@ mod tests {
}
#[test]
+ fn listing_from_tags_rejects_incomplete_bin_draft() {
+ let tags = vec![
+ vec!["key".into(), "coffee".into()],
+ vec!["title".into(), "Coffee".into()],
+ vec!["category".into(), "coffee".into()],
+ vec![TAG_RADROOTS_PRIMARY_BIN.into(), "bin-1".into()],
+ vec![
+ TAG_RADROOTS_BIN.into(),
+ "bin-1".into(),
+ "500".into(),
+ "g".into(),
+ ],
+ ];
+ let err = listing_from_tags(
+ &tags,
+ listing_d_tag(),
+ farm_ref(),
+ "seller".to_string(),
+ None,
+ None,
+ )
+ .unwrap_err();
+ assert_eq!(parse_error_tag(err), TAG_RADROOTS_PRICE.to_string());
+ }
+
+ #[test]
fn parse_farm_and_reference_helpers_cover_all_paths() {
let valid_farm_tags = vec![vec![
TAG_A.into(),
@@ -1865,18 +2170,17 @@ mod tests {
set_optional(&mut opt_none, Some(&blank));
assert_eq!(opt_none, None);
- assert!(matches!(
- parse_status("ACTIVE"),
- RadrootsListingStatus::Active
- ));
- assert!(matches!(parse_status("sold"), RadrootsListingStatus::Sold));
+ assert_eq!(format!("{:?}", parse_status("ACTIVE")), "Active");
+ assert_eq!(format!("{:?}", parse_status("sold")), "Sold");
assert_eq!(
format!("{:?}", parse_status("queued")),
"Other { value: \"queued\" }"
);
assert_eq!(parse_image_size("100x200").unwrap().w, 100);
+ assert!(parse_image_size("100").is_none());
assert!(parse_image_size("invalid").is_none());
+ assert!(parse_image_size("badx100").is_none());
assert!(parse_image_size("100xbad").is_none());
}
@@ -2050,9 +2354,9 @@ fn parse_status(value: &str) -> RadrootsListingStatus {
}
fn parse_image_size(value: &str) -> Option<RadrootsListingImageSize> {
- let mut parts = value.split('x');
- let w = parts.next()?.parse::<u32>().ok()?;
- let h = parts.next()?.parse::<u32>().ok()?;
+ let (w_raw, h_raw) = value.split_once('x')?;
+ let w = w_raw.parse::<u32>().ok()?;
+ let h = h_raw.parse::<u32>().ok()?;
Some(RadrootsListingImageSize { w, h })
}
diff --git a/crates/trade/src/listing/dvm.rs b/crates/trade/src/listing/dvm.rs
@@ -241,23 +241,20 @@ pub struct TradeListingAddress {
impl TradeListingAddress {
pub fn parse(addr: &str) -> Result<Self, TradeListingAddressError> {
- let mut parts = addr.split(':');
- let kind = parts
- .next()
- .ok_or(TradeListingAddressError::InvalidFormat)?
- .parse::<u16>()
- .map_err(|_| TradeListingAddressError::InvalidFormat)?;
- let seller_pubkey = parts
- .next()
- .ok_or(TradeListingAddressError::InvalidFormat)?
- .to_string();
- let listing_id = parts
- .next()
- .ok_or(TradeListingAddressError::InvalidFormat)?
- .to_string();
- if parts.next().is_some() {
+ let (kind_raw, seller_and_listing) = addr
+ .split_once(':')
+ .ok_or(TradeListingAddressError::InvalidFormat)?;
+ let (seller_pubkey_raw, listing_id_raw) = seller_and_listing
+ .split_once(':')
+ .ok_or(TradeListingAddressError::InvalidFormat)?;
+ if listing_id_raw.contains(':') {
return Err(TradeListingAddressError::InvalidFormat);
}
+ let kind = kind_raw
+ .parse::<u16>()
+ .map_err(|_| TradeListingAddressError::InvalidFormat)?;
+ let seller_pubkey = seller_pubkey_raw.to_string();
+ let listing_id = listing_id_raw.to_string();
if kind == KIND_PROFILE as u16
|| seller_pubkey.trim().is_empty()
|| listing_id.trim().is_empty()
@@ -456,30 +453,29 @@ mod tests {
#[test]
fn message_type_kind_and_request_flags_cover_all_variants() {
let expected_kinds = crate::listing::kinds::TRADE_LISTING_KINDS;
- let cases = [
- (TradeListingMessageType::ListingValidateRequest, true, false),
- (TradeListingMessageType::ListingValidateResult, false, true),
- (TradeListingMessageType::OrderRequest, true, false),
- (TradeListingMessageType::OrderResponse, false, true),
- (TradeListingMessageType::OrderRevision, true, false),
- (TradeListingMessageType::OrderRevisionAccept, false, true),
- (TradeListingMessageType::OrderRevisionDecline, false, true),
- (TradeListingMessageType::Question, true, false),
- (TradeListingMessageType::Answer, false, true),
- (TradeListingMessageType::DiscountRequest, true, false),
- (TradeListingMessageType::DiscountOffer, false, true),
- (TradeListingMessageType::DiscountAccept, true, false),
- (TradeListingMessageType::DiscountDecline, true, false),
- (TradeListingMessageType::Cancel, true, false),
- (TradeListingMessageType::FulfillmentUpdate, true, false),
- (TradeListingMessageType::Receipt, true, false),
- ];
-
- for (message_type, is_request, is_result) in cases {
- assert_eq!(message_type.is_request(), is_request);
- assert_eq!(message_type.is_result(), is_result);
- assert!(expected_kinds.contains(&message_type.kind()));
- }
+ let assert_case =
+ |message_type: TradeListingMessageType, is_request: bool, is_result: bool| {
+ assert_eq!(message_type.is_request(), is_request);
+ assert_eq!(message_type.is_result(), is_result);
+ assert!(expected_kinds.contains(&message_type.kind()));
+ };
+
+ assert_case(TradeListingMessageType::ListingValidateRequest, true, false);
+ assert_case(TradeListingMessageType::ListingValidateResult, false, true);
+ assert_case(TradeListingMessageType::OrderRequest, true, false);
+ assert_case(TradeListingMessageType::OrderResponse, false, true);
+ assert_case(TradeListingMessageType::OrderRevision, true, false);
+ assert_case(TradeListingMessageType::OrderRevisionAccept, false, true);
+ assert_case(TradeListingMessageType::OrderRevisionDecline, false, true);
+ assert_case(TradeListingMessageType::Question, true, false);
+ assert_case(TradeListingMessageType::Answer, false, true);
+ assert_case(TradeListingMessageType::DiscountRequest, true, false);
+ assert_case(TradeListingMessageType::DiscountOffer, false, true);
+ assert_case(TradeListingMessageType::DiscountAccept, true, false);
+ assert_case(TradeListingMessageType::DiscountDecline, true, false);
+ assert_case(TradeListingMessageType::Cancel, true, false);
+ assert_case(TradeListingMessageType::FulfillmentUpdate, true, false);
+ assert_case(TradeListingMessageType::Receipt, true, false);
}
#[test]
@@ -539,6 +535,10 @@ mod tests {
TradeListingAddressError::InvalidFormat
);
assert_eq!(
+ TradeListingAddress::parse("30340").unwrap_err(),
+ TradeListingAddressError::InvalidFormat
+ );
+ assert_eq!(
TradeListingAddress::parse("30340:seller").unwrap_err(),
TradeListingAddressError::InvalidFormat
);
@@ -573,12 +573,40 @@ mod tests {
}
#[cfg(feature = "serde_json")]
+ #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
+ struct EnvelopePayload {
+ fail: bool,
+ }
+
+ #[cfg(feature = "serde_json")]
+ impl EnvelopePayload {
+ fn ok() -> Self {
+ Self { fail: false }
+ }
+
+ fn fail() -> Self {
+ Self { fail: true }
+ }
+ }
+
+ #[cfg(feature = "serde_json")]
+ impl serde::Serialize for EnvelopePayload {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ if self.fail {
+ return Err(serde::ser::Error::custom("intentional"));
+ }
+ serializer.serialize_str("ok")
+ }
+ }
+
+ #[cfg(feature = "serde_json")]
#[test]
fn envelope_event_build_includes_order_tag() {
let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
- let payload = TradeListingValidateRequest {
- listing_event: None,
- };
+ let payload = EnvelopePayload::ok();
let built = super::trade_listing_envelope_event_build(
"pubkey",
TradeListingMessageType::OrderRequest,
@@ -590,7 +618,7 @@ mod tests {
assert_eq!(built.kind, TradeListingMessageType::OrderRequest.kind());
- let envelope: TradeListingEnvelope<TradeListingValidateRequest> =
+ let envelope: TradeListingEnvelope<serde_json::Value> =
serde_json::from_str(&built.content).unwrap();
assert_eq!(envelope.listing_addr, listing_addr.clone());
assert_eq!(envelope.order_id.as_deref(), Some("order-1"));
@@ -601,9 +629,7 @@ mod tests {
#[test]
fn envelope_event_build_omits_order_tag_when_missing() {
let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
- let payload = TradeListingValidateRequest {
- listing_event: None,
- };
+ let payload = EnvelopePayload::ok();
let built = super::trade_listing_envelope_event_build(
"pubkey",
TradeListingMessageType::ListingValidateRequest,
@@ -618,10 +644,26 @@ mod tests {
TradeListingMessageType::ListingValidateRequest.kind()
);
- let envelope: TradeListingEnvelope<TradeListingValidateRequest> =
+ let envelope: TradeListingEnvelope<serde_json::Value> =
serde_json::from_str(&built.content).unwrap();
assert_eq!(envelope.listing_addr, listing_addr);
assert!(envelope.order_id.is_none());
assert_eq!(built.tags.len(), 2);
}
+
+ #[cfg(feature = "serde_json")]
+ #[test]
+ fn envelope_event_build_propagates_payload_serialization_error() {
+ let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
+ let payload = EnvelopePayload::fail();
+ let err = super::trade_listing_envelope_event_build(
+ "pubkey",
+ TradeListingMessageType::ListingValidateRequest,
+ listing_addr,
+ None,
+ &payload,
+ )
+ .unwrap_err();
+ assert!(err.to_string().contains("intentional"));
+ }
}
diff --git a/crates/trade/src/listing/price_ext.rs b/crates/trade/src/listing/price_ext.rs
@@ -178,4 +178,33 @@ mod tests {
assert_eq!(total.quantity_amount, subtotal.quantity_amount);
assert_eq!(total.price_amount, subtotal.price_amount);
}
+
+ #[test]
+ fn try_total_for_count_propagates_subtotal_errors() {
+ let bin = RadrootsListingBin {
+ bin_id: "bin-1".into(),
+ quantity: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ price_per_canonical_unit: RadrootsCoreQuantityPrice::new(
+ RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD),
+ RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each),
+ ),
+ display_amount: None,
+ display_unit: None,
+ display_label: None,
+ display_price: None,
+ display_price_unit: None,
+ };
+
+ let err = bin.try_total_for_count(1).unwrap_err();
+ assert_eq!(
+ err,
+ RadrootsCoreQuantityPriceError::UnitMismatch {
+ have: RadrootsCoreUnit::MassG,
+ want: RadrootsCoreUnit::Each,
+ }
+ );
+ }
}
diff --git a/crates/trade/src/listing/validation.rs b/crates/trade/src/listing/validation.rs
@@ -377,10 +377,12 @@ mod tests {
let mut event = base_event(&listing);
event.tags.clear();
let err = validate_listing_event(&event).unwrap_err();
- assert!(matches!(
+ assert_eq!(
err,
- TradeListingValidationError::ParseError { .. }
- ));
+ TradeListingValidationError::ParseError {
+ error: crate::listing::codec::TradeListingParseError::MissingTag("d".to_string())
+ }
+ );
}
#[test]
@@ -419,10 +421,7 @@ mod tests {
vec!["delivery".into(), "pickup".into()],
];
let err = validate_listing_event(&event).unwrap_err();
- assert!(matches!(
- err,
- TradeListingValidationError::ParseError { .. }
- ));
+ assert!(format!("{err:?}").starts_with("ParseError"));
}
#[test]
@@ -431,7 +430,7 @@ mod tests {
let mut event = base_event(&listing);
event.author = "other".into();
let err = validate_listing_event(&event).unwrap_err();
- assert!(matches!(err, TradeListingValidationError::InvalidSeller));
+ assert_eq!(err, TradeListingValidationError::InvalidSeller);
}
#[test]
@@ -440,7 +439,7 @@ mod tests {
listing.inventory_available = None;
let event = base_event(&listing);
let err = validate_listing_event(&event).unwrap_err();
- assert!(matches!(err, TradeListingValidationError::MissingInventory));
+ assert_eq!(err, TradeListingValidationError::MissingInventory);
}
#[test]
@@ -449,10 +448,7 @@ mod tests {
let mut event = base_event(&listing);
event.kind = 0;
let err = validate_listing_event(&event).unwrap_err();
- assert!(matches!(
- err,
- TradeListingValidationError::InvalidKind { kind: 0 }
- ));
+ assert_eq!(err, TradeListingValidationError::InvalidKind { kind: 0 });
}
#[test]