follow.rs (14560B)
1 #[path = "../src/test_fixtures.rs"] 2 mod test_fixtures; 3 4 use radroots_events::{ 5 follow::{RadrootsFollow, RadrootsFollowProfile}, 6 kinds::{KIND_FOLLOW, KIND_POST}, 7 }; 8 9 use radroots_events_codec::error::{EventEncodeError, EventParseError}; 10 use radroots_events_codec::follow::decode::{data_from_event, follow_from_tags, parsed_from_event}; 11 use radroots_events_codec::follow::encode::{ 12 FollowMutation, follow_apply, follow_to_wire_parts_after, to_wire_parts, 13 to_wire_parts_with_kind, 14 }; 15 use test_fixtures::{RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS}; 16 17 #[test] 18 fn follow_to_wire_parts_builds_p_tags() { 19 let follow = RadrootsFollow { 20 list: vec![RadrootsFollowProfile { 21 published_at: 42, 22 public_key: "pubkey".to_string(), 23 relay_url: Some("wss://relay".to_string()), 24 contact_name: Some("alice".to_string()), 25 }], 26 }; 27 28 let parts = to_wire_parts(&follow).unwrap(); 29 assert_eq!(parts.kind, KIND_FOLLOW); 30 assert_eq!(parts.content, ""); 31 assert_eq!(parts.tags.len(), 1); 32 33 let tag = &parts.tags[0]; 34 assert_eq!(tag[0], "p"); 35 assert_eq!(tag[1], "pubkey"); 36 assert_eq!(tag[2], "wss://relay"); 37 assert_eq!(tag[3], "alice"); 38 } 39 40 #[test] 41 fn follow_to_wire_parts_requires_public_key() { 42 let follow = RadrootsFollow { 43 list: vec![RadrootsFollowProfile { 44 published_at: 1, 45 public_key: " ".to_string(), 46 relay_url: None, 47 contact_name: None, 48 }], 49 }; 50 51 let err = to_wire_parts(&follow).unwrap_err(); 52 assert!(matches!( 53 err, 54 EventEncodeError::EmptyRequiredField("follow.public_key") 55 )); 56 } 57 58 #[test] 59 fn follow_from_tags_defaults_published_at() { 60 let tags = vec![vec!["p".to_string(), "pubkey".to_string()]]; 61 62 let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap(); 63 assert_eq!(follow.list.len(), 1); 64 assert_eq!(follow.list[0].published_at, 123); 65 assert_eq!(follow.list[0].public_key, "pubkey"); 66 assert!(follow.list[0].relay_url.is_none()); 67 assert!(follow.list[0].contact_name.is_none()); 68 } 69 70 #[test] 71 fn follow_from_tags_accepts_contact_without_relay() { 72 let tags = vec![vec![ 73 "p".to_string(), 74 "pubkey".to_string(), 75 "alice".to_string(), 76 ]]; 77 78 let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap(); 79 assert_eq!(follow.list[0].published_at, 123); 80 assert_eq!(follow.list[0].public_key, "pubkey"); 81 assert!(follow.list[0].relay_url.is_none()); 82 assert_eq!(follow.list[0].contact_name.as_deref(), Some("alice")); 83 } 84 85 #[test] 86 fn follow_from_tags_accepts_ws_relay_and_contact_name() { 87 let tags = vec![vec![ 88 "p".to_string(), 89 "pubkey".to_string(), 90 "ws://relay.example.com".to_string(), 91 "alice".to_string(), 92 ]]; 93 94 let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap(); 95 assert_eq!( 96 follow.list[0].relay_url.as_deref(), 97 Some("ws://relay.example.com") 98 ); 99 assert_eq!(follow.list[0].contact_name.as_deref(), Some("alice")); 100 } 101 102 #[test] 103 fn follow_from_tags_uses_tag_published_at() { 104 let tags = vec![vec![ 105 "p".to_string(), 106 "pubkey".to_string(), 107 "".to_string(), 108 "".to_string(), 109 "77".to_string(), 110 ]]; 111 112 let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap(); 113 assert_eq!(follow.list[0].published_at, 77); 114 } 115 116 #[test] 117 fn follow_from_tags_rejects_wrong_kind() { 118 let tags = vec![vec!["p".to_string(), "pubkey".to_string()]]; 119 let err = follow_from_tags(KIND_POST, &tags, 123).unwrap_err(); 120 assert!(matches!( 121 err, 122 EventParseError::InvalidKind { 123 expected: "3", 124 got: KIND_POST 125 } 126 )); 127 } 128 129 #[test] 130 fn follow_from_tags_rejects_invalid_published_at_number() { 131 let tags = vec![vec![ 132 "p".to_string(), 133 "pubkey".to_string(), 134 "".to_string(), 135 "".to_string(), 136 "not-a-number".to_string(), 137 ]]; 138 let err = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap_err(); 139 assert!(matches!(err, EventParseError::InvalidNumber("p", _))); 140 } 141 142 #[test] 143 fn follow_from_tags_rejects_missing_public_key_value() { 144 let tags = vec![vec!["p".to_string()]]; 145 let err = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap_err(); 146 assert!(matches!(err, EventParseError::InvalidTag("p"))); 147 } 148 149 #[test] 150 fn follow_metadata_and_index_from_event_roundtrip() { 151 let tags = vec![vec![ 152 "p".to_string(), 153 "pubkey".to_string(), 154 RELAY_PRIMARY_WSS.to_string(), 155 "alice".to_string(), 156 "88".to_string(), 157 ]]; 158 let metadata = data_from_event( 159 "id".to_string(), 160 "author".to_string(), 161 50, 162 KIND_FOLLOW, 163 "".to_string(), 164 tags.clone(), 165 ) 166 .unwrap(); 167 assert_eq!(metadata.id, "id"); 168 assert_eq!(metadata.author, "author"); 169 assert_eq!(metadata.published_at, 50); 170 assert_eq!(metadata.kind, KIND_FOLLOW); 171 assert_eq!(metadata.data.list.len(), 1); 172 assert_eq!(metadata.data.list[0].published_at, 88); 173 assert_eq!(metadata.data.list[0].public_key, "pubkey"); 174 assert_eq!( 175 metadata.data.list[0].relay_url.as_deref(), 176 Some(RELAY_PRIMARY_WSS) 177 ); 178 assert_eq!(metadata.data.list[0].contact_name.as_deref(), Some("alice")); 179 180 let index = parsed_from_event( 181 "id".to_string(), 182 "author".to_string(), 183 50, 184 KIND_FOLLOW, 185 "".to_string(), 186 tags, 187 "sig".to_string(), 188 ) 189 .unwrap(); 190 assert_eq!(index.event.kind, KIND_FOLLOW); 191 assert_eq!(index.event.sig, "sig"); 192 assert_eq!(index.data.data.list.len(), 1); 193 } 194 195 #[test] 196 fn follow_index_from_event_propagates_parse_errors() { 197 let err = parsed_from_event( 198 "id".to_string(), 199 "author".to_string(), 200 50, 201 KIND_POST, 202 "".to_string(), 203 Vec::new(), 204 "sig".to_string(), 205 ) 206 .unwrap_err(); 207 assert!(matches!( 208 err, 209 EventParseError::InvalidKind { 210 expected: "3", 211 got: KIND_POST 212 } 213 )); 214 } 215 216 #[test] 217 fn follow_apply_adds_and_updates_entries() { 218 let follow = RadrootsFollow { 219 list: vec![ 220 RadrootsFollowProfile { 221 published_at: 1, 222 public_key: "pubkey-a".to_string(), 223 relay_url: None, 224 contact_name: Some("alice".to_string()), 225 }, 226 RadrootsFollowProfile { 227 published_at: 1, 228 public_key: "pubkey-b".to_string(), 229 relay_url: None, 230 contact_name: Some("bob".to_string()), 231 }, 232 ], 233 }; 234 235 let updated = follow_apply( 236 &follow, 237 FollowMutation::Follow { 238 public_key: "pubkey-a".to_string(), 239 relay_url: Some("wss://relay".to_string()), 240 contact_name: Some("alice-updated".to_string()), 241 }, 242 ) 243 .unwrap(); 244 assert_eq!(updated.list.len(), 2); 245 assert_eq!(updated.list[0].public_key, "pubkey-a"); 246 assert_eq!(updated.list[0].relay_url.as_deref(), Some("wss://relay")); 247 assert_eq!( 248 updated.list[0].contact_name.as_deref(), 249 Some("alice-updated") 250 ); 251 252 let added = follow_apply( 253 &follow, 254 FollowMutation::Follow { 255 public_key: "pubkey-c".to_string(), 256 relay_url: None, 257 contact_name: Some("cara".to_string()), 258 }, 259 ) 260 .unwrap(); 261 assert_eq!(added.list.len(), 3); 262 assert_eq!(added.list[2].public_key, "pubkey-c"); 263 } 264 265 #[test] 266 fn follow_apply_unfollow_removes_entries() { 267 let follow = RadrootsFollow { 268 list: vec![ 269 RadrootsFollowProfile { 270 published_at: 1, 271 public_key: "pubkey-a".to_string(), 272 relay_url: None, 273 contact_name: None, 274 }, 275 RadrootsFollowProfile { 276 published_at: 1, 277 public_key: "pubkey-b".to_string(), 278 relay_url: None, 279 contact_name: None, 280 }, 281 ], 282 }; 283 284 let removed = follow_apply( 285 &follow, 286 FollowMutation::Unfollow { 287 public_key: "pubkey-b".to_string(), 288 }, 289 ) 290 .unwrap(); 291 assert_eq!(removed.list.len(), 1); 292 assert_eq!(removed.list[0].public_key, "pubkey-a"); 293 } 294 295 #[test] 296 fn follow_apply_toggle_adds_or_removes() { 297 let follow = RadrootsFollow { 298 list: vec![RadrootsFollowProfile { 299 published_at: 1, 300 public_key: "pubkey-a".to_string(), 301 relay_url: None, 302 contact_name: None, 303 }], 304 }; 305 306 let removed = follow_apply( 307 &follow, 308 FollowMutation::Toggle { 309 public_key: "pubkey-a".to_string(), 310 relay_url: None, 311 contact_name: None, 312 }, 313 ) 314 .unwrap(); 315 assert!(removed.list.is_empty()); 316 317 let added = follow_apply( 318 &follow, 319 FollowMutation::Toggle { 320 public_key: "pubkey-b".to_string(), 321 relay_url: None, 322 contact_name: Some("bob".to_string()), 323 }, 324 ) 325 .unwrap(); 326 assert_eq!(added.list.len(), 2); 327 assert_eq!(added.list[1].public_key, "pubkey-b"); 328 } 329 330 #[test] 331 fn follow_apply_rejects_empty_pubkey() { 332 let follow = RadrootsFollow { list: Vec::new() }; 333 let err = follow_apply( 334 &follow, 335 FollowMutation::Follow { 336 public_key: " ".to_string(), 337 relay_url: None, 338 contact_name: None, 339 }, 340 ) 341 .unwrap_err(); 342 assert!(matches!( 343 err, 344 EventEncodeError::EmptyRequiredField("follow.public_key") 345 )); 346 } 347 348 #[test] 349 fn follow_apply_rejects_empty_pubkey_for_unfollow_and_toggle() { 350 let follow = RadrootsFollow { list: Vec::new() }; 351 let err = follow_apply( 352 &follow, 353 FollowMutation::Unfollow { 354 public_key: " ".to_string(), 355 }, 356 ) 357 .unwrap_err(); 358 assert!(matches!( 359 err, 360 EventEncodeError::EmptyRequiredField("follow.public_key") 361 )); 362 363 let err = follow_apply( 364 &follow, 365 FollowMutation::Toggle { 366 public_key: " ".to_string(), 367 relay_url: None, 368 contact_name: None, 369 }, 370 ) 371 .unwrap_err(); 372 assert!(matches!( 373 err, 374 EventEncodeError::EmptyRequiredField("follow.public_key") 375 )); 376 } 377 378 #[test] 379 fn follow_apply_rejects_invalid_existing_entries_and_after_mutation_propagates_error() { 380 let follow = RadrootsFollow { 381 list: vec![RadrootsFollowProfile { 382 published_at: 1, 383 public_key: " ".to_string(), 384 relay_url: None, 385 contact_name: None, 386 }], 387 }; 388 389 let err = follow_apply( 390 &follow, 391 FollowMutation::Unfollow { 392 public_key: "pubkey-a".to_string(), 393 }, 394 ) 395 .unwrap_err(); 396 assert!(matches!( 397 err, 398 EventEncodeError::EmptyRequiredField("follow.public_key") 399 )); 400 401 let err = follow_to_wire_parts_after( 402 &RadrootsFollow { list: Vec::new() }, 403 FollowMutation::Follow { 404 public_key: " ".to_string(), 405 relay_url: None, 406 contact_name: None, 407 }, 408 ) 409 .unwrap_err(); 410 assert!(matches!( 411 err, 412 EventEncodeError::EmptyRequiredField("follow.public_key") 413 )); 414 } 415 416 #[test] 417 fn follow_build_tags_normalizes_empty_optional_values() { 418 let follow = RadrootsFollow { 419 list: vec![RadrootsFollowProfile { 420 published_at: 1, 421 public_key: "pubkey".to_string(), 422 relay_url: Some("".to_string()), 423 contact_name: Some(" ".to_string()), 424 }], 425 }; 426 let parts = to_wire_parts(&follow).unwrap(); 427 assert_eq!( 428 parts.tags, 429 vec![vec!["p".to_string(), "pubkey".to_string(), " ".to_string()]] 430 ); 431 } 432 433 #[test] 434 fn follow_to_wire_parts_with_kind_and_after_mutation_work() { 435 let follow = RadrootsFollow { 436 list: vec![RadrootsFollowProfile { 437 published_at: 1, 438 public_key: "pubkey-a".to_string(), 439 relay_url: None, 440 contact_name: None, 441 }], 442 }; 443 let parts = to_wire_parts_with_kind(&follow, KIND_POST).unwrap(); 444 assert_eq!(parts.kind, KIND_POST); 445 446 let toggled = follow_to_wire_parts_after( 447 &follow, 448 FollowMutation::Toggle { 449 public_key: "pubkey-b".to_string(), 450 relay_url: Some(RELAY_PRIMARY_WSS.to_string()), 451 contact_name: Some("alice".to_string()), 452 }, 453 ) 454 .unwrap(); 455 assert_eq!(toggled.kind, KIND_FOLLOW); 456 assert_eq!(toggled.tags.len(), 2); 457 } 458 459 #[test] 460 fn follow_apply_normalizes_optional_fields_and_deduplicates_existing_list() { 461 let follow = RadrootsFollow { 462 list: vec![ 463 RadrootsFollowProfile { 464 published_at: 1, 465 public_key: " pubkey-a ".to_string(), 466 relay_url: Some(" ".to_string()), 467 contact_name: Some(" ".to_string()), 468 }, 469 RadrootsFollowProfile { 470 published_at: 2, 471 public_key: "pubkey-a".to_string(), 472 relay_url: Some(RELAY_SECONDARY_WSS.to_string()), 473 contact_name: Some("duplicate".to_string()), 474 }, 475 ], 476 }; 477 478 let updated = follow_apply( 479 &follow, 480 FollowMutation::Follow { 481 public_key: "pubkey-a".to_string(), 482 relay_url: Some(" ".to_string()), 483 contact_name: Some(" ".to_string()), 484 }, 485 ) 486 .unwrap(); 487 488 assert_eq!(updated.list.len(), 1); 489 assert_eq!(updated.list[0].public_key, "pubkey-a"); 490 assert!(updated.list[0].relay_url.is_none()); 491 assert!(updated.list[0].contact_name.is_none()); 492 } 493 494 #[test] 495 fn follow_apply_follow_with_none_preserves_existing_values() { 496 let follow = RadrootsFollow { 497 list: vec![RadrootsFollowProfile { 498 published_at: 1, 499 public_key: "pubkey-a".to_string(), 500 relay_url: Some(RELAY_PRIMARY_WSS.to_string()), 501 contact_name: Some("alice".to_string()), 502 }], 503 }; 504 505 let updated = follow_apply( 506 &follow, 507 FollowMutation::Follow { 508 public_key: "pubkey-a".to_string(), 509 relay_url: None, 510 contact_name: None, 511 }, 512 ) 513 .unwrap(); 514 assert_eq!(updated.list.len(), 1); 515 assert_eq!( 516 updated.list[0].relay_url.as_deref(), 517 Some(RELAY_PRIMARY_WSS) 518 ); 519 assert_eq!(updated.list[0].contact_name.as_deref(), Some("alice")); 520 }