lib

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

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 }