lib

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

tests.rs (11822B)


      1 use super::*;
      2 
      3 fn h(character: char) -> String {
      4     crate::test_fixtures::fixture_public_key_hex(character)
      5 }
      6 
      7 fn top_event_tags() -> Vec<Vec<String>> {
      8     vec![
      9         vec!["p".to_string(), h('b')],
     10         vec!["q".to_string(), h('f')],
     11         vec!["K".to_string(), "30402".to_string()],
     12         vec!["P".to_string(), h('b')],
     13         vec!["k".to_string(), "30402".to_string()],
     14         vec!["E".to_string(), h('a'), String::new(), h('b')],
     15         vec!["e".to_string(), h('a'), String::new(), h('b')],
     16     ]
     17 }
     18 
     19 #[test]
     20 fn projects_unordered_top_event_and_preserves_supplemental_tags() {
     21     let tags = top_event_tags();
     22     let projection =
     23         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10).expect("projection");
     24     assert!(matches!(
     25         projection.position(),
     26         RadrootsInboundNip22CommentPosition::TopLevelEvent { reference }
     27             if reference.author_hint().is_some()
     28     ));
     29     assert_eq!(projection.raw_tags(), tags);
     30     assert!(projection.diagnostics().is_empty());
     31 }
     32 
     33 #[test]
     34 fn projects_top_address_with_distinct_optional_relay_hints() {
     35     let coordinate = format!("31922:{}:market", h('b'));
     36     let tags = vec![
     37         vec![
     38             "A".to_string(),
     39             coordinate.clone(),
     40             "wss://root.example".to_string(),
     41         ],
     42         vec!["K".to_string(), "31922".to_string()],
     43         vec![
     44             "P".to_string(),
     45             h('b'),
     46             "wss://participant.example".to_string(),
     47         ],
     48         vec!["a".to_string(), coordinate],
     49         vec![
     50             "e".to_string(),
     51             h('e'),
     52             "wss://revision.example".to_string(),
     53         ],
     54         vec!["k".to_string(), "31922".to_string()],
     55         vec!["p".to_string(), h('b')],
     56     ];
     57     let projection =
     58         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10).expect("projection");
     59     let RadrootsInboundNip22CommentPosition::TopLevelAddress {
     60         reference,
     61         current_revision,
     62     } = projection.position()
     63     else {
     64         panic!("top-level address");
     65     };
     66     assert_eq!(current_revision.event_id().to_hex(), h('e'));
     67     assert!(reference.relay().is_none());
     68     assert_eq!(
     69         current_revision.relay().expect("relay").as_str(),
     70         "wss://revision.example"
     71     );
     72 }
     73 
     74 #[test]
     75 fn direct_address_revision_cardinality_precedes_parent_coordinate_parsing() {
     76     let coordinate = format!("31922:{}:market", h('b'));
     77     let tags = vec![
     78         vec!["A".to_string(), coordinate, String::new()],
     79         vec!["K".to_string(), "31922".to_string()],
     80         vec!["P".to_string(), h('b')],
     81         vec![
     82             "a".to_string(),
     83             format!("031922:{}:market", h('b')),
     84             String::new(),
     85         ],
     86         vec!["k".to_string(), "31922".to_string()],
     87         vec!["p".to_string(), h('b')],
     88     ];
     89 
     90     assert_eq!(
     91         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10).unwrap_err(),
     92         RadrootsNip22CommentProjectionError::RevisionMissing { actual: 0 }
     93     );
     94 }
     95 
     96 #[test]
     97 fn nested_parent_uses_author_hint_and_keeps_mentions() {
     98     let mut tags = top_event_tags();
     99     tags[4][1] = KIND_COMMENT.to_string();
    100     tags[6] = vec![
    101         "e".to_string(),
    102         h('c'),
    103         "wss://parent.example".to_string(),
    104         h('d'),
    105     ];
    106     tags[0][1] = h('d');
    107     tags.push(vec!["p".to_string(), h('e')]);
    108     let projection =
    109         project_nip22_comment_parts(KIND_COMMENT, &tags, "Reply", 10).expect("projection");
    110     let RadrootsInboundNip22CommentPosition::Nested { parent } = projection.position() else {
    111         panic!("nested");
    112     };
    113     assert_eq!(parent.event_id().to_hex(), h('c'));
    114     assert_eq!(parent.author().pubkey().to_hex(), h('d'));
    115     assert_eq!(projection.mentions().len(), 1);
    116     assert_eq!(projection.mentions()[0].pubkey().to_hex(), h('e'));
    117 }
    118 
    119 #[test]
    120 fn missing_parent_hint_with_multiple_participants_is_ambiguous() {
    121     let mut tags = top_event_tags();
    122     tags[4][1] = KIND_COMMENT.to_string();
    123     tags[6] = vec!["e".to_string(), h('c')];
    124     tags.push(vec!["p".to_string(), h('e')]);
    125     assert_eq!(
    126         project_nip22_comment_parts(KIND_COMMENT, &tags, "Reply", 10).unwrap_err(),
    127         RadrootsNip22CommentProjectionError::ParentAuthorAmbiguous
    128     );
    129 }
    130 
    131 #[test]
    132 fn diagnoses_same_tag_advisory_values_in_stable_order() {
    133     let mut tags = top_event_tags();
    134     tags[5] = vec![
    135         "E".to_string(),
    136         h('a'),
    137         "WSS://bad.example".to_string(),
    138         "not-an-author".to_string(),
    139     ];
    140     let raw_tag = tags[5].clone();
    141     let projection =
    142         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10).expect("projection");
    143     assert_eq!(
    144         projection
    145             .diagnostics()
    146             .iter()
    147             .map(|diagnostic| (
    148                 diagnostic.code(),
    149                 diagnostic.tag_index(),
    150                 diagnostic.raw_tag()
    151             ))
    152             .collect::<Vec<_>>(),
    153         vec![
    154             ("comment_root_relay_ignored", 5, raw_tag.as_slice()),
    155             ("comment_root_author_hint_ignored", 5, raw_tag.as_slice()),
    156         ]
    157     );
    158 }
    159 
    160 #[test]
    161 fn hard_gates_mixed_duplicate_and_inconsistent_authority() {
    162     let mut tags = top_event_tags();
    163     tags.push(vec!["A".to_string(), format!("30402:{}:listing", h('b'))]);
    164     assert_eq!(
    165         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10).unwrap_err(),
    166         RadrootsNip22CommentProjectionError::RootCardinality { actual: 2 }
    167     );
    168 
    169     let mut tags = top_event_tags();
    170     tags.push(vec!["K".to_string(), "30402".to_string()]);
    171     assert!(matches!(
    172         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10),
    173         Err(RadrootsNip22CommentProjectionError::RootKindCardinality { actual: 2 })
    174     ));
    175 
    176     let mut tags = top_event_tags();
    177     tags[3][1] = h('c');
    178     assert!(matches!(
    179         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10),
    180         Err(RadrootsNip22CommentProjectionError::RootAuthorMismatch { .. })
    181     ));
    182 }
    183 
    184 #[test]
    185 fn rejects_reserved_external_root_and_parent_authority_forms() {
    186     let mut external_root = top_event_tags();
    187     external_root.push(vec![
    188         "I".to_string(),
    189         "https://example.com/root".to_string(),
    190     ]);
    191     assert!(matches!(
    192         project_nip22_comment_parts(KIND_COMMENT, &external_root, "Comment", 10),
    193         Err(RadrootsNip22CommentProjectionError::RootFormUnsupported { .. })
    194     ));
    195 
    196     let mut external_parent = top_event_tags();
    197     external_parent.push(vec![
    198         "i".to_string(),
    199         "https://example.com/parent".to_string(),
    200     ]);
    201     assert!(matches!(
    202         project_nip22_comment_parts(KIND_COMMENT, &external_parent, "Comment", 10),
    203         Err(RadrootsNip22CommentProjectionError::ParentFormUnsupported { .. })
    204     ));
    205 }
    206 
    207 #[test]
    208 fn hard_gates_noncanonical_kind_tokens_and_valid_hint_mismatches() {
    209     for value in ["030402", "+30402"] {
    210         let mut tags = top_event_tags();
    211         tags[2][1] = value.to_string();
    212         assert!(matches!(
    213             project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10),
    214             Err(RadrootsNip22CommentProjectionError::RootKindUnsupported { .. })
    215         ));
    216     }
    217 
    218     let mut tags = top_event_tags();
    219     tags[5][3] = h('c');
    220     assert!(matches!(
    221         project_nip22_comment_parts(KIND_COMMENT, &tags, "Comment", 10),
    222         Err(RadrootsNip22CommentProjectionError::RootAuthorMismatch { tag_index: 5 })
    223     ));
    224 }
    225 
    226 #[test]
    227 fn uses_unicode_white_space_for_blank_content() {
    228     let tags = top_event_tags();
    229     project_nip22_comment_parts(KIND_COMMENT, &tags, "\u{001c}", 10)
    230         .expect("U+001C is not Unicode White_Space");
    231     assert_eq!(
    232         project_nip22_comment_parts(KIND_COMMENT, &tags, "\u{00a0}", 10).unwrap_err(),
    233         RadrootsNip22CommentProjectionError::ContentMissing
    234     );
    235 }
    236 
    237 #[test]
    238 fn enforces_inbound_structural_tag_limits_exactly() {
    239     let mut exact_tag_count = top_event_tags();
    240     exact_tag_count.extend(
    241         (exact_tag_count.len()..RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT)
    242             .map(|_| vec!["x".to_string()]),
    243     );
    244     project_nip22_comment_parts(KIND_COMMENT, &exact_tag_count, "Comment", 10)
    245         .expect("exact tag-count limit");
    246 
    247     let mut overflow_tag_count = exact_tag_count;
    248     overflow_tag_count.push(vec!["x".to_string()]);
    249     assert_eq!(
    250         project_nip22_comment_parts(KIND_COMMENT, &overflow_tag_count, "Comment", 10).unwrap_err(),
    251         RadrootsNip22CommentProjectionError::TagCountExceeded {
    252             max: RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT,
    253             actual: RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT + 1,
    254         }
    255     );
    256 
    257     let mut exact_element_count = top_event_tags();
    258     let authority_element_count = exact_element_count.iter().map(Vec::len).sum::<usize>();
    259     let supplemental_element_count =
    260         RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT - authority_element_count;
    261     let supplemental_tag_count = RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT - exact_element_count.len();
    262     let four_element_tags = supplemental_tag_count - 10;
    263     exact_element_count.extend((0..four_element_tags).map(|_| {
    264         vec![
    265             "x".to_string(),
    266             "x".to_string(),
    267             "x".to_string(),
    268             "x".to_string(),
    269         ]
    270     }));
    271     exact_element_count.extend((0..10).map(|_| {
    272         vec![
    273             "x".to_string(),
    274             "x".to_string(),
    275             "x".to_string(),
    276             "x".to_string(),
    277             "x".to_string(),
    278         ]
    279     }));
    280     assert_eq!(
    281         exact_element_count.iter().map(Vec::len).sum::<usize>(),
    282         supplemental_element_count + authority_element_count
    283     );
    284     project_nip22_comment_parts(KIND_COMMENT, &exact_element_count, "Comment", 10)
    285         .expect("exact total tag-element limit");
    286 
    287     let mut overflow_element_count = exact_element_count;
    288     overflow_element_count[0].push("x".to_string());
    289     assert_eq!(
    290         project_nip22_comment_parts(KIND_COMMENT, &overflow_element_count, "Comment", 10)
    291             .unwrap_err(),
    292         RadrootsNip22CommentProjectionError::TagElementCountExceeded {
    293             max: RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT,
    294             actual: RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT + 1,
    295         }
    296     );
    297 }
    298 
    299 #[test]
    300 fn enforces_inbound_aggregate_tag_budget_exactly() {
    301     let mut tags = top_event_tags();
    302     let current = tags
    303         .iter()
    304         .flat_map(|tag| tag.iter())
    305         .map(String::len)
    306         .sum::<usize>();
    307     let remaining = RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES - current;
    308     while tags
    309         .iter()
    310         .flat_map(|tag| tag.iter())
    311         .map(String::len)
    312         .sum::<usize>()
    313         + RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES
    314         <= RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES
    315     {
    316         tags.push(vec![
    317             String::new(),
    318             "x".repeat(RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES),
    319         ]);
    320     }
    321     let now = tags
    322         .iter()
    323         .flat_map(|tag| tag.iter())
    324         .map(String::len)
    325         .sum::<usize>();
    326     tags.push(vec![String::new(), "x".repeat(remaining - (now - current))]);
    327     validate_tag_and_wire_budgets(&tags, "Comment", 10).expect("exact aggregate tag budget");
    328     tags.last_mut().expect("last tag")[1].push('x');
    329     assert!(matches!(
    330         validate_tag_and_wire_budgets(&tags, "Comment", 10),
    331         Err(RadrootsNip22CommentProjectionError::TagBytesExceeded { max, actual })
    332             if max == RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES && actual == max + 1
    333     ));
    334 }
    335 
    336 #[test]
    337 fn verified_projection_api_cannot_accept_an_unverified_envelope() {
    338     fn project(
    339         event: &RadrootsSignatureVerifiedEvent,
    340     ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> {
    341         project_verified_nip22_comment_event(event)
    342     }
    343     let _ = project;
    344 }