lib

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

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 }