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 }