tests.rs (9015B)
1 use super::*; 2 use radroots_event::post::AuthoredUpdate; 3 4 #[test] 5 fn thread_exclusion_precedes_ask_and_media_projection() { 6 let projection = project_inbound_post_parts( 7 KIND_POST, 8 &[ 9 vec!["e".to_string(), "parent".to_string()], 10 vec!["t".to_string(), "radroots-ask".to_string()], 11 vec!["imeta".to_string(), "x malformed".to_string()], 12 ], 13 "reply", 14 ) 15 .unwrap(); 16 17 assert_eq!( 18 projection.classification(), 19 RadrootsPostClassification::ThreadExcluded 20 ); 21 assert!(projection.imeta().is_empty()); 22 assert!(projection.diagnostics().is_empty()); 23 } 24 25 #[test] 26 fn every_empty_or_malformed_event_reference_is_thread_excluded() { 27 for event_reference in [ 28 vec!["e".to_string()], 29 vec!["e".to_string(), String::new()], 30 vec!["e".to_string(), "not-an-event-id".to_string()], 31 ] { 32 let projection = 33 project_inbound_post_parts(KIND_POST, &[event_reference], "candidate").unwrap(); 34 35 assert_eq!( 36 projection.classification(), 37 RadrootsPostClassification::ThreadExcluded 38 ); 39 assert!(!projection.classification().is_root_card()); 40 } 41 } 42 43 #[test] 44 fn normalized_ask_precedes_malformed_media_and_retains_diagnostics() { 45 let projection = project_inbound_post_parts( 46 KIND_POST, 47 &[ 48 vec!["t".to_string(), " RADROOTS-ASK ".to_string()], 49 vec![ 50 "imeta".to_string(), 51 "url https://cdn.example/leaf.webp".to_string(), 52 "x malformed".to_string(), 53 ], 54 ], 55 "Question https://cdn.example/leaf.webp", 56 ) 57 .unwrap(); 58 59 assert_eq!(projection.classification(), RadrootsPostClassification::Ask); 60 assert_eq!( 61 diagnostic_codes(projection.diagnostics()), 62 ["imeta_metadata_missing", "imeta_hash_invalid"] 63 ); 64 assert_eq!( 65 projection.ask_marker().unwrap(), 66 ["t".to_string(), " RADROOTS-ASK ".to_string()] 67 ); 68 } 69 70 #[test] 71 fn photo_preserves_repeatable_fallbacks_and_ordered_unknown_fields() { 72 let projection = project_inbound_post_parts( 73 KIND_POST, 74 &[qualifying_imeta(vec![ 75 "fallback https://cache-one.example/harvest.webp", 76 "x-farm cultivar-strawberry", 77 "fallback https://cache-two.example/harvest.webp", 78 "future-field retained value", 79 ])], 80 "Harvest https://cdn.example/harvest.webp", 81 ) 82 .unwrap(); 83 let media = &projection.imeta()[0]; 84 85 assert_eq!( 86 projection.classification(), 87 RadrootsPostClassification::PhotoUpdate 88 ); 89 assert_eq!( 90 media.fallbacks(), 91 [ 92 "https://cache-one.example/harvest.webp".to_string(), 93 "https://cache-two.example/harvest.webp".to_string(), 94 ] 95 ); 96 assert_eq!( 97 media.unknown_fields(), 98 [ 99 "x-farm cultivar-strawberry".to_string(), 100 "future-field retained value".to_string(), 101 ] 102 ); 103 assert!(media.qualifies_photo()); 104 } 105 106 #[test] 107 fn duplicate_singletons_and_mixed_imeta_downgrade_to_update() { 108 let mut duplicate = qualifying_imeta(Vec::new()); 109 duplicate.insert( 110 3, 111 "x bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), 112 ); 113 let malformed = vec![ 114 "imeta".to_string(), 115 "url https://cdn.example/leaf.webp".to_string(), 116 "x malformed".to_string(), 117 ]; 118 let projection = project_inbound_post_parts( 119 KIND_POST, 120 &[duplicate, malformed], 121 "Harvest https://cdn.example/harvest.webp and https://cdn.example/leaf.webp", 122 ) 123 .unwrap(); 124 125 assert_eq!( 126 projection.classification(), 127 RadrootsPostClassification::Update 128 ); 129 assert_eq!( 130 diagnostic_codes(projection.diagnostics()), 131 [ 132 "imeta_singleton_duplicate", 133 "imeta_metadata_missing", 134 "imeta_hash_invalid", 135 ] 136 ); 137 } 138 139 #[test] 140 fn duplicate_urls_and_excess_imeta_downgrade_to_update() { 141 let duplicate = qualifying_imeta(Vec::new()); 142 let projection = project_inbound_post_parts( 143 KIND_POST, 144 &[duplicate.clone(), duplicate], 145 "Harvest https://cdn.example/harvest.webp", 146 ) 147 .unwrap(); 148 assert_eq!( 149 projection.classification(), 150 RadrootsPostClassification::Update 151 ); 152 assert_eq!( 153 diagnostic_codes(projection.diagnostics()), 154 ["duplicate_imeta_url"] 155 ); 156 157 let imeta = qualifying_imeta(Vec::new()); 158 let tags = vec![imeta; RADROOTS_POST_IMETA_MAX_COUNT + 1]; 159 let projection = 160 project_inbound_post_parts(KIND_POST, &tags, "Harvest https://cdn.example/harvest.webp") 161 .unwrap(); 162 assert_eq!( 163 projection.classification(), 164 RadrootsPostClassification::Update 165 ); 166 assert_eq!( 167 projection.diagnostics().first(), 168 Some(&RadrootsPostDiagnostic::ImetaCountExceeded) 169 ); 170 } 171 172 #[test] 173 fn invalid_imeta_fields_report_stable_ordered_diagnostics() { 174 let projection = project_inbound_post_parts( 175 KIND_POST, 176 &[vec![ 177 "imeta".to_string(), 178 "url ftp://cdn.example/harvest.webp".to_string(), 179 "x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(), 180 "m image/webp;quality=90".to_string(), 181 "dim 01x0".to_string(), 182 "size 0".to_string(), 183 "alt \t".to_string(), 184 "fallback file:///harvest.webp".to_string(), 185 ]], 186 "Harvest ftp://cdn.example/harvest.webp", 187 ) 188 .unwrap(); 189 190 assert_eq!( 191 projection.classification(), 192 RadrootsPostClassification::Update 193 ); 194 assert_eq!( 195 diagnostic_codes(projection.diagnostics()), 196 [ 197 "imeta_url_invalid", 198 "imeta_hash_invalid", 199 "imeta_mime_invalid", 200 "imeta_dimensions_invalid", 201 "imeta_size_invalid", 202 "imeta_alt_invalid", 203 "imeta_fallback_url_invalid", 204 ] 205 ); 206 } 207 208 #[test] 209 fn malformed_fields_and_oversized_alt_never_qualify_photo() { 210 let oversized_alt = "a".repeat(RADROOTS_POST_ALT_MAX_BYTES + 1); 211 let mut tag = qualifying_imeta(Vec::new()); 212 tag.push("malformed".to_string()); 213 tag[6] = format!("alt {oversized_alt}"); 214 let projection = project_inbound_post_parts( 215 KIND_POST, 216 &[tag], 217 "Harvest https://cdn.example/harvest.webp", 218 ) 219 .unwrap(); 220 221 assert_eq!( 222 projection.classification(), 223 RadrootsPostClassification::Update 224 ); 225 assert_eq!( 226 diagnostic_codes(projection.diagnostics()), 227 ["imeta_field_invalid", "imeta_alt_too_large"] 228 ); 229 } 230 231 #[test] 232 fn malformed_and_duplicate_normalized_ask_markers_are_distinct() { 233 let malformed = project_inbound_post_parts( 234 KIND_POST, 235 &[vec![ 236 "t".to_string(), 237 "RADROOTS-ASK".to_string(), 238 "extra".to_string(), 239 ]], 240 "Question", 241 ) 242 .unwrap(); 243 assert_eq!( 244 malformed.classification(), 245 RadrootsPostClassification::Update 246 ); 247 assert_eq!( 248 diagnostic_codes(malformed.diagnostics()), 249 ["ask_marker_shape"] 250 ); 251 252 let error = project_inbound_post_parts( 253 KIND_POST, 254 &[ 255 vec!["t".to_string(), "radroots-ask".to_string()], 256 vec!["t".to_string(), " RADROOTS-ASK ".to_string()], 257 ], 258 "Question", 259 ) 260 .unwrap_err(); 261 assert_eq!(error.code(), "ask_marker_count"); 262 } 263 264 #[test] 265 fn empty_inbound_root_is_update_without_becoming_valid_authored_content() { 266 let projection = project_inbound_post_parts(KIND_POST, &[], "\t").unwrap(); 267 assert_eq!( 268 projection.classification(), 269 RadrootsPostClassification::Update 270 ); 271 assert!(AuthoredUpdate::new("\t").is_err()); 272 } 273 274 #[test] 275 fn projection_rejects_wrong_kind_and_oversized_content() { 276 assert_eq!( 277 project_inbound_post_parts(20, &[], "photo") 278 .unwrap_err() 279 .code(), 280 "invalid_kind" 281 ); 282 let oversized = "x".repeat(RADROOTS_POST_CONTENT_MAX_BYTES + 1); 283 assert_eq!( 284 project_inbound_post_parts(KIND_POST, &[], &oversized) 285 .unwrap_err() 286 .code(), 287 "post_content_too_large" 288 ); 289 } 290 291 fn qualifying_imeta(extra: Vec<&str>) -> Vec<String> { 292 let mut tag = vec![ 293 "imeta".to_string(), 294 "url https://cdn.example/harvest.webp".to_string(), 295 "x aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), 296 "m image/webp".to_string(), 297 "dim 1200x900".to_string(), 298 "size 12345".to_string(), 299 "alt Harvest".to_string(), 300 ]; 301 tag.extend(extra.into_iter().map(str::to_string)); 302 tag 303 } 304 305 fn diagnostic_codes(diagnostics: &[RadrootsPostDiagnostic]) -> Vec<&'static str> { 306 diagnostics 307 .iter() 308 .map(|diagnostic| diagnostic.code()) 309 .collect() 310 }