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