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