post.rs (24534B)
1 use radroots_events::{ 2 farm::RadrootsFarmRef, 3 kinds::{KIND_ARTICLE, KIND_COMMENT, KIND_FARM, KIND_POST}, 4 post::RadrootsPost, 5 social::{ 6 RadrootsSocialFarmAnchor, RadrootsSocialLocation, RadrootsSocialMediaDimensions, 7 RadrootsSocialMediaMetadata, RadrootsSocialMediaThumbnail, RadrootsSocialTarget, 8 }, 9 tags::{TAG_A, TAG_G, TAG_IMETA, TAG_LOCATION, TAG_Q, TAG_T}, 10 }; 11 use radroots_events_codec::error::{EventEncodeError, EventParseError}; 12 use radroots_events_codec::post::decode::{ 13 data_from_event, parsed_from_event, post_from_content, post_from_event, 14 }; 15 use radroots_events_codec::post::encode::{ 16 post_build_tags, to_wire_parts, to_wire_parts_with_kind, 17 }; 18 19 const QUOTE_ID: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; 20 const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; 21 const ARTICLE_D_TAG: &str = "BBBBBBBBBBBBBBBBBBBBBA"; 22 23 fn content_post() -> RadrootsPost { 24 RadrootsPost { 25 content: "field update".to_string(), 26 farm: None, 27 address_refs: None, 28 location: None, 29 topics: None, 30 quote_refs: None, 31 media: None, 32 } 33 } 34 35 #[test] 36 fn post_to_wire_parts_requires_content() { 37 let post = RadrootsPost { 38 content: " ".to_string(), 39 farm: None, 40 address_refs: None, 41 location: None, 42 topics: None, 43 quote_refs: None, 44 media: None, 45 }; 46 47 let err = to_wire_parts(&post).unwrap_err(); 48 assert!(matches!( 49 err, 50 EventEncodeError::EmptyRequiredField("content") 51 )); 52 } 53 54 #[test] 55 fn post_to_wire_parts_sets_kind_and_content() { 56 let post = RadrootsPost { 57 content: "hello".to_string(), 58 farm: None, 59 address_refs: None, 60 location: None, 61 topics: None, 62 quote_refs: None, 63 media: None, 64 }; 65 66 let parts = to_wire_parts(&post).unwrap(); 67 assert_eq!(parts.kind, KIND_POST); 68 assert_eq!(parts.content, "hello"); 69 assert!(parts.tags.is_empty()); 70 } 71 72 #[test] 73 fn post_to_wire_parts_with_kind_rejects_non_post_kind() { 74 let post = RadrootsPost { 75 content: "hello".to_string(), 76 farm: None, 77 address_refs: None, 78 location: None, 79 topics: None, 80 quote_refs: None, 81 media: None, 82 }; 83 84 assert!(matches!( 85 to_wire_parts_with_kind(&post, KIND_ARTICLE), 86 Err(EventEncodeError::InvalidKind(KIND_ARTICLE)) 87 )); 88 } 89 90 #[test] 91 fn post_to_wire_parts_roundtrips_optional_social_tags() { 92 let post = RadrootsPost { 93 content: "field update".to_string(), 94 farm: Some(RadrootsSocialFarmAnchor { 95 farm: RadrootsFarmRef { 96 pubkey: "farm_pubkey".to_string(), 97 d_tag: FARM_D_TAG.to_string(), 98 }, 99 relays: Some(vec!["wss://farm-relay.example.test".to_string()]), 100 }), 101 address_refs: Some(vec![RadrootsSocialTarget::Address { 102 address: format!("30023:article_author:{ARTICLE_D_TAG}"), 103 author: Some("article_author".to_string()), 104 event_kind: Some(30023), 105 relays: Some(vec!["wss://article-relay.example.test".to_string()]), 106 }]), 107 location: Some(RadrootsSocialLocation { 108 name: Some("North field".to_string()), 109 geohash: Some("c23nb62w20st".to_string()), 110 }), 111 topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]), 112 quote_refs: Some(vec![ 113 RadrootsSocialTarget::Event { 114 id: QUOTE_ID.to_string(), 115 author: None, 116 event_kind: None, 117 relays: Some(vec!["wss://quote-relay.example.test".to_string()]), 118 }, 119 RadrootsSocialTarget::Address { 120 address: format!("30023:quote_author:{ARTICLE_D_TAG}"), 121 author: Some("quote_author".to_string()), 122 event_kind: Some(30023), 123 relays: None, 124 }, 125 ]), 126 media: Some(vec![RadrootsSocialMediaMetadata { 127 imeta: Some(vec![vec![ 128 "url https://media.example.test/field.jpg".to_string(), 129 "m image/jpeg".to_string(), 130 format!("x {QUOTE_ID}"), 131 "dim 1200x800".to_string(), 132 "alt Field rows".to_string(), 133 "service https://media.example.test".to_string(), 134 ]]), 135 ..RadrootsSocialMediaMetadata::default() 136 }]), 137 }; 138 139 let parts = to_wire_parts(&post).unwrap(); 140 assert_eq!(parts.kind, KIND_POST); 141 assert!(parts.tags.iter().any(|tag| { 142 tag.first().map(|value| value.as_str()) == Some(TAG_A) 143 && tag.get(1).map(|value| value.as_str()) 144 == Some("30340:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAA") 145 })); 146 assert!(parts.tags.iter().any(|tag| { 147 tag.first().map(|value| value.as_str()) == Some(TAG_A) 148 && tag.get(1).map(|value| value.as_str()) 149 == Some("30023:article_author:BBBBBBBBBBBBBBBBBBBBBA") 150 })); 151 assert!(parts.tags.iter().any(|tag| { 152 tag.first().map(|value| value.as_str()) == Some(TAG_LOCATION) 153 && tag.get(1).map(|value| value.as_str()) == Some("North field") 154 })); 155 assert!(parts.tags.iter().any(|tag| { 156 tag.first().map(|value| value.as_str()) == Some(TAG_G) 157 && tag.get(1).map(|value| value.as_str()) == Some("c23nb62w20st") 158 })); 159 assert!(parts.tags.iter().any(|tag| { 160 tag.first().map(|value| value.as_str()) == Some(TAG_T) 161 && tag.get(1).map(|value| value.as_str()) == Some("soil") 162 })); 163 assert!(parts.tags.iter().any(|tag| { 164 tag.first().map(|value| value.as_str()) == Some(TAG_Q) 165 && tag.get(1).map(|value| value.as_str()) == Some(QUOTE_ID) 166 })); 167 assert!(parts.tags.iter().any(|tag| { 168 tag.first().map(|value| value.as_str()) == Some(TAG_IMETA) 169 && tag 170 .iter() 171 .any(|value| value == "url https://media.example.test/field.jpg") 172 })); 173 174 let decoded = post_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 175 assert_eq!(decoded.content, "field update"); 176 assert_eq!( 177 decoded.farm.as_ref().map(|farm| farm.farm.pubkey.as_str()), 178 Some("farm_pubkey") 179 ); 180 assert_eq!(decoded.address_refs.as_ref().map(Vec::len), Some(1)); 181 assert_eq!( 182 decoded 183 .location 184 .as_ref() 185 .and_then(|location| location.name.as_deref()), 186 Some("North field") 187 ); 188 assert_eq!(decoded.topics.as_ref().map(Vec::len), Some(2)); 189 assert_eq!(decoded.quote_refs.as_ref().map(Vec::len), Some(2)); 190 let media = decoded.media.as_ref().expect("media"); 191 assert_eq!( 192 media[0].url.as_deref(), 193 Some("https://media.example.test/field.jpg") 194 ); 195 assert_eq!(media[0].mime_type.as_deref(), Some("image/jpeg")); 196 assert_eq!( 197 media[0].dimensions.as_ref().map(|value| value.width), 198 Some(1200) 199 ); 200 assert_eq!(media[0].alt.as_deref(), Some("Field rows")); 201 assert_eq!(media[0].services.as_ref().map(Vec::len), Some(1)); 202 } 203 204 #[test] 205 fn post_build_tags_covers_optional_social_encode_branches() { 206 let mut post = content_post(); 207 post.farm = Some(RadrootsSocialFarmAnchor { 208 farm: RadrootsFarmRef { 209 pubkey: "farm_pubkey".to_string(), 210 d_tag: FARM_D_TAG.to_string(), 211 }, 212 relays: Some(vec!["wss://farm-relay.example.test".to_string()]), 213 }); 214 post.address_refs = Some(vec![RadrootsSocialTarget::Address { 215 address: format!("30023:article_author:{ARTICLE_D_TAG}"), 216 author: None, 217 event_kind: None, 218 relays: Some(vec!["wss://article-relay.example.test".to_string()]), 219 }]); 220 post.quote_refs = Some(vec![ 221 RadrootsSocialTarget::Event { 222 id: QUOTE_ID.to_string(), 223 author: None, 224 event_kind: None, 225 relays: Some(vec!["wss://quote-relay.example.test".to_string()]), 226 }, 227 RadrootsSocialTarget::Address { 228 address: format!("30023:quote_author:{ARTICLE_D_TAG}"), 229 author: None, 230 event_kind: None, 231 relays: Some(vec!["wss://quote-address-relay.example.test".to_string()]), 232 }, 233 ]); 234 post.media = Some(vec![RadrootsSocialMediaMetadata { 235 thumbnails: Some(vec![RadrootsSocialMediaThumbnail { 236 url: "https://media.example.test/thumb.jpg".to_string(), 237 dimensions: Some(RadrootsSocialMediaDimensions { 238 width: 120, 239 height: 80, 240 }), 241 }]), 242 ..RadrootsSocialMediaMetadata::default() 243 }]); 244 245 let tags = post_build_tags(&post).unwrap(); 246 assert!(tags.iter().any(|tag| { 247 tag.first().map(|value| value.as_str()) == Some(TAG_A) 248 && tag 249 .iter() 250 .any(|value| value == "wss://farm-relay.example.test") 251 })); 252 assert!(tags.iter().any(|tag| { 253 tag.first().map(|value| value.as_str()) == Some(TAG_A) 254 && tag 255 .iter() 256 .any(|value| value == "wss://article-relay.example.test") 257 })); 258 assert!(tags.iter().any(|tag| { 259 tag.first().map(|value| value.as_str()) == Some(TAG_Q) 260 && tag 261 .iter() 262 .any(|value| value == "wss://quote-relay.example.test") 263 })); 264 assert!(tags.iter().any(|tag| { 265 tag.first().map(|value| value.as_str()) == Some(TAG_Q) 266 && tag 267 .iter() 268 .any(|value| value == "wss://quote-address-relay.example.test") 269 })); 270 assert!(tags.iter().any(|tag| { 271 tag.first().map(|value| value.as_str()) == Some(TAG_IMETA) 272 && tag.iter().any(|value| value == "dim 120x80") 273 })); 274 275 let mut no_relay_post = content_post(); 276 no_relay_post.farm = Some(RadrootsSocialFarmAnchor { 277 farm: RadrootsFarmRef { 278 pubkey: "farm_pubkey".to_string(), 279 d_tag: FARM_D_TAG.to_string(), 280 }, 281 relays: None, 282 }); 283 no_relay_post.address_refs = Some(vec![RadrootsSocialTarget::Address { 284 address: format!("30023:article_author:{ARTICLE_D_TAG}"), 285 author: None, 286 event_kind: None, 287 relays: None, 288 }]); 289 no_relay_post.quote_refs = Some(vec![RadrootsSocialTarget::Event { 290 id: QUOTE_ID.to_string(), 291 author: None, 292 event_kind: None, 293 relays: None, 294 }]); 295 no_relay_post.media = Some(vec![RadrootsSocialMediaMetadata { 296 thumbnails: Some(vec![RadrootsSocialMediaThumbnail { 297 url: "https://media.example.test/thumb-no-dim.jpg".to_string(), 298 dimensions: None, 299 }]), 300 ..RadrootsSocialMediaMetadata::default() 301 }]); 302 303 let tags = post_build_tags(&no_relay_post).unwrap(); 304 let farm_tag = tags 305 .iter() 306 .find(|tag| { 307 tag.first().map(String::as_str) == Some(TAG_A) 308 && tag.get(1).map(String::as_str) 309 == Some("30340:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAA") 310 }) 311 .expect("farm tag"); 312 assert_eq!(farm_tag.len(), 2); 313 let address_tag = tags 314 .iter() 315 .find(|tag| { 316 tag.first().map(String::as_str) == Some(TAG_A) 317 && tag.get(1).map(String::as_str) 318 == Some("30023:article_author:BBBBBBBBBBBBBBBBBBBBBA") 319 }) 320 .expect("address tag"); 321 assert_eq!(address_tag.len(), 2); 322 let quote_tag = tags 323 .iter() 324 .find(|tag| tag.first().map(String::as_str) == Some(TAG_Q)) 325 .expect("quote tag"); 326 assert_eq!(quote_tag.len(), 2); 327 let imeta = tags 328 .iter() 329 .find(|tag| tag.first().map(String::as_str) == Some(TAG_IMETA)) 330 .expect("imeta tag"); 331 assert!( 332 imeta 333 .iter() 334 .any(|value| value == "thumb https://media.example.test/thumb-no-dim.jpg") 335 ); 336 assert!(!imeta.iter().any(|value| value.starts_with("dim "))); 337 } 338 339 #[test] 340 fn post_social_tags_reject_malformed_supported_structures() { 341 let mut post = content_post(); 342 post.address_refs = Some(vec![RadrootsSocialTarget::Event { 343 id: QUOTE_ID.to_string(), 344 author: None, 345 event_kind: None, 346 relays: None, 347 }]); 348 assert!(matches!( 349 post_build_tags(&post), 350 Err(EventEncodeError::InvalidField("address_refs")) 351 )); 352 353 post.address_refs = Some(vec![RadrootsSocialTarget::Address { 354 address: "not-an-address".to_string(), 355 author: None, 356 event_kind: None, 357 relays: None, 358 }]); 359 assert!(matches!( 360 post_build_tags(&post), 361 Err(EventEncodeError::InvalidField("address_refs")) 362 )); 363 364 post.address_refs = Some(vec![RadrootsSocialTarget::Address { 365 address: format!("30340:farm_pubkey:{FARM_D_TAG}"), 366 author: Some("farm_pubkey".to_string()), 367 event_kind: Some(30340), 368 relays: None, 369 }]); 370 assert!(matches!( 371 post_build_tags(&post), 372 Err(EventEncodeError::InvalidField("address_refs")) 373 )); 374 375 post.address_refs = Some(vec![RadrootsSocialTarget::Address { 376 address: format!("30023:article_author:{ARTICLE_D_TAG}"), 377 author: Some("other_author".to_string()), 378 event_kind: Some(30023), 379 relays: None, 380 }]); 381 assert!(matches!( 382 post_build_tags(&post), 383 Err(EventEncodeError::InvalidField("address_refs")) 384 )); 385 386 post.address_refs = Some(vec![RadrootsSocialTarget::Address { 387 address: format!("30023:article_author:{ARTICLE_D_TAG}"), 388 author: Some("article_author".to_string()), 389 event_kind: Some(30024), 390 relays: None, 391 }]); 392 assert!(matches!( 393 post_build_tags(&post), 394 Err(EventEncodeError::InvalidField("address_refs")) 395 )); 396 397 post.address_refs = None; 398 post.farm = Some(RadrootsSocialFarmAnchor { 399 farm: RadrootsFarmRef { 400 pubkey: String::new(), 401 d_tag: FARM_D_TAG.to_string(), 402 }, 403 relays: None, 404 }); 405 assert!(matches!( 406 post_build_tags(&post), 407 Err(EventEncodeError::EmptyRequiredField("farm.pubkey")) 408 )); 409 410 post.farm = Some(RadrootsSocialFarmAnchor { 411 farm: RadrootsFarmRef { 412 pubkey: "farm_pubkey".to_string(), 413 d_tag: String::new(), 414 }, 415 relays: None, 416 }); 417 assert!(matches!( 418 post_build_tags(&post), 419 Err(EventEncodeError::EmptyRequiredField("farm.d_tag")) 420 )); 421 422 post.farm = Some(RadrootsSocialFarmAnchor { 423 farm: RadrootsFarmRef { 424 pubkey: "farm_pubkey".to_string(), 425 d_tag: "bad d".to_string(), 426 }, 427 relays: None, 428 }); 429 assert!(matches!( 430 post_build_tags(&post), 431 Err(EventEncodeError::InvalidField("farm")) 432 )); 433 434 post.farm = None; 435 post.quote_refs = Some(vec![RadrootsSocialTarget::Event { 436 id: "not-hex".to_string(), 437 author: None, 438 event_kind: None, 439 relays: None, 440 }]); 441 assert!(matches!( 442 post_build_tags(&post), 443 Err(EventEncodeError::InvalidField("quote_refs")) 444 )); 445 446 post.quote_refs = Some(vec![RadrootsSocialTarget::Address { 447 address: "not-an-address".to_string(), 448 author: None, 449 event_kind: None, 450 relays: None, 451 }]); 452 assert!(matches!( 453 post_build_tags(&post), 454 Err(EventEncodeError::InvalidField("quote_refs")) 455 )); 456 457 post.quote_refs = Some(vec![RadrootsSocialTarget::Address { 458 address: format!("30023:quote_author:{ARTICLE_D_TAG}"), 459 author: None, 460 event_kind: Some(30024), 461 relays: None, 462 }]); 463 assert!(matches!( 464 post_build_tags(&post), 465 Err(EventEncodeError::InvalidField("quote_refs")) 466 )); 467 468 post.quote_refs = Some(vec![RadrootsSocialTarget::External { 469 id: "https://example.test/object".to_string(), 470 external_kind: "web".to_string(), 471 hint: None, 472 }]); 473 assert!(matches!( 474 post_build_tags(&post), 475 Err(EventEncodeError::InvalidField("quote_refs")) 476 )); 477 478 post.quote_refs = None; 479 post.media = Some(vec![RadrootsSocialMediaMetadata { 480 imeta: Some(vec![Vec::new()]), 481 ..RadrootsSocialMediaMetadata::default() 482 }]); 483 assert!(matches!( 484 post_build_tags(&post), 485 Err(EventEncodeError::InvalidField("imeta")) 486 )); 487 488 post.media = Some(vec![RadrootsSocialMediaMetadata { 489 imeta: Some(vec![vec![" ".to_string()]]), 490 ..RadrootsSocialMediaMetadata::default() 491 }]); 492 assert!(matches!( 493 post_build_tags(&post), 494 Err(EventEncodeError::InvalidField("imeta")) 495 )); 496 497 post.media = Some(vec![RadrootsSocialMediaMetadata { 498 thumbnails: Some(vec![RadrootsSocialMediaThumbnail { 499 url: " ".to_string(), 500 dimensions: None, 501 }]), 502 ..RadrootsSocialMediaMetadata::default() 503 }]); 504 assert!(matches!( 505 post_build_tags(&post), 506 Err(EventEncodeError::InvalidField("imeta")) 507 )); 508 509 let err = post_from_event( 510 KIND_POST, 511 &[vec![TAG_IMETA.to_string(), "bad-imeta-entry".to_string()]], 512 "hello", 513 ) 514 .unwrap_err(); 515 assert!(matches!(err, EventParseError::InvalidTag(TAG_IMETA))); 516 } 517 518 #[test] 519 fn post_media_structured_fields_encode_and_decode_imeta() { 520 let mut post = content_post(); 521 post.topics = Some(vec![ 522 "soil".to_string(), 523 " ".to_string(), 524 "market".to_string(), 525 ]); 526 post.media = Some(vec![ 527 RadrootsSocialMediaMetadata::default(), 528 RadrootsSocialMediaMetadata { 529 url: Some("https://media.example.test/field.jpg".to_string()), 530 mime_type: Some("image/jpeg".to_string()), 531 sha256: Some(QUOTE_ID.to_string()), 532 original_sha256: Some(QUOTE_ID.to_string()), 533 size: Some(42), 534 dimensions: Some(RadrootsSocialMediaDimensions { 535 width: 1200, 536 height: 800, 537 }), 538 blurhash: Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj".to_string()), 539 thumbnails: Some(vec![RadrootsSocialMediaThumbnail { 540 url: "https://media.example.test/thumb.jpg".to_string(), 541 dimensions: Some(RadrootsSocialMediaDimensions { 542 width: 120, 543 height: 80, 544 }), 545 }]), 546 image: Some("https://media.example.test/poster.jpg".to_string()), 547 summary: Some("Field row image".to_string()), 548 alt: Some("rows in field".to_string()), 549 fallback: Some("https://media.example.test/fallback.jpg".to_string()), 550 magnet: Some("magnet:?xt=urn:btih:fixture".to_string()), 551 content_hashes: Some(vec!["hash-a".to_string(), "hash-b".to_string()]), 552 services: Some(vec!["https://media.example.test".to_string()]), 553 imeta: None, 554 }, 555 ]); 556 557 let parts = to_wire_parts(&post).unwrap(); 558 let topic_tags = parts 559 .tags 560 .iter() 561 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_T)) 562 .count(); 563 assert_eq!(topic_tags, 2); 564 565 let imeta = parts 566 .tags 567 .iter() 568 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_IMETA)) 569 .expect("imeta tag"); 570 for expected in [ 571 "url https://media.example.test/field.jpg", 572 "m image/jpeg", 573 "size 42", 574 "dim 1200x800", 575 "blurhash LEHV6nWB2yk8pyo0adR*.7kCMdnj", 576 "thumb https://media.example.test/thumb.jpg", 577 "dim 120x80", 578 "image https://media.example.test/poster.jpg", 579 "summary Field row image", 580 "alt rows in field", 581 "fallback https://media.example.test/fallback.jpg", 582 "magnet magnet:?xt=urn:btih:fixture", 583 "i hash-a", 584 "i hash-b", 585 "service https://media.example.test", 586 ] { 587 assert!(imeta.iter().any(|value| value == expected), "{expected}"); 588 } 589 590 let decoded = post_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 591 let media = decoded.media.expect("media"); 592 assert_eq!(media.len(), 1); 593 assert_eq!(media[0].original_sha256.as_deref(), Some(QUOTE_ID)); 594 assert_eq!(media[0].size, Some(42)); 595 assert_eq!( 596 media[0].blurhash.as_deref(), 597 Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj") 598 ); 599 assert_eq!( 600 media[0].image.as_deref(), 601 Some("https://media.example.test/poster.jpg") 602 ); 603 assert_eq!(media[0].summary.as_deref(), Some("Field row image")); 604 assert_eq!( 605 media[0].fallback.as_deref(), 606 Some("https://media.example.test/fallback.jpg") 607 ); 608 assert_eq!( 609 media[0].magnet.as_deref(), 610 Some("magnet:?xt=urn:btih:fixture") 611 ); 612 assert_eq!(media[0].content_hashes.as_ref().map(Vec::len), Some(2)); 613 } 614 615 #[test] 616 fn post_decode_rejects_more_invalid_imeta_shapes() { 617 for tags in [ 618 vec![TAG_IMETA.to_string()], 619 vec![TAG_IMETA.to_string(), " ".to_string()], 620 ] { 621 let err = post_from_event(KIND_POST, &[tags], "hello").unwrap_err(); 622 assert!(matches!(err, EventParseError::InvalidTag(TAG_IMETA))); 623 } 624 625 for entry in ["url ", "size not-a-number", "dim bad", "dim 0x10"] { 626 let err = post_from_event( 627 KIND_POST, 628 &[vec![TAG_IMETA.to_string(), entry.to_string()]], 629 "hello", 630 ) 631 .unwrap_err(); 632 assert!(matches!( 633 err, 634 EventParseError::InvalidTag(TAG_IMETA) | EventParseError::InvalidNumber(TAG_IMETA, _) 635 )); 636 } 637 } 638 639 #[test] 640 fn post_decode_handles_non_farm_address_refs_without_relays() { 641 let article = format!("30023:article_author:{ARTICLE_D_TAG}"); 642 let farm = format!("{KIND_FARM}:farm_pubkey:{FARM_D_TAG}"); 643 let decoded = post_from_event( 644 KIND_POST, 645 &[ 646 vec![TAG_A.to_string(), farm.clone()], 647 vec![TAG_A.to_string(), article.clone()], 648 ], 649 "address only", 650 ) 651 .unwrap(); 652 653 let anchor = decoded.farm.expect("farm anchor"); 654 assert_eq!(anchor.farm.d_tag, FARM_D_TAG); 655 assert_eq!(anchor.relays, None); 656 let refs = decoded.address_refs.expect("address refs"); 657 assert_eq!(refs.len(), 1); 658 match &refs[0] { 659 RadrootsSocialTarget::Address { 660 address, 661 author, 662 event_kind, 663 relays, 664 } => { 665 assert_eq!(address, &article); 666 assert_eq!(author.as_deref(), Some("article_author")); 667 assert_eq!(*event_kind, Some(30023)); 668 assert_eq!(relays, &None); 669 } 670 _ => panic!("expected address target"), 671 } 672 } 673 674 #[test] 675 fn post_from_content_requires_kind_and_content() { 676 let err = post_from_content(KIND_COMMENT, "hello").unwrap_err(); 677 assert!(matches!( 678 err, 679 EventParseError::InvalidKind { 680 expected: "1", 681 got: KIND_COMMENT 682 } 683 )); 684 685 let err = post_from_content(KIND_POST, " ").unwrap_err(); 686 assert!(matches!(err, EventParseError::InvalidTag("content"))); 687 } 688 689 #[test] 690 fn post_metadata_and_index_from_event_roundtrip() { 691 let metadata = data_from_event( 692 "id".to_string(), 693 "author".to_string(), 694 77, 695 KIND_POST, 696 "hello".to_string(), 697 Vec::new(), 698 ) 699 .unwrap(); 700 assert_eq!(metadata.id, "id"); 701 assert_eq!(metadata.author, "author"); 702 assert_eq!(metadata.published_at, 77); 703 assert_eq!(metadata.kind, KIND_POST); 704 assert_eq!(metadata.data.content, "hello"); 705 706 let index = parsed_from_event( 707 "id".to_string(), 708 "author".to_string(), 709 77, 710 KIND_POST, 711 "hello".to_string(), 712 Vec::new(), 713 "sig".to_string(), 714 ) 715 .unwrap(); 716 assert_eq!(index.event.id, "id"); 717 assert_eq!(index.event.author, "author"); 718 assert_eq!(index.event.created_at, 77); 719 assert_eq!(index.event.kind, KIND_POST); 720 assert_eq!(index.event.content, "hello"); 721 assert_eq!(index.event.sig, "sig"); 722 assert_eq!(index.data.data.content, "hello"); 723 } 724 725 #[test] 726 fn post_index_from_event_propagates_parse_errors() { 727 let err = parsed_from_event( 728 "id".to_string(), 729 "author".to_string(), 730 77, 731 KIND_COMMENT, 732 "hello".to_string(), 733 Vec::new(), 734 "sig".to_string(), 735 ) 736 .unwrap_err(); 737 assert!(matches!( 738 err, 739 EventParseError::InvalidKind { 740 expected: "1", 741 got: KIND_COMMENT 742 } 743 )); 744 }