lib

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

list.rs (12184B)


      1 use radroots_events::{
      2     kinds::{KIND_LIST_MUTE, KIND_LIST_READ_WRITE_RELAYS, KIND_LIST_SET_FOLLOW, KIND_POST},
      3     list::{RadrootsList, RadrootsListEntry},
      4     tags::TAG_R,
      5 };
      6 use radroots_events_codec::error::{EventEncodeError, EventParseError};
      7 use radroots_events_codec::list::decode::{
      8     data_from_event, list_entries_from_tags, list_from_tags, parsed_from_event,
      9 };
     10 use radroots_events_codec::list::encode::{list_build_tags, to_wire_parts_with_kind};
     11 
     12 fn sample_list() -> RadrootsList {
     13     RadrootsList {
     14         content: "private".to_string(),
     15         entries: vec![
     16             RadrootsListEntry {
     17                 tag: "p".to_string(),
     18                 values: vec!["pubkey".to_string()],
     19             },
     20             RadrootsListEntry {
     21                 tag: "t".to_string(),
     22                 values: vec!["orchard".to_string()],
     23             },
     24         ],
     25     }
     26 }
     27 
     28 #[test]
     29 fn list_build_tags_and_decode_roundtrip() {
     30     let list = sample_list();
     31     let tags = list_build_tags(&list).unwrap();
     32     let decoded = list_from_tags(KIND_LIST_MUTE, list.content.clone(), &tags).unwrap();
     33     assert_eq!(decoded.content, list.content);
     34     assert_eq!(decoded.entries.len(), list.entries.len());
     35     assert_eq!(decoded.entries[0].tag, list.entries[0].tag);
     36     assert_eq!(decoded.entries[0].values, list.entries[0].values);
     37     assert_eq!(decoded.entries[1].tag, list.entries[1].tag);
     38     assert_eq!(decoded.entries[1].values, list.entries[1].values);
     39 }
     40 
     41 #[test]
     42 fn list_encode_and_decode_reject_invalid_inputs() {
     43     let invalid = RadrootsList {
     44         content: "".to_string(),
     45         entries: vec![RadrootsListEntry {
     46             tag: " ".to_string(),
     47             values: vec!["pubkey".to_string()],
     48         }],
     49     };
     50     let err = list_build_tags(&invalid).unwrap_err();
     51     assert!(matches!(
     52         err,
     53         EventEncodeError::EmptyRequiredField("entry.tag")
     54     ));
     55 
     56     let invalid = RadrootsList {
     57         content: "".to_string(),
     58         entries: vec![RadrootsListEntry {
     59             tag: "p".to_string(),
     60             values: vec![" ".to_string()],
     61         }],
     62     };
     63     let err = list_build_tags(&invalid).unwrap_err();
     64     assert!(matches!(
     65         err,
     66         EventEncodeError::EmptyRequiredField("entry.values")
     67     ));
     68 
     69     let invalid = RadrootsList {
     70         content: "".to_string(),
     71         entries: vec![RadrootsListEntry {
     72             tag: "p".to_string(),
     73             values: Vec::new(),
     74         }],
     75     };
     76     let err = list_build_tags(&invalid).unwrap_err();
     77     assert!(matches!(
     78         err,
     79         EventEncodeError::EmptyRequiredField("entry.values")
     80     ));
     81 
     82     let err = to_wire_parts_with_kind(&invalid, KIND_LIST_MUTE).unwrap_err();
     83     assert!(matches!(
     84         err,
     85         EventEncodeError::EmptyRequiredField("entry.values")
     86     ));
     87 
     88     let err = to_wire_parts_with_kind(&sample_list(), KIND_POST).unwrap_err();
     89     assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST)));
     90 
     91     let err = list_from_tags(KIND_POST, "private".to_string(), &[]).unwrap_err();
     92     assert!(matches!(
     93         err,
     94         EventParseError::InvalidKind {
     95             expected: "nip51 standard or list-set kind",
     96             got: KIND_POST
     97         }
     98     ));
     99 }
    100 
    101 #[test]
    102 fn list_set_kind_roundtrips_generic_entries() {
    103     let list = sample_list();
    104     let parts = to_wire_parts_with_kind(&list, KIND_LIST_SET_FOLLOW).unwrap();
    105 
    106     assert_eq!(parts.kind, KIND_LIST_SET_FOLLOW);
    107     let decoded = list_from_tags(parts.kind, parts.content, &parts.tags).unwrap();
    108     assert_eq!(decoded.entries.len(), list.entries.len());
    109     assert_eq!(decoded.entries[0].tag, "p");
    110 }
    111 
    112 #[test]
    113 fn list_entries_from_tags_rejects_empty_entry_fields() {
    114     let err = list_entries_from_tags(&[vec!["".to_string(), "x".to_string()]]).unwrap_err();
    115     assert!(matches!(err, EventParseError::InvalidTag("tag")));
    116 
    117     let err = list_entries_from_tags(&[vec!["p".to_string(), " ".to_string()]]).unwrap_err();
    118     assert!(matches!(err, EventParseError::InvalidTag("tag")));
    119 
    120     let err = list_from_tags(
    121         KIND_LIST_MUTE,
    122         "private".to_string(),
    123         &[vec!["".to_string(), "x".to_string()]],
    124     )
    125     .unwrap_err();
    126     assert!(matches!(err, EventParseError::InvalidTag("tag")));
    127 }
    128 
    129 #[test]
    130 fn list_metadata_and_index_from_event_roundtrip() {
    131     let list = sample_list();
    132     let parts = to_wire_parts_with_kind(&list, KIND_LIST_MUTE).unwrap();
    133 
    134     let metadata = data_from_event(
    135         "id".to_string(),
    136         "author".to_string(),
    137         44,
    138         KIND_LIST_MUTE,
    139         parts.content.clone(),
    140         parts.tags.clone(),
    141     )
    142     .unwrap();
    143     assert_eq!(metadata.id, "id");
    144     assert_eq!(metadata.author, "author");
    145     assert_eq!(metadata.published_at, 44);
    146     assert_eq!(metadata.kind, KIND_LIST_MUTE);
    147     assert_eq!(metadata.data.content, list.content);
    148     assert_eq!(metadata.data.entries.len(), list.entries.len());
    149     assert_eq!(metadata.data.entries[0].tag, list.entries[0].tag);
    150     assert_eq!(metadata.data.entries[0].values, list.entries[0].values);
    151 
    152     let index = parsed_from_event(
    153         "id".to_string(),
    154         "author".to_string(),
    155         44,
    156         KIND_LIST_MUTE,
    157         parts.content,
    158         parts.tags,
    159         "sig".to_string(),
    160     )
    161     .unwrap();
    162     assert_eq!(index.event.kind, KIND_LIST_MUTE);
    163     assert_eq!(index.event.sig, "sig");
    164     assert_eq!(index.data.data.entries.len(), 2);
    165 }
    166 
    167 #[test]
    168 fn list_index_from_event_propagates_parse_errors() {
    169     let err = parsed_from_event(
    170         "id".to_string(),
    171         "author".to_string(),
    172         44,
    173         KIND_POST,
    174         "private".to_string(),
    175         Vec::new(),
    176         "sig".to_string(),
    177     )
    178     .unwrap_err();
    179     assert!(matches!(
    180         err,
    181         EventParseError::InvalidKind {
    182             expected: "nip51 standard or list-set kind",
    183             got: KIND_POST
    184         }
    185     ));
    186 }
    187 
    188 #[test]
    189 fn relay_list_kind_roundtrips_nip65_r_tags() {
    190     let list = RadrootsList {
    191         content: String::new(),
    192         entries: vec![
    193             RadrootsListEntry {
    194                 tag: TAG_R.to_string(),
    195                 values: vec!["wss://read.example.test".to_string(), "read".to_string()],
    196             },
    197             RadrootsListEntry {
    198                 tag: TAG_R.to_string(),
    199                 values: vec!["wss://write.example.test".to_string(), "write".to_string()],
    200             },
    201             RadrootsListEntry {
    202                 tag: TAG_R.to_string(),
    203                 values: vec!["wss://both.example.test".to_string()],
    204             },
    205             RadrootsListEntry {
    206                 tag: TAG_R.to_string(),
    207                 values: vec!["ws://local-relay.example.test".to_string()],
    208             },
    209         ],
    210     };
    211 
    212     let parts = to_wire_parts_with_kind(&list, KIND_LIST_READ_WRITE_RELAYS).unwrap();
    213     assert_eq!(parts.kind, KIND_LIST_READ_WRITE_RELAYS);
    214     assert!(parts.content.is_empty());
    215     assert_eq!(parts.tags.len(), 4);
    216 
    217     let decoded = list_from_tags(parts.kind, parts.content, &parts.tags).unwrap();
    218     assert_eq!(decoded.entries.len(), 4);
    219     assert_eq!(decoded.entries[0].values[1], "read");
    220     assert_eq!(decoded.entries[1].values[1], "write");
    221     assert_eq!(decoded.entries[2].values.len(), 1);
    222     assert_eq!(
    223         decoded.entries[3].values[0],
    224         "ws://local-relay.example.test"
    225     );
    226 }
    227 
    228 #[test]
    229 fn relay_list_kind_validates_url_shape_and_markers() {
    230     let invalid_tag = RadrootsList {
    231         content: String::new(),
    232         entries: vec![RadrootsListEntry {
    233             tag: "p".to_string(),
    234             values: vec!["wss://relay.example.test".to_string()],
    235         }],
    236     };
    237     assert!(matches!(
    238         to_wire_parts_with_kind(&invalid_tag, KIND_LIST_READ_WRITE_RELAYS),
    239         Err(EventEncodeError::InvalidField("relay.tag"))
    240     ));
    241     assert!(matches!(
    242         list_from_tags(
    243             KIND_LIST_READ_WRITE_RELAYS,
    244             String::new(),
    245             &[vec![
    246                 "p".to_string(),
    247                 "wss://relay.example.test".to_string()
    248             ]]
    249         ),
    250         Err(EventParseError::InvalidTag(TAG_R))
    251     ));
    252 
    253     let missing_url = RadrootsList {
    254         content: String::new(),
    255         entries: vec![RadrootsListEntry {
    256             tag: TAG_R.to_string(),
    257             values: Vec::new(),
    258         }],
    259     };
    260     assert!(matches!(
    261         to_wire_parts_with_kind(&missing_url, KIND_LIST_READ_WRITE_RELAYS),
    262         Err(EventEncodeError::EmptyRequiredField("relay.url"))
    263     ));
    264     assert!(matches!(
    265         list_from_tags(
    266             KIND_LIST_READ_WRITE_RELAYS,
    267             String::new(),
    268             &[vec![TAG_R.to_string()]]
    269         ),
    270         Err(EventParseError::InvalidTag(TAG_R))
    271     ));
    272 
    273     let invalid_url = RadrootsList {
    274         content: String::new(),
    275         entries: vec![RadrootsListEntry {
    276             tag: TAG_R.to_string(),
    277             values: vec!["https://relay.example.test".to_string()],
    278         }],
    279     };
    280     assert!(matches!(
    281         to_wire_parts_with_kind(&invalid_url, KIND_LIST_READ_WRITE_RELAYS),
    282         Err(EventEncodeError::InvalidField("relay.url"))
    283     ));
    284     assert!(matches!(
    285         list_from_tags(
    286             KIND_LIST_READ_WRITE_RELAYS,
    287             String::new(),
    288             &[vec![
    289                 TAG_R.to_string(),
    290                 "https://relay.example.test".to_string()
    291             ]]
    292         ),
    293         Err(EventParseError::InvalidTag(TAG_R))
    294     ));
    295 
    296     for invalid_empty_relay in ["wss://", "ws://"] {
    297         let invalid_empty_url = RadrootsList {
    298             content: String::new(),
    299             entries: vec![RadrootsListEntry {
    300                 tag: TAG_R.to_string(),
    301                 values: vec![invalid_empty_relay.to_string()],
    302             }],
    303         };
    304         assert!(matches!(
    305             to_wire_parts_with_kind(&invalid_empty_url, KIND_LIST_READ_WRITE_RELAYS),
    306             Err(EventEncodeError::InvalidField("relay.url"))
    307         ));
    308         assert!(matches!(
    309             list_from_tags(
    310                 KIND_LIST_READ_WRITE_RELAYS,
    311                 String::new(),
    312                 &[vec![TAG_R.to_string(), invalid_empty_relay.to_string()]]
    313             ),
    314             Err(EventParseError::InvalidTag(TAG_R))
    315         ));
    316     }
    317 
    318     let invalid_marker = RadrootsList {
    319         content: String::new(),
    320         entries: vec![RadrootsListEntry {
    321             tag: TAG_R.to_string(),
    322             values: vec!["wss://relay.example.test".to_string(), "both".to_string()],
    323         }],
    324     };
    325     assert!(matches!(
    326         to_wire_parts_with_kind(&invalid_marker, KIND_LIST_READ_WRITE_RELAYS),
    327         Err(EventEncodeError::InvalidField("relay.marker"))
    328     ));
    329     assert!(matches!(
    330         list_from_tags(
    331             KIND_LIST_READ_WRITE_RELAYS,
    332             String::new(),
    333             &[vec![
    334                 TAG_R.to_string(),
    335                 "wss://relay.example.test".to_string(),
    336                 "both".to_string()
    337             ]]
    338         ),
    339         Err(EventParseError::InvalidTag(TAG_R))
    340     ));
    341 
    342     let extra_marker = RadrootsList {
    343         content: String::new(),
    344         entries: vec![RadrootsListEntry {
    345             tag: TAG_R.to_string(),
    346             values: vec![
    347                 "wss://relay.example.test".to_string(),
    348                 "read".to_string(),
    349                 "write".to_string(),
    350             ],
    351         }],
    352     };
    353     assert!(matches!(
    354         to_wire_parts_with_kind(&extra_marker, KIND_LIST_READ_WRITE_RELAYS),
    355         Err(EventEncodeError::InvalidField("relay.marker"))
    356     ));
    357     assert!(matches!(
    358         list_from_tags(
    359             KIND_LIST_READ_WRITE_RELAYS,
    360             String::new(),
    361             &[vec![
    362                 TAG_R.to_string(),
    363                 "wss://relay.example.test".to_string(),
    364                 "read".to_string(),
    365                 "write".to_string()
    366             ]]
    367         ),
    368         Err(EventParseError::InvalidTag(TAG_R))
    369     ));
    370 
    371     let empty = RadrootsList {
    372         content: String::new(),
    373         entries: Vec::new(),
    374     };
    375     assert!(matches!(
    376         to_wire_parts_with_kind(&empty, KIND_LIST_READ_WRITE_RELAYS),
    377         Err(EventEncodeError::EmptyRequiredField("relay.entries"))
    378     ));
    379     assert!(matches!(
    380         list_from_tags(KIND_LIST_READ_WRITE_RELAYS, String::new(), &[]),
    381         Err(EventParseError::MissingTag(TAG_R))
    382     ));
    383 }