lib

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

list_set.rs (9550B)


      1 mod common;
      2 #[path = "../src/test_fixtures.rs"]
      3 mod test_fixtures;
      4 
      5 use common::{AUTHOR, EVENT_ID, EVENT_SIG};
      6 use radroots_event::{
      7     envelope::kind::{KIND_CALENDAR, KIND_LIST_SET_FOLLOW, KIND_POST, is_nip51_list_set_kind},
      8     social::list::ListEntry,
      9     social::list_set::ListSet,
     10 };
     11 use radroots_event_codec::decode::EventParseError;
     12 use radroots_event_codec::decode::list_set::{
     13     data_from_event, list_set_from_tags, parsed_from_event,
     14 };
     15 use radroots_event_codec::encode::EventEncodeError;
     16 use radroots_event_codec::encode::list_set::{list_set_build_tags, to_wire_parts_with_kind};
     17 use test_fixtures::APP_PRIMARY_HTTPS;
     18 
     19 fn app_url(path: &str) -> String {
     20     format!("{APP_PRIMARY_HTTPS}/{path}")
     21 }
     22 
     23 fn sample_list_set() -> ListSet {
     24     ListSet {
     25         d_tag: "members.owners".to_string(),
     26         content: "private".to_string(),
     27         entries: vec![
     28             ListEntry {
     29                 tag: "p".to_string(),
     30                 values: vec!["owner".to_string()],
     31             },
     32             ListEntry {
     33                 tag: "t".to_string(),
     34                 values: vec!["orchard".to_string()],
     35             },
     36         ],
     37         title: Some("owners".to_string()),
     38         description: Some("core team".to_string()),
     39         image: Some(app_url("team.png")),
     40     }
     41 }
     42 
     43 #[test]
     44 fn list_set_build_tags_and_decode_roundtrip() {
     45     let list_set = sample_list_set();
     46     let tags = list_set_build_tags(&list_set).unwrap();
     47     let decoded =
     48         list_set_from_tags(KIND_LIST_SET_FOLLOW, list_set.content.clone(), &tags).unwrap();
     49     assert_eq!(decoded.d_tag, list_set.d_tag);
     50     assert_eq!(decoded.title, list_set.title);
     51     assert_eq!(decoded.description, list_set.description);
     52     assert_eq!(decoded.image, list_set.image);
     53     assert_eq!(decoded.entries.len(), list_set.entries.len());
     54     assert_eq!(decoded.entries[0].tag, list_set.entries[0].tag);
     55     assert_eq!(decoded.entries[0].values, list_set.entries[0].values);
     56     assert_eq!(decoded.entries[1].tag, list_set.entries[1].tag);
     57     assert_eq!(decoded.entries[1].values, list_set.entries[1].values);
     58 }
     59 
     60 #[test]
     61 fn generic_list_set_decode_rejects_calendar_kind() {
     62     assert!(is_nip51_list_set_kind(KIND_CALENDAR));
     63     let tags = list_set_build_tags(&sample_list_set()).unwrap();
     64 
     65     let err = list_set_from_tags(KIND_CALENDAR, "private".to_string(), &tags).unwrap_err();
     66 
     67     assert!(matches!(
     68         err,
     69         EventParseError::InvalidKind {
     70             expected: "generic nip51 list-set kind",
     71             got: KIND_CALENDAR
     72         }
     73     ));
     74 }
     75 
     76 #[test]
     77 fn generic_list_set_encode_rejects_calendar_kind() {
     78     assert!(is_nip51_list_set_kind(KIND_CALENDAR));
     79 
     80     let err = to_wire_parts_with_kind(&sample_list_set(), KIND_CALENDAR).unwrap_err();
     81 
     82     assert!(matches!(err, EventEncodeError::InvalidKind(KIND_CALENDAR)));
     83 }
     84 
     85 #[test]
     86 fn list_set_encode_and_decode_reject_invalid_inputs() {
     87     let invalid = ListSet {
     88         d_tag: " ".to_string(),
     89         content: "".to_string(),
     90         entries: Vec::new(),
     91         title: None,
     92         description: None,
     93         image: None,
     94     };
     95     let err = list_set_build_tags(&invalid).unwrap_err();
     96     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
     97     let err = to_wire_parts_with_kind(&invalid, KIND_LIST_SET_FOLLOW).unwrap_err();
     98     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
     99 
    100     let invalid = ListSet {
    101         d_tag: "farm:invalid:owners".to_string(),
    102         content: "".to_string(),
    103         entries: Vec::new(),
    104         title: None,
    105         description: None,
    106         image: None,
    107     };
    108     let err = list_set_build_tags(&invalid).unwrap_err();
    109     assert!(matches!(err, EventEncodeError::InvalidField("d_tag")));
    110 
    111     let err = to_wire_parts_with_kind(&sample_list_set(), KIND_POST).unwrap_err();
    112     assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST)));
    113 
    114     let err = list_set_from_tags(KIND_POST, "".to_string(), &[]).unwrap_err();
    115     assert!(matches!(
    116         err,
    117         EventParseError::InvalidKind {
    118             expected: "generic nip51 list-set kind",
    119             got: KIND_POST
    120         }
    121     ));
    122 
    123     let mut invalid_entry_tag = sample_list_set();
    124     invalid_entry_tag.entries[0].tag = " ".to_string();
    125     let err = list_set_build_tags(&invalid_entry_tag).unwrap_err();
    126     assert!(matches!(
    127         err,
    128         EventEncodeError::EmptyRequiredField("entry.tag")
    129     ));
    130 
    131     let mut invalid_entry_values = sample_list_set();
    132     invalid_entry_values.entries[0].values.clear();
    133     let err = list_set_build_tags(&invalid_entry_values).unwrap_err();
    134     assert!(matches!(
    135         err,
    136         EventEncodeError::EmptyRequiredField("entry.values")
    137     ));
    138 
    139     let mut invalid_entry_first = sample_list_set();
    140     invalid_entry_first.entries[0].values = vec![" ".to_string()];
    141     let err = list_set_build_tags(&invalid_entry_first).unwrap_err();
    142     assert!(matches!(
    143         err,
    144         EventEncodeError::EmptyRequiredField("entry.values")
    145     ));
    146 }
    147 
    148 #[test]
    149 fn list_set_decode_rejects_invalid_tag_shapes() {
    150     let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "".to_string(), &[]).unwrap_err();
    151     assert!(matches!(err, EventParseError::MissingTag("d")));
    152 
    153     let err = list_set_from_tags(
    154         KIND_LIST_SET_FOLLOW,
    155         "".to_string(),
    156         &[vec!["d".to_string(), " ".to_string()]],
    157     )
    158     .unwrap_err();
    159     assert!(matches!(err, EventParseError::InvalidTag("d")));
    160 
    161     let err = list_set_from_tags(
    162         KIND_LIST_SET_FOLLOW,
    163         "".to_string(),
    164         &[vec!["".to_string(), "value".to_string()]],
    165     )
    166     .unwrap_err();
    167     assert!(matches!(err, EventParseError::InvalidTag("tag")));
    168 
    169     let err = list_set_from_tags(
    170         KIND_LIST_SET_FOLLOW,
    171         "".to_string(),
    172         &[
    173             vec!["d".to_string(), "members.owners".to_string()],
    174             vec!["p".to_string(), " ".to_string()],
    175         ],
    176     )
    177     .unwrap_err();
    178     assert!(matches!(err, EventParseError::InvalidTag("tag")));
    179 
    180     let err = list_set_from_tags(
    181         KIND_LIST_SET_FOLLOW,
    182         "".to_string(),
    183         &[
    184             vec!["d".to_string(), "farm:invalid:members".to_string()],
    185             vec!["p".to_string(), "owner".to_string()],
    186         ],
    187     )
    188     .unwrap_err();
    189     assert!(matches!(err, EventParseError::InvalidTag("d")));
    190 }
    191 
    192 #[test]
    193 fn list_set_metadata_and_index_from_event_roundtrip() {
    194     let list_set = sample_list_set();
    195     let parts = to_wire_parts_with_kind(&list_set, KIND_LIST_SET_FOLLOW).unwrap();
    196 
    197     let metadata = data_from_event(
    198         "id".to_string(),
    199         "author".to_string(),
    200         44,
    201         KIND_LIST_SET_FOLLOW,
    202         parts.content.clone(),
    203         parts.tags.clone(),
    204     )
    205     .unwrap();
    206     assert_eq!(metadata.id, "id");
    207     assert_eq!(metadata.author, "author");
    208     assert_eq!(metadata.published_at, 44);
    209     assert_eq!(metadata.kind, KIND_LIST_SET_FOLLOW);
    210     assert_eq!(metadata.data.d_tag, "members.owners");
    211 
    212     let index = parsed_from_event(
    213         EVENT_ID.to_string(),
    214         AUTHOR.to_string(),
    215         44,
    216         KIND_LIST_SET_FOLLOW,
    217         parts.content,
    218         parts.tags,
    219         EVENT_SIG.to_string(),
    220     )
    221     .unwrap();
    222     assert_eq!(index.event.kind_u32(), KIND_LIST_SET_FOLLOW);
    223     assert_eq!(index.event.signature_hex(), EVENT_SIG);
    224     assert_eq!(index.data.data.entries.len(), 2);
    225 }
    226 
    227 #[test]
    228 fn list_set_index_from_event_propagates_parse_errors() {
    229     let err = parsed_from_event(
    230         "id".to_string(),
    231         "author".to_string(),
    232         44,
    233         KIND_POST,
    234         "private".to_string(),
    235         Vec::new(),
    236         "sig".to_string(),
    237     )
    238     .unwrap_err();
    239     assert!(matches!(
    240         err,
    241         EventParseError::InvalidKind {
    242             expected: "generic nip51 list-set kind",
    243             got: KIND_POST
    244         }
    245     ));
    246 }
    247 
    248 #[test]
    249 fn list_set_decode_keeps_first_optional_display_tags() {
    250     let tags = vec![
    251         vec!["d".to_string(), "members.owners".to_string()],
    252         vec!["title".to_string(), "owners".to_string()],
    253         vec!["title".to_string(), "ignored".to_string()],
    254         vec!["description".to_string(), "team".to_string()],
    255         vec!["description".to_string(), "ignored".to_string()],
    256         vec!["image".to_string(), app_url("a.png")],
    257         vec!["image".to_string(), app_url("b.png")],
    258         vec!["p".to_string(), "owner".to_string()],
    259     ];
    260     let decoded = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags).unwrap();
    261     let expected_image = app_url("a.png");
    262     assert_eq!(decoded.title.as_deref(), Some("owners"));
    263     assert_eq!(decoded.description.as_deref(), Some("team"));
    264     assert_eq!(decoded.image.as_deref(), Some(expected_image.as_str()));
    265 }
    266 
    267 #[test]
    268 fn list_set_decode_keeps_first_d_tag() {
    269     let tags = vec![
    270         vec!["d".to_string(), "members.owners".to_string()],
    271         vec!["d".to_string(), "members.ignore".to_string()],
    272         vec!["p".to_string(), "owner".to_string()],
    273     ];
    274     let decoded = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags).unwrap();
    275     assert_eq!(decoded.d_tag, "members.owners");
    276 }
    277 
    278 #[test]
    279 fn list_set_build_tags_omits_blank_optional_metadata() {
    280     let mut list_set = sample_list_set();
    281     list_set.title = Some(" ".to_string());
    282     list_set.description = Some(" ".to_string());
    283     list_set.image = Some(" ".to_string());
    284     let tags = list_set_build_tags(&list_set).unwrap();
    285     assert!(!tags.iter().any(|tag| tag[0] == "title"));
    286     assert!(!tags.iter().any(|tag| tag[0] == "description"));
    287     assert!(!tags.iter().any(|tag| tag[0] == "image"));
    288 }