lib

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

list.rs (12752B)


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