tests.rs (6074B)
1 use super::*; 2 use radroots_event::envelope::EventTags; 3 4 const HASH: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; 5 6 #[test] 7 fn normalizes_tolerant_decimal_and_currency_values() { 8 let tags = focused_tags(vec![ 9 tag(&["price", "0003.5000", "cad"]), 10 tag(&["radroots:price_unit", "lb"]), 11 tag(&["radroots:quantity", "0010.00", "lb"]), 12 ]); 13 14 let projection = focused(project(&tags).unwrap()); 15 assert_eq!(projection.price().amount(), "3.5"); 16 assert_eq!(projection.price().currency().as_str(), "CAD"); 17 assert_eq!(projection.quantity().unwrap().amount(), "10"); 18 } 19 20 #[test] 21 fn partitions_raw_markers_before_shape_or_capability_validation() { 22 let operational = tags(vec![tag(&["radroots:bin"]), tag(&["delivery"])]); 23 assert_eq!( 24 project(&operational).unwrap(), 25 RadrootsFoodAvailabilityProjectionOutcome::Excluded( 26 ClassifiedListingPartition::OperationalListing 27 ) 28 ); 29 30 let mixed = tags(vec![ 31 tag(&["radroots:price_unit"]), 32 tag(&["radroots:price"]), 33 ]); 34 assert_eq!( 35 project(&mixed).unwrap_err().code(), 36 "food_profile_ambiguous" 37 ); 38 39 let malformed_focused = tags(vec![tag(&["radroots:price_unit"])]); 40 assert_eq!( 41 project(&malformed_focused).unwrap_err().code(), 42 "food_tag_invalid" 43 ); 44 assert_eq!( 45 project_inbound_food_availability_parts( 46 KIND_CLASSIFIED_LISTING, 47 200, 48 &malformed_focused, 49 "", 50 ) 51 .unwrap_err() 52 .code(), 53 "food_tag_invalid" 54 ); 55 } 56 57 #[test] 58 fn focused_marker_errors_and_prohibited_capabilities_are_stable() { 59 let mut malformed_unit = base_tags(); 60 malformed_unit.push(tag(&["price", "3", "CAD"])); 61 malformed_unit.push(tag(&["radroots:price_unit"])); 62 malformed_unit.push(tag(&["status", "active"])); 63 assert_eq!( 64 project(&tags(malformed_unit)).unwrap_err().code(), 65 "price_unit_invalid" 66 ); 67 68 let mut prohibited = base_tags(); 69 prohibited.push(tag(&["radroots:price_unit", "lb"])); 70 prohibited.push(tag(&["status", "active"])); 71 prohibited.push(tag(&["delivery", "tomorrow"])); 72 assert_eq!( 73 project(&tags(prohibited)).unwrap_err().code(), 74 "prohibited_capability" 75 ); 76 } 77 78 #[test] 79 fn retains_ordered_bounded_image_diagnostics() { 80 let url = format!("https://media.example/{HASH}.webp"); 81 let mut extra = focused_tags(Vec::new()).to_vec(); 82 extra.extend([ 83 tag(&["image", &url]), 84 tag(&["image", &url, "0x600", "extra"]), 85 ]); 86 let projection = focused(project(&tags(extra)).unwrap()); 87 assert_eq!( 88 projection.diagnostics(), 89 &[ 90 RadrootsFoodAvailabilityImageDiagnostic::ShapeInvalid, 91 RadrootsFoodAvailabilityImageDiagnostic::DimensionsMissing, 92 RadrootsFoodAvailabilityImageDiagnostic::ShapeInvalid, 93 RadrootsFoodAvailabilityImageDiagnostic::DimensionsInvalid, 94 RadrootsFoodAvailabilityImageDiagnostic::DuplicateUrl, 95 RadrootsFoodAvailabilityImageDiagnostic::DuplicateDigest, 96 ] 97 ); 98 99 let mut bounded = focused_tags(Vec::new()).to_vec(); 100 bounded.extend((0..=RADROOTS_FOOD_IMAGE_MAX_COUNT).map(|index| { 101 tag(&[ 102 "image", 103 &format!("https://media.example/{index:064x}.webp"), 104 "800x600", 105 ]) 106 })); 107 let projection = focused(project(&tags(bounded)).unwrap()); 108 assert_eq!(projection.images().len(), RADROOTS_FOOD_IMAGE_MAX_COUNT); 109 assert_eq!( 110 projection.diagnostics().first(), 111 Some(&RadrootsFoodAvailabilityImageDiagnostic::CountExceeded) 112 ); 113 } 114 115 #[test] 116 fn accepts_optional_standard_tags_but_bounds_raw_decimal_digits() { 117 let mut optional = focused_tags(Vec::new()).to_vec(); 118 optional.extend([tag(&["t", "vegetables"]), tag(&["g", "c28"])]); 119 assert!(matches!( 120 project(&tags(optional)).unwrap(), 121 RadrootsFoodAvailabilityProjectionOutcome::Focused(_) 122 )); 123 124 let mut oversized = base_tags(); 125 oversized.push(tag(&["price", "00000000000000000000000000003", "CAD"])); 126 oversized.push(tag(&["radroots:price_unit", "lb"])); 127 oversized.push(tag(&["status", "active"])); 128 assert_eq!( 129 project(&tags(oversized)).unwrap_err().code(), 130 "price_invalid" 131 ); 132 } 133 134 fn project( 135 tags: &EventTags, 136 ) -> Result<RadrootsFoodAvailabilityProjectionOutcome, RadrootsFoodAvailabilityProjectionError> { 137 project_inbound_food_availability_parts( 138 KIND_CLASSIFIED_LISTING, 139 200, 140 tags, 141 "Carrots available this week.", 142 ) 143 } 144 145 fn focused( 146 outcome: RadrootsFoodAvailabilityProjectionOutcome, 147 ) -> RadrootsInboundFoodAvailabilityProjection { 148 match outcome { 149 RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) => *projection, 150 RadrootsFoodAvailabilityProjectionOutcome::Excluded(partition) => { 151 panic!("unexpected exclusion: {partition:?}") 152 } 153 } 154 } 155 156 fn focused_tags(mut additional: Vec<Vec<String>>) -> EventTags { 157 let mut values = base_tags(); 158 if !additional 159 .iter() 160 .any(|value| value.first().is_some_and(|name| name == "price")) 161 { 162 values.push(tag(&["price", "3", "CAD"])); 163 } 164 values.append(&mut additional); 165 if !values.iter().any(|value| { 166 value 167 .first() 168 .is_some_and(|name| name == "radroots:price_unit") 169 }) { 170 values.push(tag(&["radroots:price_unit", "lb"])); 171 } 172 values.push(tag(&["status", "active"])); 173 tags(values) 174 } 175 176 fn base_tags() -> Vec<Vec<String>> { 177 vec![ 178 tag(&["d", "nantes-carrots"]), 179 tag(&["title", "Nantes Carrots"]), 180 tag(&["summary", "Fresh bunches"]), 181 tag(&["published_at", "100"]), 182 tag(&["location", "Central Saanich, BC"]), 183 ] 184 } 185 186 fn tags(values: Vec<Vec<String>>) -> EventTags { 187 EventTags::new(values).unwrap() 188 } 189 190 fn tag(values: &[&str]) -> Vec<String> { 191 values.iter().map(|value| (*value).to_string()).collect() 192 }