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 }