food_availability_profile.rs (7828B)
1 mod support; 2 3 use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; 4 use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; 5 use radroots_event::food::availability::{ 6 FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityImage, 7 FoodAvailabilityStatus, FoodContent, FoodCurrency, FoodIdentifier, FoodImageDimensions, 8 FoodPrice, FoodPublishedAt, FoodQuantity, FoodText, FoodUnit, 9 }; 10 use radroots_event::media::AuthoredImage; 11 use radroots_event_codec::admission::food_availability::{ 12 RadrootsFoodAvailabilityAdmissionOutcome, verify_and_admit_food_availability_event, 13 }; 14 use radroots_nostr::Error; 15 use radroots_nostr::event::GenericBuilder; 16 use radroots_nostr::event::Kind as RadrootsNostrKind; 17 use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp; 18 use radroots_nostr::event::build_food_availability as build_food_availability_event; 19 use radroots_nostr::event::from_nostr; 20 use radroots_nostr::tag::Tag as RadrootsNostrTag; 21 use support::FIXTURE_ALICE_SECRET_KEY_HEX; 22 23 const CREATED_AT: u64 = 1_784_347_200; 24 25 #[test] 26 fn typed_food_builder_signs_the_exact_strict_profile() { 27 let keys = fixture_keys(); 28 let created_at = RadrootsNostrTimestamp::from_secs(CREATED_AT); 29 let event = build_food_availability_event(&details(), created_at) 30 .expect("typed FoodAvailability builder") 31 .sign_with_keys(&keys) 32 .expect("signed FoodAvailability event"); 33 34 assert_eq!( 35 event.kind, 36 RadrootsNostrKind::Custom( 37 u16::try_from(radroots_event::envelope::kind::KIND_CLASSIFIED_LISTING) 38 .expect("classified listing kind") 39 ) 40 ); 41 assert_eq!(event.created_at, created_at); 42 assert_eq!( 43 event 44 .tags 45 .iter() 46 .map(|tag| tag.as_slice().to_vec()) 47 .collect::<Vec<_>>(), 48 vec![ 49 vec!["d", "nantes-carrots"], 50 vec!["title", "Nantes Carrots"], 51 vec!["summary", "Fresh bunches"], 52 vec!["published_at", "1784347100"], 53 vec!["location", "Central Saanich, BC"], 54 vec!["price", "3", "CAD"], 55 vec!["radroots:price_unit", "lb"], 56 vec!["radroots:quantity", "24", "lb"], 57 vec!["status", "active"], 58 ] 59 ); 60 event.verify().expect("valid NIP-01 event"); 61 62 let envelope = from_nostr(&event).expect("Radroots event adapter"); 63 assert!(matches!( 64 verify_and_admit_food_availability_event(envelope) 65 .expect("verified FoodAvailability admission"), 66 RadrootsFoodAvailabilityAdmissionOutcome::Admitted(_) 67 )); 68 } 69 70 #[test] 71 fn typed_food_builder_keeps_timestamp_validation_inside_construction() { 72 let error = 73 build_food_availability_event(&details(), RadrootsNostrTimestamp::from_secs(1_784_347_000)) 74 .err() 75 .expect("created_at before published_at must fail"); 76 77 assert!(matches!(error, Error::FoodAvailabilityEncode(_))); 78 } 79 80 #[test] 81 fn typed_food_builder_preserves_a_byte_verified_blossom_image_tuple() { 82 let image = blossom_image(); 83 let image_url = image.url().to_owned(); 84 let event = build_food_availability_event( 85 &details_with_images(vec![image]), 86 RadrootsNostrTimestamp::from_secs(CREATED_AT), 87 ) 88 .expect("typed media FoodAvailability builder") 89 .sign_with_keys(&fixture_keys()) 90 .expect("signed media FoodAvailability event"); 91 92 let image_tags = event 93 .tags 94 .iter() 95 .filter(|tag| tag.as_slice().first().is_some_and(|name| name == "image")) 96 .map(|tag| tag.as_slice().to_vec()) 97 .collect::<Vec<_>>(); 98 assert_eq!( 99 image_tags, 100 vec![vec!["image".to_owned(), image_url, "640x480".to_owned(),]] 101 ); 102 } 103 104 #[test] 105 fn generic_builder_reserves_focused_and_ambiguous_listing_profiles() { 106 let keys = fixture_keys(); 107 let kind = RadrootsNostrKind::Custom( 108 u16::try_from(radroots_event::envelope::kind::KIND_CLASSIFIED_LISTING) 109 .expect("classified listing kind"), 110 ); 111 112 for tags in [ 113 vec![ 114 RadrootsNostrTag::parse(["d", "focused"]).expect("d tag"), 115 RadrootsNostrTag::parse(["radroots:price_unit", "lb"]).expect("focused marker"), 116 ], 117 vec![ 118 RadrootsNostrTag::parse(["d", "ambiguous"]).expect("d tag"), 119 RadrootsNostrTag::parse(["radroots:price_unit", "lb"]).expect("focused marker"), 120 RadrootsNostrTag::parse(["radroots:primary_bin", "bin-1"]).expect("operational marker"), 121 ], 122 ] { 123 assert!(matches!( 124 GenericBuilder::new(kind, "reserved") 125 .tags(tags) 126 .sign_with_keys(&keys), 127 Err(Error::TypedAuthoringRequired { kind: 30_402 }) 128 )); 129 } 130 } 131 132 #[test] 133 fn generic_builder_retains_marker_free_and_operational_nip99_compatibility() { 134 let keys = fixture_keys(); 135 let kind = RadrootsNostrKind::Custom( 136 u16::try_from(radroots_event::envelope::kind::KIND_CLASSIFIED_LISTING) 137 .expect("classified listing kind"), 138 ); 139 140 for tags in [ 141 vec![RadrootsNostrTag::parse(["d", "generic"]).expect("d tag")], 142 vec![ 143 RadrootsNostrTag::parse(["d", "operational"]).expect("d tag"), 144 RadrootsNostrTag::parse(["radroots:primary_bin", "bin-1"]).expect("operational marker"), 145 ], 146 ] { 147 let event = GenericBuilder::new(kind, "compatible") 148 .tags(tags) 149 .custom_created_at(RadrootsNostrTimestamp::from_secs(CREATED_AT)) 150 .sign_with_keys(&keys) 151 .expect("non-focused NIP-99 remains generic-authorable"); 152 event.verify().expect("valid generic NIP-99 event"); 153 } 154 } 155 156 fn fixture_keys() -> RadrootsNostrKeys { 157 RadrootsNostrKeys::new( 158 RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture secret key"), 159 ) 160 } 161 162 fn details() -> FoodAvailabilityDetails { 163 details_with_images(Vec::new()) 164 } 165 166 fn details_with_images(images: Vec<FoodAvailabilityImage>) -> FoodAvailabilityDetails { 167 FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts { 168 content: FoodContent::new("Carrots available this week.").expect("content"), 169 identifier: FoodIdentifier::parse("nantes-carrots").expect("identifier"), 170 title: FoodText::new("Nantes Carrots").expect("title"), 171 summary: FoodText::new("Fresh bunches").expect("summary"), 172 published_at: FoodPublishedAt::new(1_784_347_100).expect("published_at"), 173 location: FoodText::new("Central Saanich, BC").expect("location"), 174 price: FoodPrice::new( 175 "3", 176 FoodCurrency::parse("CAD").expect("currency"), 177 FoodUnit::Pound, 178 ) 179 .expect("price"), 180 quantity: Some(FoodQuantity::new("24", FoodUnit::Pound).expect("quantity")), 181 status: FoodAvailabilityStatus::Active, 182 images, 183 }) 184 .expect("FoodAvailability details") 185 } 186 187 fn blossom_image() -> FoodAvailabilityImage { 188 let bytes = b"victoria-carrots-image-fixture"; 189 let hash = Sha256::digest(bytes); 190 let media_type = MediaType::parse("image/webp").expect("image media type"); 191 let verified = BlobDescriptor::new( 192 BlobUrl::parse(&format!("https://media.example/{hash}.webp")).expect("Blossom URL"), 193 hash, 194 bytes.len() as u64, 195 media_type.clone(), 196 CREATED_AT, 197 ) 198 .expect("Blossom descriptor") 199 .approve_reference() 200 .expect("approved Blossom reference") 201 .verify_bytes(bytes, &media_type) 202 .expect("byte-verified Blossom descriptor"); 203 let image = AuthoredImage::try_from_verified_descriptor(verified).expect("authored image"); 204 let dimensions = FoodImageDimensions::new(640, 480).expect("image dimensions"); 205 FoodAvailabilityImage::new(image, dimensions) 206 }