tests.rs (14341B)
1 use super::*; 2 3 fn h(character: char) -> String { 4 crate::test_fixtures::fixture_public_key_hex(character) 5 } 6 7 fn valid_event_tags() -> Vec<Vec<String>> { 8 vec![ 9 vec!["e".to_string(), h('a')], 10 vec!["k".to_string(), "1".to_string()], 11 ] 12 } 13 14 #[test] 15 fn projects_raw_mixed_duplicates_trailing_and_unknown_tags() { 16 let coordinate_b = format!("30402:{}:produce", h('b')); 17 let coordinate_c = format!("31923:{}:market", h('c')); 18 let tags = vec![ 19 vec!["x".to_string(), "unknown".to_string()], 20 vec!["e".to_string(), h('f'), "relay".to_string()], 21 vec![ 22 "a".to_string(), 23 coordinate_c.clone(), 24 "trailing".to_string(), 25 ], 26 vec!["e".to_string(), h('a')], 27 vec!["e".to_string(), h('F'), "duplicate".to_string()], 28 vec!["a".to_string(), coordinate_b.clone()], 29 vec![ 30 "a".to_string(), 31 format!("030402:{}:produce", h('B')), 32 "duplicate".to_string(), 33 ], 34 vec!["k".to_string(), "31923".to_string(), "trailing".to_string()], 35 vec!["k".to_string(), "1".to_string()], 36 ]; 37 let projection = project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, "", 1) 38 .expect("projection"); 39 40 assert_eq!(projection.raw_tags(), tags); 41 assert_eq!( 42 projection 43 .event_targets() 44 .iter() 45 .map(|target| (target.event_id().to_hex(), target.tag_index())) 46 .collect::<Vec<_>>(), 47 vec![(h('a'), 3), (h('f'), 1)] 48 ); 49 assert_eq!( 50 projection 51 .address_targets() 52 .iter() 53 .map(|target| (target.coordinate().as_str(), target.tag_index())) 54 .collect::<Vec<_>>(), 55 vec![(coordinate_b.as_str(), 5), (coordinate_c.as_str(), 2)] 56 ); 57 assert_eq!( 58 projection 59 .kind_advisories() 60 .iter() 61 .map(|advisory| (advisory.kind(), advisory.tag_index())) 62 .collect::<Vec<_>>(), 63 vec![(1, 8), (31_923, 7)] 64 ); 65 assert_eq!(projection.event_targets()[1].raw_tag(), tags[1]); 66 assert_eq!(projection.address_targets()[1].raw_tag(), tags[2]); 67 assert!(projection.diagnostics().is_empty()); 68 } 69 70 #[test] 71 fn emits_advisory_diagnostics_in_source_order() { 72 let coordinate = format!("30402:{}:produce", h('b')); 73 let tags = vec![ 74 vec!["k".to_string()], 75 vec!["a".to_string(), coordinate], 76 vec!["k".to_string(), "+30402".to_string()], 77 vec!["k".to_string(), "30402".to_string()], 78 vec![ 79 "k".to_string(), 80 "30402".to_string(), 81 "duplicate".to_string(), 82 ], 83 vec!["k".to_string(), "31923".to_string()], 84 vec!["k".to_string(), "65536".to_string()], 85 ]; 86 let projection = project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, "", 1) 87 .expect("projection"); 88 89 assert_eq!( 90 projection 91 .diagnostics() 92 .iter() 93 .map(|diagnostic| (diagnostic.code(), diagnostic.tag_index())) 94 .collect::<Vec<_>>(), 95 vec![ 96 ("deletion_kind_advisory_shape_ignored", 0), 97 ("deletion_kind_advisory_invalid_ignored", 2), 98 ("deletion_kind_advisory_duplicate_ignored", 4), 99 ("deletion_kind_advisory_conflict_ignored", 5), 100 ("deletion_kind_advisory_invalid_ignored", 6), 101 ] 102 ); 103 assert_eq!( 104 projection 105 .kind_advisories() 106 .iter() 107 .map(RadrootsInboundNip09DeletionKindAdvisory::kind) 108 .collect::<Vec<_>>(), 109 vec![30_402, 31_923] 110 ); 111 for diagnostic in projection.diagnostics() { 112 assert_eq!(diagnostic.raw_tag(), tags[diagnostic.tag_index()]); 113 } 114 } 115 116 #[test] 117 fn event_target_prevents_unprovable_kind_conflict() { 118 let tags = vec![ 119 vec!["a".to_string(), format!("30402:{}:produce", h('b'))], 120 vec!["e".to_string(), h('a')], 121 vec!["k".to_string(), "31923".to_string()], 122 ]; 123 let projection = project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, "", 1) 124 .expect("mixed projection"); 125 assert_eq!(projection.kind_advisories()[0].kind(), 31_923); 126 assert!(projection.diagnostics().is_empty()); 127 } 128 129 #[test] 130 fn accepts_empty_content_and_trailing_target_fields() { 131 let tags = vec![ 132 vec!["e".to_string(), h('a'), String::new(), "extra".to_string()], 133 vec![ 134 "a".to_string(), 135 format!("30000:{}:", h('b')), 136 "extra".to_string(), 137 ], 138 ]; 139 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, "", 1) 140 .expect("trailing fields"); 141 } 142 143 #[test] 144 fn first_malformed_target_in_source_order_is_a_hard_error() { 145 let tags = vec![ 146 vec!["a".to_string(), format!("30000:{}:", h('b'))], 147 vec!["e".to_string()], 148 vec!["a".to_string(), "bad".to_string()], 149 ]; 150 assert_eq!( 151 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, "", 1).unwrap_err(), 152 RadrootsNip09DeletionProjectionError::EventTargetShape { tag_index: 1 } 153 ); 154 155 let tags = vec![ 156 vec!["e".to_string(), h('a')], 157 vec!["a".to_string(), "bad".to_string()], 158 vec!["e".to_string(), "bad".to_string()], 159 ]; 160 assert!(matches!( 161 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, "", 1), 162 Err(RadrootsNip09DeletionProjectionError::AddressTargetInvalid { tag_index: 1, .. }) 163 )); 164 } 165 166 #[test] 167 fn missing_target_union_follows_target_shape_and_validity() { 168 assert_eq!( 169 project_nip09_deletion_request_parts( 170 KIND_DELETION_REQUEST, 171 &[vec!["x".to_string()], vec!["k".to_string()]], 172 "", 173 1, 174 ) 175 .unwrap_err(), 176 RadrootsNip09DeletionProjectionError::TargetMissing 177 ); 178 assert_eq!( 179 project_nip09_deletion_request_parts( 180 KIND_DELETION_REQUEST, 181 &[vec!["e".to_string()]], 182 "", 183 1, 184 ) 185 .unwrap_err(), 186 RadrootsNip09DeletionProjectionError::EventTargetShape { tag_index: 0 } 187 ); 188 } 189 190 #[test] 191 fn enforces_exact_error_precedence_before_target_semantics() { 192 let oversized_content = "x".repeat(RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES + 1); 193 let oversized_tags = vec![vec!["e".to_string()]; RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + 1]; 194 assert!(matches!( 195 project_nip09_deletion_request_parts(1, &oversized_tags, &oversized_content, 1), 196 Err(RadrootsNip09DeletionProjectionError::UnsupportedKind { actual: 1 }) 197 )); 198 assert!(matches!( 199 project_nip09_deletion_request_parts( 200 KIND_DELETION_REQUEST, 201 &oversized_tags, 202 &oversized_content, 203 1 204 ), 205 Err(RadrootsNip09DeletionProjectionError::ContentTooLarge { .. }) 206 )); 207 assert!(matches!( 208 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &oversized_tags, "", 1), 209 Err(RadrootsNip09DeletionProjectionError::TagCountExceeded { .. }) 210 )); 211 212 let too_many_elements = vec![ 213 vec!["x".to_string(); RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT], 214 vec!["e".to_string()], 215 ]; 216 assert_eq!( 217 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &too_many_elements, "", 1) 218 .unwrap_err(), 219 RadrootsNip09DeletionProjectionError::TagElementCountExceeded { 220 max: RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT, 221 actual: RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT + 1, 222 } 223 ); 224 225 let oversized_element = vec![ 226 vec![ 227 "x".to_string(), 228 "x".repeat(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES + 1), 229 ], 230 vec!["e".to_string()], 231 ]; 232 assert_eq!( 233 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &oversized_element, "", 1) 234 .unwrap_err(), 235 RadrootsNip09DeletionProjectionError::TagElementTooLarge { 236 max: RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, 237 actual: RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES + 1, 238 tag_index: 0, 239 element_index: 1, 240 } 241 ); 242 } 243 244 #[test] 245 fn accepts_exact_shared_resource_boundaries() { 246 let mut tag_count = valid_event_tags(); 247 tag_count.extend( 248 (tag_count.len()..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT).map(|_| vec!["x".to_string()]), 249 ); 250 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tag_count, "", 1) 251 .expect("exact tag-count boundary"); 252 253 let mut element_count = valid_event_tags(); 254 element_count.push(vec!["x".to_string(); 8]); 255 element_count.extend( 256 (element_count.len()..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT) 257 .map(|_| vec!["x".to_string(); 4]), 258 ); 259 assert_eq!( 260 element_count.iter().map(Vec::len).sum::<usize>(), 261 RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT 262 ); 263 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &element_count, "", 1) 264 .expect("exact tag-element boundary"); 265 266 let exact_element = vec![ 267 vec!["e".to_string(), h('a')], 268 vec![ 269 "x".to_string(), 270 "x".repeat(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES), 271 ], 272 ]; 273 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &exact_element, "", 1) 274 .expect("exact individual tag-element boundary"); 275 276 let mut tag_bytes = valid_event_tags(); 277 tag_bytes.extend((0..31).map(|_| { 278 vec![ 279 String::new(), 280 "x".repeat(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES), 281 ] 282 })); 283 tag_bytes.push(vec![String::new(), "x".repeat(4_029)]); 284 assert_eq!( 285 tag_bytes 286 .iter() 287 .flat_map(|tag| tag.iter()) 288 .map(String::len) 289 .sum::<usize>(), 290 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES 291 ); 292 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tag_bytes, "", 1) 293 .expect("exact aggregate tag-byte boundary"); 294 295 project_nip09_deletion_request_parts( 296 KIND_DELETION_REQUEST, 297 &valid_event_tags(), 298 &"x".repeat(RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES), 299 1, 300 ) 301 .expect("exact content boundary"); 302 } 303 304 #[test] 305 fn enforces_aggregate_tag_and_compact_wire_budgets_before_targets() { 306 let mut tag_bytes = vec![ 307 vec![ 308 String::new(), 309 "x".repeat(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES), 310 ]; 311 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES 312 / RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES 313 ]; 314 tag_bytes.push(vec!["e".to_string()]); 315 assert_eq!( 316 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tag_bytes, "", 1).unwrap_err(), 317 RadrootsNip09DeletionProjectionError::TagBytesExceeded { 318 max: RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, 319 actual: RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES + 1, 320 } 321 ); 322 323 let escaped_content = "\u{0001}".repeat(50_000); 324 assert!(matches!( 325 project_nip09_deletion_request_parts( 326 KIND_DELETION_REQUEST, 327 &[vec!["e".to_string()]], 328 &escaped_content, 329 1 330 ), 331 Err(RadrootsNip09DeletionProjectionError::EventWireTooLarge { .. }) 332 )); 333 } 334 335 #[test] 336 fn error_codes_and_messages_are_stable() { 337 let errors = [ 338 RadrootsNip09DeletionProjectionError::UnsupportedKind { actual: 1 }, 339 RadrootsNip09DeletionProjectionError::ContentTooLarge { max: 1, actual: 2 }, 340 RadrootsNip09DeletionProjectionError::TagCountExceeded { max: 1, actual: 2 }, 341 RadrootsNip09DeletionProjectionError::TagElementCountExceeded { max: 1, actual: 2 }, 342 RadrootsNip09DeletionProjectionError::TagElementTooLarge { 343 max: 1, 344 actual: 2, 345 tag_index: 3, 346 element_index: 4, 347 }, 348 RadrootsNip09DeletionProjectionError::TagBytesExceeded { max: 1, actual: 2 }, 349 RadrootsNip09DeletionProjectionError::EventWireTooLarge { max: 1, actual: 2 }, 350 RadrootsNip09DeletionProjectionError::EventTargetShape { tag_index: 0 }, 351 RadrootsNip09DeletionProjectionError::EventTargetInvalid { 352 tag_index: 0, 353 error: ParseError::InvalidFormat, 354 }, 355 RadrootsNip09DeletionProjectionError::AddressTargetShape { tag_index: 0 }, 356 RadrootsNip09DeletionProjectionError::AddressTargetInvalid { 357 tag_index: 0, 358 error: Nip01CoordinateParseError::InvalidFormat, 359 }, 360 RadrootsNip09DeletionProjectionError::TargetMissing, 361 ]; 362 let expected = [ 363 "unsupported_kind", 364 "deletion_content_too_large", 365 "deletion_tag_count_exceeded", 366 "deletion_tag_element_count_exceeded", 367 "deletion_tag_element_too_large", 368 "deletion_tag_bytes_exceeded", 369 "deletion_event_wire_too_large", 370 "deletion_event_target_shape", 371 "deletion_event_target_invalid", 372 "deletion_address_target_shape", 373 "deletion_address_target_invalid", 374 "deletion_target_missing", 375 ]; 376 for (error, expected) in errors.into_iter().zip(expected) { 377 assert_eq!(error.code(), expected); 378 assert!(!error.to_string().is_empty()); 379 } 380 } 381 382 #[test] 383 fn verified_projection_api_cannot_accept_an_unverified_envelope() { 384 fn project( 385 event: &RadrootsSignatureVerifiedEvent, 386 ) -> Result<RadrootsInboundNip09DeletionProjection, RadrootsNip09DeletionProjectionError> { 387 project_verified_nip09_deletion_request_event(event) 388 } 389 let _ = project; 390 } 391 392 #[test] 393 fn wire_estimator_uses_actual_created_at_width() { 394 let tags = valid_event_tags(); 395 let mut content = "\u{0001}".repeat(43_600); 396 while project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, &content, u64::MAX) 397 .is_ok() 398 { 399 content.push('\u{0001}'); 400 } 401 assert!(matches!( 402 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, &content, u64::MAX), 403 Err(RadrootsNip09DeletionProjectionError::EventWireTooLarge { .. }) 404 )); 405 project_nip09_deletion_request_parts(KIND_DELETION_REQUEST, &tags, &content, 1) 406 .expect("short created_at width remains within the wire budget"); 407 }