lib

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

tests.rs (12943B)


      1 use super::*;
      2 
      3 fn h(character: char) -> String {
      4     crate::test_fixtures::fixture_public_key_hex(character)
      5 }
      6 
      7 #[test]
      8 fn tolerant_inbound_accepts_blank_content_and_absent_participants() {
      9     let projection = project_nip10_reply_parts(
     10         KIND_POST,
     11         &[vec![
     12             "e".to_string(),
     13             h('a'),
     14             String::new(),
     15             "root".to_string(),
     16         ]],
     17         "",
     18         10,
     19     )
     20     .expect("optional participants and blank content do not erase a Reply");
     21 
     22     assert!(projection.is_direct());
     23     assert!(projection.participants().is_empty());
     24     assert_eq!(
     25         projection
     26             .diagnostics()
     27             .iter()
     28             .map(RadrootsNip10ReplyDiagnostic::code)
     29             .collect::<Vec<_>>(),
     30         vec!["reply_author_missing_ignored"]
     31     );
     32     assert_eq!(projection.diagnostics()[0].tag_index(), None);
     33     assert_eq!(projection.diagnostics()[0].raw_tag(), None);
     34 }
     35 
     36 #[test]
     37 fn tolerant_inbound_retains_raw_optional_metadata_and_orders_diagnostics() {
     38     let root_id = h('a');
     39     let parent_id = h('d');
     40     let participant = h('b');
     41     let parent_author_hint = h('c');
     42     let tags = vec![
     43         vec![
     44             "e".to_string(),
     45             root_id.clone(),
     46             "https://relay.example".to_string(),
     47             "root".to_string(),
     48             "not-a-pubkey".to_string(),
     49         ],
     50         vec![
     51             "e".to_string(),
     52             parent_id.clone(),
     53             String::new(),
     54             "reply".to_string(),
     55             parent_author_hint,
     56         ],
     57         vec!["p".to_string()],
     58         vec!["p".to_string(), "not-a-pubkey".to_string()],
     59         vec![
     60             "p".to_string(),
     61             participant.clone(),
     62             "https://relay.example".to_string(),
     63         ],
     64         vec!["p".to_string(), participant.clone()],
     65     ];
     66 
     67     let projection = project_nip10_reply_parts(KIND_POST, &tags, "Reply", 10)
     68         .expect("malformed optional metadata must not erase a Reply");
     69 
     70     assert!(!projection.is_direct());
     71     assert_eq!(projection.root().tag_index(), 0);
     72     assert_eq!(projection.root().raw_tag(), tags[0]);
     73     assert_eq!(projection.root().event_id().to_hex(), root_id);
     74     assert!(projection.root().relay().is_none());
     75     assert!(projection.root().author_hint().is_none());
     76     let parent = projection.reply_reference().expect("reply reference");
     77     assert_eq!(parent.tag_index(), 1);
     78     assert_eq!(parent.raw_tag(), tags[1]);
     79     assert_eq!(parent.event_id().to_hex(), parent_id);
     80     assert_eq!(projection.participants().len(), 1);
     81     assert_eq!(projection.participants()[0].tag_index(), 4);
     82     assert_eq!(projection.participants()[0].raw_tag(), tags[4]);
     83     assert_eq!(projection.participants()[0].pubkey().to_hex(), participant);
     84     assert!(projection.participants()[0].relay().is_none());
     85 
     86     assert_eq!(
     87         projection
     88             .diagnostics()
     89             .iter()
     90             .map(RadrootsNip10ReplyDiagnostic::code)
     91             .collect::<Vec<_>>(),
     92         vec![
     93             "reply_reference_relay_ignored",
     94             "reply_reference_author_ignored",
     95             "reply_author_shape_ignored",
     96             "reply_author_invalid_ignored",
     97             "reply_author_relay_ignored",
     98             "reply_author_duplicate_ignored",
     99             "reply_author_mismatch_ignored",
    100         ]
    101     );
    102     assert_eq!(
    103         projection
    104             .diagnostics()
    105             .iter()
    106             .map(RadrootsNip10ReplyDiagnostic::tag_index)
    107             .collect::<Vec<_>>(),
    108         vec![
    109             Some(0),
    110             Some(0),
    111             Some(2),
    112             Some(3),
    113             Some(4),
    114             Some(5),
    115             Some(1),
    116         ]
    117     );
    118     for diagnostic in projection.diagnostics() {
    119         let Some(tag_index) = diagnostic.tag_index() else {
    120             continue;
    121         };
    122         assert_eq!(diagnostic.raw_tag().expect("source tag"), tags[tag_index]);
    123     }
    124 }
    125 
    126 #[test]
    127 fn inbound_projection_uses_the_canonical_relay_hint_profile() {
    128     let root_id = h('a');
    129     let root_author = h('b');
    130     for relay in [
    131         "wss://%65xample.com",
    132         "wss://127.1",
    133         "wss://relay.example:01",
    134         "wss://[2001:0db8::1]",
    135         "wss://relay.example/%2f",
    136     ] {
    137         let root_tag = vec![
    138             "e".to_string(),
    139             root_id.clone(),
    140             relay.to_string(),
    141             "root".to_string(),
    142         ];
    143         let tags = vec![root_tag.clone(), vec!["p".to_string(), root_author.clone()]];
    144         let projection = project_nip10_reply_parts(KIND_POST, &tags, "Reply", 10)
    145             .unwrap_or_else(|error| panic!("{relay} must remain advisory: {error}"));
    146 
    147         assert!(projection.root().relay().is_none(), "{relay}");
    148         assert_eq!(projection.root().raw_tag(), root_tag);
    149         assert_eq!(
    150             projection
    151                 .diagnostics()
    152                 .iter()
    153                 .map(RadrootsNip10ReplyDiagnostic::code)
    154                 .collect::<Vec<_>>(),
    155             vec!["reply_reference_relay_ignored"],
    156             "{relay}"
    157         );
    158         assert_eq!(
    159             projection.diagnostics()[0].raw_tag(),
    160             Some(root_tag.as_slice()),
    161             "{relay}"
    162         );
    163     }
    164 
    165     let tags = vec![
    166         vec![
    167             "e".to_string(),
    168             root_id,
    169             "wss://[2001:db8::1]:65535/nostr?region=ca-bc".to_string(),
    170             "root".to_string(),
    171         ],
    172         vec![
    173             "p".to_string(),
    174             root_author,
    175             "ws://127.0.0.1:21003".to_string(),
    176         ],
    177     ];
    178     let projection = project_nip10_reply_parts(KIND_POST, &tags, "Reply", 10)
    179         .expect("canonical reference and participant relays");
    180     assert_eq!(
    181         projection.root().relay().expect("root relay").as_str(),
    182         "wss://[2001:db8::1]:65535/nostr?region=ca-bc"
    183     );
    184     assert_eq!(
    185         projection.participants()[0]
    186             .relay()
    187             .expect("participant relay")
    188             .as_str(),
    189         "ws://127.0.0.1:21003"
    190     );
    191     assert!(projection.diagnostics().is_empty());
    192 }
    193 
    194 #[test]
    195 fn inbound_relay_syntax_and_tag_element_budgets_remain_separate() {
    196     let prefix = "wss://relay.example/";
    197     let relay = format!(
    198         "{prefix}{}",
    199         "a".repeat(RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1 - prefix.len())
    200     );
    201     assert_eq!(relay.len(), RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1);
    202     NostrRelayHint::parse(&relay).expect("relay syntax has no Reply wire budget");
    203 
    204     let tags = vec![
    205         vec!["e".to_string(), h('a'), relay, "root".to_string()],
    206         vec!["p".to_string(), h('b')],
    207     ];
    208     assert!(matches!(
    209         project_nip10_reply_parts(KIND_POST, &tags, "Reply", 10),
    210         Err(RadrootsNip10ReplyProjectionError::TagElementTooLarge {
    211             max: RADROOTS_POST_TAG_ELEMENT_MAX_BYTES,
    212             actual,
    213             tag_index: 0,
    214             element_index: 2,
    215         }) if actual == RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1
    216     ));
    217 }
    218 
    219 #[test]
    220 fn tolerant_inbound_keeps_reference_ids_and_markers_as_hard_gates() {
    221     let author = h('b');
    222     for (tag, expected) in [
    223         (
    224             vec![
    225                 "e".to_string(),
    226                 "not-an-event-id".to_string(),
    227                 String::new(),
    228                 "root".to_string(),
    229             ],
    230             "reply_event_id_invalid",
    231         ),
    232         (
    233             vec![
    234                 "e".to_string(),
    235                 h('a'),
    236                 String::new(),
    237                 "mention".to_string(),
    238             ],
    239             "reply_marker_missing",
    240         ),
    241     ] {
    242         let error = project_nip10_reply_parts(
    243             KIND_POST,
    244             &[tag, vec!["p".to_string(), author.clone()]],
    245             "Reply",
    246             10,
    247         )
    248         .unwrap_err();
    249         assert_eq!(error.code(), expected);
    250     }
    251 }
    252 
    253 #[test]
    254 fn marked_inbound_retains_citations_and_ignores_malformed_supplements() {
    255     let root_id = h('a');
    256     let citation_id = h('c');
    257     let tags = vec![
    258         vec!["e".to_string()],
    259         vec![
    260             "e".to_string(),
    261             citation_id.clone(),
    262             "wss://relay.example".to_string(),
    263             String::new(),
    264             h('b'),
    265         ],
    266         vec!["e".to_string(), "not-an-event-id".to_string()],
    267         vec![
    268             "e".to_string(),
    269             h('d'),
    270             String::new(),
    271             "mention".to_string(),
    272         ],
    273         vec![
    274             "e".to_string(),
    275             root_id.clone(),
    276             String::new(),
    277             "root".to_string(),
    278         ],
    279     ];
    280 
    281     let projection = project_nip10_reply_parts(KIND_POST, &tags, "Reply", 10)
    282         .expect("supplemental references must not erase a marked Reply");
    283 
    284     assert_eq!(projection.root().event_id().to_hex(), root_id);
    285     assert_eq!(projection.citations().len(), 1);
    286     assert_eq!(projection.citations()[0].tag_index(), 1);
    287     assert_eq!(projection.citations()[0].raw_tag(), tags[1]);
    288     assert_eq!(projection.citations()[0].event_id().to_hex(), citation_id);
    289     assert_eq!(
    290         projection.citations()[0]
    291             .relay()
    292             .expect("citation relay")
    293             .as_str(),
    294         "wss://relay.example"
    295     );
    296     assert_eq!(
    297         projection.citations()[0]
    298             .author_hint()
    299             .expect("citation author")
    300             .to_hex(),
    301         h('b')
    302     );
    303     assert_eq!(
    304         projection
    305             .diagnostics()
    306             .iter()
    307             .map(RadrootsNip10ReplyDiagnostic::code)
    308             .collect::<Vec<_>>(),
    309         vec![
    310             "reply_citation_shape_ignored",
    311             "reply_citation_event_id_ignored",
    312             "reply_citation_marker_ignored",
    313             "reply_author_missing_ignored",
    314             "reply_author_mismatch_ignored",
    315         ]
    316     );
    317     assert_eq!(
    318         projection
    319             .diagnostics()
    320             .iter()
    321             .map(RadrootsNip10ReplyDiagnostic::tag_index)
    322             .collect::<Vec<_>>(),
    323         vec![Some(0), Some(2), Some(3), None, Some(1)]
    324     );
    325 }
    326 
    327 #[test]
    328 fn positional_inbound_accepts_empty_markers_and_tolerates_middle_citations() {
    329     let root_id = h('a');
    330     let root_author = h('b');
    331     let direct_tags = vec![
    332         vec![
    333             "e".to_string(),
    334             root_id.clone(),
    335             "wss://root.relay.example".to_string(),
    336             String::new(),
    337             root_author.clone(),
    338         ],
    339         vec!["p".to_string(), root_author.clone()],
    340     ];
    341     let direct = project_nip10_reply_parts(KIND_POST, &direct_tags, "Direct", 10)
    342         .expect("empty marker and fifth author are valid positional input");
    343     assert_eq!(direct.style(), RadrootsNip10ReplyStyle::LegacyPositional);
    344     assert!(direct.is_direct());
    345     assert_eq!(
    346         direct
    347             .root()
    348             .author_hint()
    349             .expect("root author hint")
    350             .to_hex(),
    351         root_author
    352     );
    353     assert!(direct.diagnostics().is_empty());
    354 
    355     let parent_id = h('c');
    356     let parent_author = h('d');
    357     let citation_id = h('e');
    358     let citation_author = h('f');
    359     let nested_tags = vec![
    360         vec![
    361             "e".to_string(),
    362             root_id,
    363             String::new(),
    364             String::new(),
    365             root_author.clone(),
    366         ],
    367         vec![
    368             "e".to_string(),
    369             "1".repeat(64),
    370             String::new(),
    371             "mention".to_string(),
    372         ],
    373         vec![
    374             "e".to_string(),
    375             citation_id.clone(),
    376             String::new(),
    377             String::new(),
    378             citation_author.clone(),
    379         ],
    380         vec![
    381             "e".to_string(),
    382             parent_id.clone(),
    383             "https://parent.relay.example".to_string(),
    384             String::new(),
    385             parent_author.clone(),
    386         ],
    387         vec!["p".to_string(), root_author],
    388         vec!["p".to_string(), parent_author],
    389         vec!["p".to_string(), citation_author],
    390     ];
    391     let nested = project_nip10_reply_parts(KIND_POST, &nested_tags, "Nested", 10)
    392         .expect("malformed middle citations must not erase positional anchors");
    393     assert_eq!(nested.style(), RadrootsNip10ReplyStyle::LegacyPositional);
    394     assert_eq!(nested.parent().event_id().to_hex(), parent_id);
    395     assert_eq!(nested.citations().len(), 1);
    396     assert_eq!(nested.citations()[0].tag_index(), 2);
    397     assert_eq!(nested.citations()[0].event_id().to_hex(), citation_id);
    398     assert_eq!(
    399         nested
    400             .diagnostics()
    401             .iter()
    402             .map(RadrootsNip10ReplyDiagnostic::code)
    403             .collect::<Vec<_>>(),
    404         vec![
    405             "reply_citation_marker_ignored",
    406             "reply_reference_relay_ignored",
    407         ]
    408     );
    409     assert_eq!(nested.diagnostics()[0].tag_index(), Some(1));
    410     assert_eq!(
    411         nested.diagnostics()[0].raw_tag(),
    412         Some(nested_tags[1].as_slice())
    413     );
    414     assert_eq!(nested.diagnostics()[1].tag_index(), Some(3));
    415     assert_eq!(
    416         nested.diagnostics()[1].raw_tag(),
    417         Some(nested_tags[3].as_slice())
    418     );
    419 }