mod.rs (7231B)
1 pub mod decode; 2 pub mod encode; 3 4 use crate::d_tag::is_d_tag_base64url; 5 use radroots_event::envelope::kind::{KIND_CALENDAR, is_nip51_list_set_kind}; 6 7 pub(crate) const fn is_generic_list_set_codec_kind(kind: u32) -> bool { 8 is_nip51_list_set_kind(kind) && kind != KIND_CALENDAR 9 } 10 11 fn list_set_requires_base64(d_tag: &str) -> bool { 12 d_tag.starts_with("farm:") || d_tag.starts_with("coop:") || d_tag.starts_with("resource:") 13 } 14 15 fn list_set_base64_id_is_valid(d_tag: &str) -> bool { 16 if !list_set_requires_base64(d_tag) { 17 return true; 18 } 19 let mut parts = d_tag.splitn(3, ':'); 20 let _ = parts.next(); 21 let id = parts.next().unwrap_or(""); 22 let suffix = parts.next().unwrap_or(""); 23 !id.trim().is_empty() && !suffix.trim().is_empty() && is_d_tag_base64url(id) 24 } 25 26 #[cfg(test)] 27 mod tests { 28 use super::{ 29 decode::list_set_from_tags, encode::list_set_build_tags, is_generic_list_set_codec_kind, 30 }; 31 use crate::error::{EventEncodeError, EventParseError}; 32 use radroots_event::{ 33 envelope::kind::{KIND_CALENDAR, KIND_LIST_SET_FOLLOW, KIND_POST, is_nip51_list_set_kind}, 34 social::list::ListEntry, 35 social::list_set::ListSet, 36 }; 37 38 #[test] 39 fn generic_list_set_codec_kind_excludes_calendar() { 40 assert!(is_nip51_list_set_kind(KIND_CALENDAR)); 41 assert!(!is_generic_list_set_codec_kind(KIND_CALENDAR)); 42 assert!(is_generic_list_set_codec_kind(KIND_LIST_SET_FOLLOW)); 43 assert!(!is_generic_list_set_codec_kind(KIND_POST)); 44 } 45 46 #[test] 47 fn list_set_tags_round_trip() { 48 let list = ListSet { 49 d_tag: "members.owners".to_string(), 50 content: "".to_string(), 51 entries: vec![ 52 ListEntry { 53 tag: "p".to_string(), 54 values: vec!["owner_pubkey".to_string()], 55 }, 56 ListEntry { 57 tag: "p".to_string(), 58 values: vec!["worker_pubkey".to_string(), "wss://relay".to_string()], 59 }, 60 ], 61 title: Some("Owners".to_string()), 62 description: None, 63 image: None, 64 }; 65 let tags = list_set_build_tags(&list).expect("build tags"); 66 let parsed = list_set_from_tags(KIND_LIST_SET_FOLLOW, list.content.clone(), &tags) 67 .expect("parse list set"); 68 assert_eq!(parsed.d_tag, list.d_tag); 69 assert_eq!(parsed.title, list.title); 70 assert_eq!(parsed.entries.len(), list.entries.len()); 71 assert_eq!(parsed.entries[0].values[0], "owner_pubkey"); 72 } 73 74 #[test] 75 fn list_set_rejects_invalid_farm_d_tag_on_encode() { 76 let list = ListSet { 77 d_tag: "farm:invalid:members".to_string(), 78 content: "".to_string(), 79 entries: vec![ListEntry { 80 tag: "p".to_string(), 81 values: vec!["pubkey".to_string()], 82 }], 83 title: None, 84 description: None, 85 image: None, 86 }; 87 let err = list_set_build_tags(&list).expect_err("expected invalid d_tag"); 88 assert!(matches!(err, EventEncodeError::InvalidField("d_tag"))); 89 } 90 91 #[test] 92 fn list_set_rejects_invalid_farm_d_tag_on_decode() { 93 let tags = vec![ 94 vec!["d".to_string(), "farm:invalid:members".to_string()], 95 vec!["p".to_string(), "pubkey".to_string()], 96 ]; 97 let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "".to_string(), &tags) 98 .expect_err("expected invalid d_tag"); 99 assert!(matches!(err, EventParseError::InvalidTag("d"))); 100 } 101 102 #[test] 103 fn list_set_accepts_resource_base64_d_tag() { 104 let list = ListSet { 105 d_tag: "resource:AAAAAAAAAAAAAAAAAAAAAA:members".to_string(), 106 content: "".to_string(), 107 entries: vec![ListEntry { 108 tag: "p".to_string(), 109 values: vec!["pubkey".to_string()], 110 }], 111 title: None, 112 description: None, 113 image: None, 114 }; 115 let tags = list_set_build_tags(&list).expect("build tags"); 116 let parsed = list_set_from_tags(KIND_LIST_SET_FOLLOW, list.content.clone(), &tags) 117 .expect("parse list set"); 118 assert_eq!(parsed.d_tag, list.d_tag); 119 } 120 121 #[test] 122 fn list_set_rejects_empty_prefixed_id_or_suffix() { 123 let list = ListSet { 124 d_tag: "farm::members".to_string(), 125 content: "".to_string(), 126 entries: vec![ListEntry { 127 tag: "p".to_string(), 128 values: vec!["pubkey".to_string()], 129 }], 130 title: None, 131 description: None, 132 image: None, 133 }; 134 let err = list_set_build_tags(&list).expect_err("expected invalid d_tag"); 135 assert!(matches!(err, EventEncodeError::InvalidField("d_tag"))); 136 137 let tags = vec![ 138 vec!["d".to_string(), "coop:AAAAAAAAAAAAAAAAAAAAAA:".to_string()], 139 vec!["p".to_string(), "pubkey".to_string()], 140 ]; 141 let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "".to_string(), &tags) 142 .expect_err("expected invalid d_tag"); 143 assert!(matches!(err, EventParseError::InvalidTag("d"))); 144 } 145 146 #[test] 147 fn list_set_decode_ignores_short_tags() { 148 let tags = vec![ 149 vec!["d".to_string(), "members.owners".to_string()], 150 vec!["p".to_string(), "owner".to_string()], 151 vec!["subject".to_string()], 152 ]; 153 let parsed = 154 list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags).expect("parsed"); 155 assert_eq!(parsed.d_tag, "members.owners"); 156 assert_eq!(parsed.entries.len(), 1); 157 assert_eq!(parsed.entries[0].tag, "p"); 158 } 159 160 #[test] 161 fn list_set_decode_rejects_empty_entry_value() { 162 let tags = vec![ 163 vec!["d".to_string(), "members.owners".to_string()], 164 vec!["p".to_string(), " ".to_string()], 165 ]; 166 let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags) 167 .expect_err("expected invalid entry tag"); 168 assert!(matches!(err, EventParseError::InvalidTag("tag"))); 169 } 170 171 #[test] 172 fn list_set_decode_rejects_invalid_kind() { 173 let tags = vec![ 174 vec!["d".to_string(), "members.owners".to_string()], 175 vec!["p".to_string(), "owner".to_string()], 176 ]; 177 let err = list_set_from_tags(KIND_POST, "private".to_string(), &tags) 178 .expect_err("expected invalid kind"); 179 assert!(matches!( 180 err, 181 EventParseError::InvalidKind { 182 expected: "generic nip51 list-set kind", 183 got: KIND_POST 184 } 185 )); 186 } 187 188 #[test] 189 fn list_set_decode_rejects_empty_tag_name() { 190 let tags = vec![ 191 vec!["d".to_string(), "members.owners".to_string()], 192 vec!["".to_string(), "owner".to_string()], 193 ]; 194 let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags) 195 .expect_err("expected invalid empty tag name"); 196 assert!(matches!(err, EventParseError::InvalidTag("tag"))); 197 } 198 }