tests.rs (69012B)
1 use super::*; 2 use crate::envelope::EventEnvelopeParts; 3 use std::collections::BTreeSet; 4 5 static AMBIGUOUS_TEST_CONTRACTS: &[EventContract] = &[ 6 event_contract!( 7 "radroots.test.one.v1", 8 KIND_POST, 9 "Test One", 10 "Test", 11 EventClass::Regular, 12 EventPrivacy::Public, 13 AuthorRole::Any, 14 ContentSchema::PlainText, 15 EventDiscriminator::KindOnly, 16 NO_TAGS, 17 SOCIAL_REDUCERS, 18 ), 19 event_contract!( 20 "radroots.test.two.v1", 21 KIND_POST, 22 "Test Two", 23 "Test", 24 EventClass::Regular, 25 EventPrivacy::Public, 26 AuthorRole::Any, 27 ContentSchema::PlainText, 28 EventDiscriminator::KindOnly, 29 NO_TAGS, 30 SOCIAL_REDUCERS, 31 ), 32 ]; 33 34 static REQUIRED_MANY_TEST_TAGS: &[TagContract] = &[tag( 35 "test_many", 36 TagCardinality::RequiredMany, 37 TagSemantic::Topic, 38 TagValueType::Text, 39 false, 40 )]; 41 42 static OPTIONAL_ONE_TEST_TAGS: &[TagContract] = &[tag( 43 "test_optional", 44 TagCardinality::OptionalOne, 45 TagSemantic::Topic, 46 TagValueType::Text, 47 false, 48 )]; 49 50 static DUPLICATE_REQUIRED_TEST_TAGS: &[TagContract] = &[ 51 tag( 52 "test_required", 53 TagCardinality::RequiredOne, 54 TagSemantic::Topic, 55 TagValueType::Text, 56 false, 57 ), 58 tag( 59 "test_required", 60 TagCardinality::RequiredOne, 61 TagSemantic::Category, 62 TagValueType::Text, 63 false, 64 ), 65 ]; 66 67 static DUPLICATE_OPTIONAL_TEST_TAGS: &[TagContract] = &[ 68 tag( 69 "test_optional", 70 TagCardinality::OptionalOne, 71 TagSemantic::Topic, 72 TagValueType::Text, 73 false, 74 ), 75 tag( 76 "test_optional", 77 TagCardinality::OptionalOne, 78 TagSemantic::Category, 79 TagValueType::Text, 80 false, 81 ), 82 ]; 83 84 fn synthetic_event_contract(id: &'static str, tags: &'static [TagContract]) -> EventContract { 85 EventContract { 86 id, 87 kind: KIND_POST, 88 name: "Test", 89 payload_type: "Test", 90 class: EventClass::Regular, 91 stability: EventStability::Experimental, 92 privacy: EventPrivacy::Public, 93 required_author_role: AuthorRole::Any, 94 content_schema: ContentSchema::PlainText, 95 authoring_policy: EventAuthoringPolicy::GenericDraft, 96 discriminator: EventDiscriminator::KindOnly, 97 tags, 98 reducers: SOCIAL_REDUCERS, 99 } 100 } 101 102 fn synthetic_kind_contract(kind: u32) -> KindContract { 103 KindContract { 104 kind, 105 canonical_constant: "KIND_TEST", 106 name: "Test", 107 class: EventClass::Regular, 108 standard: NostrStandard::Radroots, 109 accepted_event_contracts: &[], 110 } 111 } 112 113 #[test] 114 fn author_role_catalog_is_event_owned_complete_and_schema_stable() { 115 let declared = AuthorRole::ALL.into_iter().collect::<BTreeSet<_>>(); 116 let used = all_event_contracts_registry_v7() 117 .iter() 118 .map(EventContract::required_author_role) 119 .collect::<BTreeSet<_>>(); 120 let labels = AuthorRole::ALL 121 .into_iter() 122 .map(AuthorRole::as_str) 123 .collect::<BTreeSet<_>>(); 124 125 assert_eq!(used, declared, "every event author role must be exercised"); 126 assert_eq!(labels.len(), AuthorRole::ALL.len()); 127 assert!( 128 labels.iter().all(|label| { 129 !label.is_empty() 130 && label 131 .bytes() 132 .all(|byte| byte.is_ascii_lowercase() || byte == b'_') 133 }), 134 "author-role manifest labels must remain canonical snake case" 135 ); 136 } 137 138 #[test] 139 fn authoring_policy_matrix_is_centralized_and_complete() { 140 let generic = EventAuthoringPolicy::GenericDraft; 141 assert!(generic.permits_generic_draft()); 142 assert!(generic.permits_typed_authoring()); 143 assert!(!generic.is_read_only()); 144 145 let typed = EventAuthoringPolicy::TypedOnly; 146 assert!(!typed.permits_generic_draft()); 147 assert!(typed.permits_typed_authoring()); 148 assert!(!typed.is_read_only()); 149 150 let read_only = EventAuthoringPolicy::ReadOnly; 151 assert!(!read_only.permits_generic_draft()); 152 assert!(!read_only.permits_typed_authoring()); 153 assert!(read_only.is_read_only()); 154 } 155 156 fn unsigned_event(kind: u32, tags: Vec<Vec<&str>>, content: &str) -> EventEnvelope { 157 EventEnvelope::new(EventEnvelopeParts { 158 id: "0".repeat(64), 159 author: crate::test_valid_hex_64('1'), 160 created_at: 1_700_000_000, 161 kind, 162 tags: tags 163 .into_iter() 164 .map(|tag| tag.into_iter().map(ToOwned::to_owned).collect()) 165 .collect(), 166 content: content.to_owned(), 167 sig: "2".repeat(128), 168 }) 169 .expect("event envelope") 170 } 171 172 fn unsigned_event_owned(kind: u32, tags: Vec<Vec<String>>, content: &str) -> EventEnvelope { 173 EventEnvelope::new(EventEnvelopeParts { 174 id: "0".repeat(64), 175 author: crate::test_valid_hex_64('1'), 176 created_at: 1_700_000_000, 177 kind, 178 tags, 179 content: content.to_owned(), 180 sig: "2".repeat(128), 181 }) 182 .expect("event envelope") 183 } 184 185 fn hex_64(character: char) -> String { 186 crate::test_valid_hex_64(character) 187 } 188 189 fn event_ref_tag(name: &str, event_id: &str, author: &str, kind: u32) -> Vec<String> { 190 vec![ 191 name.to_owned(), 192 event_id.to_owned(), 193 author.to_owned(), 194 kind.to_string(), 195 String::new(), 196 ] 197 } 198 199 fn owned_tag(values: &[&str]) -> Vec<String> { 200 values.iter().map(|value| (*value).to_owned()).collect() 201 } 202 203 #[test] 204 fn exposes_one_kind_contract_per_supported_kind() { 205 let mut kinds = BTreeSet::new(); 206 for contract in all_kind_contracts() { 207 assert!( 208 kinds.insert(contract.kind), 209 "duplicate kind {}", 210 contract.kind 211 ); 212 assert!(!contract.accepted_event_contracts.is_empty()); 213 } 214 } 215 216 #[test] 217 fn exposes_unique_event_contract_ids() { 218 let mut ids = BTreeSet::new(); 219 for contract in all_event_contracts() { 220 assert!( 221 ids.insert(contract.id), 222 "duplicate event contract {}", 223 contract.id 224 ); 225 assert!(kind_contract(contract.kind).is_some()); 226 } 227 } 228 229 #[test] 230 fn every_kind_references_known_matching_event_contracts() { 231 for kind in all_kind_contracts() { 232 for id in kind.accepted_event_contracts { 233 let event = event_contract(id).expect("accepted event contract"); 234 assert_eq!(event.kind, kind.kind, "{}", id); 235 } 236 } 237 } 238 239 #[test] 240 fn event_contract_classes_match_kind_contracts() { 241 for contract in all_event_contracts() { 242 let kind = kind_contract(contract.kind).expect("event kind contract"); 243 assert_eq!(contract.class, kind.class, "{}", contract.id); 244 } 245 } 246 247 #[test] 248 fn every_event_contract_is_listed_by_its_kind_contract() { 249 for contract in all_event_contracts() { 250 let kind = kind_contract(contract.kind).expect("event kind contract"); 251 assert!( 252 kind.accepted_event_contracts.contains(&contract.id), 253 "{}", 254 contract.id 255 ); 256 } 257 } 258 259 #[test] 260 fn calendar_contracts_expose_current_nip52_types_content_and_tags() { 261 let date = event_contract("radroots.calendar.date_event.v1").expect("calendar date"); 262 assert_eq!( 263 date.payload_type, 264 "RadrootsAuthoredCalendarDateEvent / RadrootsParsedNip52CalendarDateEvent / RadrootsAdmittedCalendarDateEvent" 265 ); 266 assert_eq!(date.content_schema, ContentSchema::PlainText); 267 assert_eq!(date.authoring_policy(), EventAuthoringPolicy::TypedOnly); 268 assert!( 269 date.tags 270 .iter() 271 .any(|tag| { tag.name == "title" && tag.cardinality == TagCardinality::RequiredOne }) 272 ); 273 assert!( 274 date.tags 275 .iter() 276 .any(|tag| { tag.name == "name" && tag.cardinality == TagCardinality::OptionalOne }) 277 ); 278 assert!( 279 date.tags 280 .iter() 281 .any(|tag| tag.name == "start" && tag.value_type == TagValueType::CalendarDate) 282 ); 283 assert!(!date.tags.iter().any(|tag| tag.name == "D")); 284 285 let time = event_contract("radroots.calendar.time_event.v1").expect("calendar time"); 286 assert_eq!( 287 time.payload_type, 288 "RadrootsAuthoredCalendarTimeEvent / RadrootsParsedNip52CalendarTimeEvent / RadrootsAdmittedCalendarTimeEvent" 289 ); 290 assert_eq!(time.content_schema, ContentSchema::PlainText); 291 assert_eq!(time.authoring_policy(), EventAuthoringPolicy::TypedOnly); 292 assert!(time.tags.iter().any(|tag| { 293 tag.name == "D" 294 && tag.cardinality == TagCardinality::RequiredMany 295 && tag.value_type == TagValueType::UtcDayIndex 296 })); 297 assert!( 298 time.tags 299 .iter() 300 .any(|tag| { tag.name == "name" && tag.cardinality == TagCardinality::OptionalOne }) 301 ); 302 303 let calendar = event_contract("radroots.calendar.collection.v1").expect("calendar"); 304 assert_eq!(calendar.payload_type, "RadrootsAdmittedCalendar"); 305 assert_eq!(calendar.content_schema, ContentSchema::PlainText); 306 assert!(calendar.tags.iter().any(|tag| { 307 tag.name == "a" 308 && tag.cardinality == TagCardinality::OptionalMany 309 && tag.value_type == TagValueType::CalendarEventCoordinate 310 })); 311 312 let rsvp = event_contract("radroots.calendar.rsvp.v1").expect("calendar RSVP"); 313 assert_eq!(rsvp.payload_type, "RadrootsAdmittedCalendarEventRsvp"); 314 assert_eq!(rsvp.content_schema, ContentSchema::PlainText); 315 assert!(rsvp.tags.iter().any(|tag| { 316 tag.name == "status" 317 && tag.cardinality == TagCardinality::RequiredOne 318 && tag.value_type == TagValueType::CalendarRsvpStatus 319 })); 320 } 321 322 #[test] 323 fn calendar_collection_and_rsvp_contracts_enforce_strict_nip52_shapes() { 324 let author = "a".repeat(64); 325 let event_coordinate = format!("31923:{author}:wash-pack"); 326 let second_coordinate = format!("31922:{author}:market-day"); 327 let event_id = "b".repeat(64); 328 let blossom_image = format!("https://media.example/{}.webp", "c".repeat(64)); 329 let collection = vec![ 330 owned_tag(&["d", "AAAAAAAAAAAAAAAAAAAAAA"]), 331 owned_tag(&["title", "Farm calendar"]), 332 owned_tag(&["description", "Upcoming farm work"]), 333 vec!["image".to_owned(), blossom_image], 334 vec![ 335 "a".to_owned(), 336 event_coordinate.clone(), 337 "wss://relay.example/events".to_owned(), 338 ], 339 vec!["a".to_owned(), second_coordinate], 340 ]; 341 assert_eq!( 342 validate_event_contract_parts( 343 KIND_CALENDAR, 344 &collection, 345 "Detailed shared schedule.", 346 "radroots.calendar.collection.v1", 347 ), 348 Ok(()) 349 ); 350 351 let mut duplicate_collection = collection.clone(); 352 duplicate_collection.push(vec!["a".to_owned(), event_coordinate.clone()]); 353 assert!(matches!( 354 validate_event_contract_parts( 355 KIND_CALENDAR, 356 &duplicate_collection, 357 "", 358 "radroots.calendar.collection.v1", 359 ), 360 Err(ContractValidationError::TagValueMismatch { name: "a", .. }) 361 )); 362 363 let rsvp = vec![ 364 owned_tag(&["d", "AAAAAAAAAAAAAAAAAAAAAQ"]), 365 vec![ 366 "a".to_owned(), 367 event_coordinate.clone(), 368 "wss://relay.example/a".to_owned(), 369 ], 370 vec!["e".to_owned(), event_id, "wss://relay.example/e".to_owned()], 371 owned_tag(&["status", "tentative"]), 372 owned_tag(&["fb", "busy"]), 373 vec![ 374 "p".to_owned(), 375 author.clone(), 376 "wss://relay.example/p".to_owned(), 377 ], 378 ]; 379 assert_eq!( 380 validate_event_contract_parts( 381 KIND_CALENDAR_EVENT_RSVP, 382 &rsvp, 383 "I expect to attend.", 384 "radroots.calendar.rsvp.v1", 385 ), 386 Ok(()) 387 ); 388 389 let mut mismatched_author = rsvp.clone(); 390 let participant = mismatched_author 391 .iter_mut() 392 .find(|tag| tag.first().map(String::as_str) == Some("p")) 393 .unwrap(); 394 participant[1] = "d".repeat(64); 395 assert!(matches!( 396 validate_event_contract_parts( 397 KIND_CALENDAR_EVENT_RSVP, 398 &mismatched_author, 399 "", 400 "radroots.calendar.rsvp.v1", 401 ), 402 Err(ContractValidationError::TagValueMismatch { name: "p", .. }) 403 )); 404 405 let declined_with_observed_free_busy = vec![ 406 owned_tag(&["d", "AAAAAAAAAAAAAAAAAAAAAg"]), 407 vec!["a".to_owned(), event_coordinate], 408 owned_tag(&["status", "declined"]), 409 owned_tag(&["fb", "free"]), 410 ]; 411 assert_eq!( 412 validate_event_contract_parts( 413 KIND_CALENDAR_EVENT_RSVP, 414 &declined_with_observed_free_busy, 415 "", 416 "radroots.calendar.rsvp.v1", 417 ), 418 Ok(()) 419 ); 420 } 421 422 #[test] 423 fn calendar_date_contract_validates_gregorian_exclusive_ranges_without_day_tags() { 424 let valid = vec![ 425 owned_tag(&["d", "market-day"]), 426 owned_tag(&["title", "Market day"]), 427 owned_tag(&["start", "2026-06-20"]), 428 owned_tag(&["end", "2026-06-21"]), 429 owned_tag(&["location", "Moss Street Market"]), 430 owned_tag(&["location", "Victoria, BC"]), 431 ]; 432 assert_eq!( 433 validate_event_contract_parts( 434 KIND_CALENDAR_DATE_EVENT, 435 &valid, 436 "Farm stand pickup window.", 437 "radroots.calendar.date_event.v1", 438 ), 439 Ok(()) 440 ); 441 442 let duplicate_legacy_name = vec![ 443 owned_tag(&["d", "market-day"]), 444 owned_tag(&["title", "Market day"]), 445 owned_tag(&["name", "Market day"]), 446 owned_tag(&["name", "Deprecated duplicate"]), 447 owned_tag(&["start", "2026-06-20"]), 448 ]; 449 assert!(matches!( 450 validate_event_contract_parts( 451 KIND_CALENDAR_DATE_EVENT, 452 &duplicate_legacy_name, 453 "description", 454 "radroots.calendar.date_event.v1", 455 ), 456 Err(ContractValidationError::TagCardinalityMismatch { name: "name", .. }) 457 )); 458 459 for invalid in [ 460 vec![ 461 owned_tag(&["d", "market-day"]), 462 owned_tag(&["title", "Market day"]), 463 owned_tag(&["start", "2026-02-29"]), 464 ], 465 vec![ 466 owned_tag(&["d", "market-day"]), 467 owned_tag(&["title", "Market day"]), 468 owned_tag(&["start", "2026-06-20"]), 469 owned_tag(&["end", "2026-06-20"]), 470 ], 471 vec![ 472 owned_tag(&["d", "market-day"]), 473 owned_tag(&["title", "Market day"]), 474 owned_tag(&["start", "2026-06-20"]), 475 owned_tag(&["D", "20624"]), 476 ], 477 ] { 478 assert!(matches!( 479 validate_event_contract_parts( 480 KIND_CALENDAR_DATE_EVENT, 481 &invalid, 482 "description", 483 "radroots.calendar.date_event.v1", 484 ), 485 Err(ContractValidationError::TagValueMismatch { .. }) 486 )); 487 } 488 } 489 490 #[test] 491 fn calendar_time_contract_requires_exact_derived_bounded_day_coverage() { 492 let valid = vec![ 493 owned_tag(&["d", "wash-pack"]), 494 owned_tag(&["title", "Wash and pack"]), 495 owned_tag(&["start", "86399"]), 496 owned_tag(&["end", "86401"]), 497 owned_tag(&["D", "0"]), 498 owned_tag(&["D", "1"]), 499 ]; 500 assert_eq!( 501 validate_event_contract_parts( 502 KIND_CALENDAR_TIME_EVENT, 503 &valid, 504 "Pack CSA shares.", 505 "radroots.calendar.time_event.v1", 506 ), 507 Ok(()) 508 ); 509 510 for invalid in [ 511 vec![ 512 owned_tag(&["d", "wash-pack"]), 513 owned_tag(&["title", "Wash and pack"]), 514 owned_tag(&["start", "086399"]), 515 owned_tag(&["D", "0"]), 516 ], 517 vec![ 518 owned_tag(&["d", "wash-pack"]), 519 owned_tag(&["title", "Wash and pack"]), 520 owned_tag(&["start", "86399"]), 521 owned_tag(&["end", "86401"]), 522 owned_tag(&["D", "1"]), 523 owned_tag(&["D", "0"]), 524 ], 525 vec![ 526 owned_tag(&["d", "wash-pack"]), 527 owned_tag(&["title", "Wash and pack"]), 528 owned_tag(&["start", "0"]), 529 owned_tag(&["end", "31708800"]), 530 owned_tag(&["D", "0"]), 531 ], 532 ] { 533 assert!(matches!( 534 validate_event_contract_parts( 535 KIND_CALENDAR_TIME_EVENT, 536 &invalid, 537 "description", 538 "radroots.calendar.time_event.v1", 539 ), 540 Err(ContractValidationError::TagValueMismatch { .. }) 541 )); 542 } 543 } 544 545 #[test] 546 fn trade_mutation_contract_requires_exact_contract_tag() { 547 for contract_id in [ 548 "radroots.trade.proposal.v1", 549 "radroots.trade.decision.v1", 550 "radroots.trade.revision_proposal.v1", 551 "radroots.trade.revision_decision.v1", 552 "radroots.trade.cancellation.v1", 553 ] { 554 let contract = event_contract(contract_id).expect("trade mutation contract"); 555 assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly); 556 assert_eq!( 557 contract.tags.iter().map(|tag| tag.name).collect::<Vec<_>>(), 558 ["contract", "d", "x", "p"] 559 ); 560 let contract_tag = &contract.tags[0]; 561 assert_eq!(contract_tag.semantic, TagSemantic::Contract); 562 assert_eq!(contract_tag.value_type, TagValueType::ContractId); 563 assert!(!contract_tag.relay_indexed); 564 let mutation = &contract.tags[2]; 565 assert_eq!(mutation.cardinality, TagCardinality::RequiredMany); 566 assert_eq!(mutation.semantic, TagSemantic::TradeMutation); 567 assert_eq!(mutation.value_type, TagValueType::MutationId); 568 assert!(mutation.relay_indexed); 569 let parties = &contract.tags[3]; 570 assert_eq!(parties.cardinality, TagCardinality::RequiredMany); 571 assert_eq!(parties.semantic, TagSemantic::Participant); 572 assert_eq!(parties.value_type, TagValueType::PublicKey); 573 } 574 } 575 576 #[test] 577 fn covers_public_kind_arrays() { 578 for kind in COMMERCIAL_EVENT_KINDS 579 .iter() 580 .chain(PUBLIC_SOCIAL_KINDS.iter()) 581 .chain(PRIVATE_FARM_OPS_KINDS.iter()) 582 .chain(NIP29_GROUP_KINDS.iter()) 583 .chain(KNOWLEDGE_EVENT_KINDS.iter()) 584 { 585 assert!(kind_contract(*kind).is_some(), "missing kind {kind}"); 586 } 587 } 588 589 #[test] 590 fn classified_listing_kind_profiles_are_partitioned_and_explicit() { 591 let kind = kind_contract(KIND_CLASSIFIED_LISTING).expect("classified listing kind"); 592 assert_eq!(kind.canonical_constant, "KIND_CLASSIFIED_LISTING"); 593 assert_eq!(kind.name, "Classified Listing"); 594 assert_eq!(kind.class, EventClass::Addressable); 595 assert_eq!(kind.standard, NostrStandard::Nip99); 596 assert_eq!( 597 kind.accepted_event_contracts, 598 &[ 599 "radroots.operational_listing.published.v1", 600 "radroots.food.availability.v1", 601 ] 602 ); 603 604 let operational = event_contract("radroots.operational_listing.published.v1") 605 .expect("operational listing profile"); 606 assert_eq!(operational.name, "Operational Listing"); 607 assert_eq!(operational.payload_type, "RadrootsOperationalListing"); 608 assert_eq!(operational.content_schema, ContentSchema::Markdown); 609 assert_eq!( 610 operational.discriminator, 611 EventDiscriminator::ClassifiedListingPartition( 612 ClassifiedListingPartition::OperationalListing, 613 ) 614 ); 615 616 let food = 617 event_contract("radroots.food.availability.v1").expect("focused food availability profile"); 618 assert_eq!(food.name, "Food Availability"); 619 assert_eq!( 620 food.payload_type, 621 "RadrootsFoodAvailabilityDetails / RadrootsInboundFoodAvailabilityProjection" 622 ); 623 assert_eq!(food.class, EventClass::Addressable); 624 assert_eq!(food.privacy, EventPrivacy::Public); 625 assert_eq!(food.required_author_role(), AuthorRole::Seller); 626 assert_eq!(food.content_schema, ContentSchema::Markdown); 627 assert_eq!(food.authoring_policy(), EventAuthoringPolicy::TypedOnly); 628 assert_eq!(food.discriminator, EventDiscriminator::AdmissionOnly); 629 assert_eq!(food.reducers, &[Reducer::MarketProjection]); 630 assert_eq!(event_contract_family(food), Some(ContractFamily::Market)); 631 assert_eq!( 632 food.tags 633 .iter() 634 .map(|tag| (tag.name, tag.cardinality)) 635 .collect::<Vec<_>>(), 636 vec![ 637 ("d", TagCardinality::RequiredOne), 638 ("title", TagCardinality::RequiredOne), 639 ("summary", TagCardinality::RequiredOne), 640 ("published_at", TagCardinality::RequiredOne), 641 ("location", TagCardinality::RequiredOne), 642 ("price", TagCardinality::RequiredOne), 643 ("radroots:price_unit", TagCardinality::RequiredOne), 644 ("radroots:quantity", TagCardinality::OptionalOne), 645 ("status", TagCardinality::RequiredOne), 646 ("image", TagCardinality::OptionalMany), 647 ] 648 ); 649 assert!(event_contract("radroots.listing.published.v1").is_none()); 650 651 let seller = "a".repeat(64); 652 let tags = vec![ 653 owned_tag(&["d", "carrots"]), 654 owned_tag(&["p", seller.as_str()]), 655 owned_tag(&["a", format!("30340:{seller}:victoria-farm").as_str()]), 656 owned_tag(&["key", "carrots"]), 657 owned_tag(&["title", "Carrots"]), 658 owned_tag(&["category", "produce"]), 659 owned_tag(&["radroots:primary_bin", "field-bin"]), 660 owned_tag(&["radroots:bin", "field-bin", "1000", "g"]), 661 owned_tag(&["radroots:price", "field-bin", "3", "CAD", "1", "lb"]), 662 ]; 663 assert_eq!( 664 validate_event_contract_parts( 665 KIND_CLASSIFIED_LISTING, 666 &tags, 667 "# Carrots", 668 "radroots.operational_listing.published.v1", 669 ), 670 Ok(()) 671 ); 672 assert_eq!( 673 identify_event_contract(KIND_CLASSIFIED_LISTING, &tags, "# Carrots") 674 .expect("operational discriminator") 675 .id, 676 operational.id 677 ); 678 679 let malformed_operational = vec![owned_tag(&["radroots:bin"])]; 680 assert_eq!( 681 identify_event_contract(KIND_CLASSIFIED_LISTING, &malformed_operational, "Carrots",) 682 .expect("raw operational marker partitions before shape validation") 683 .id, 684 operational.id 685 ); 686 assert_eq!( 687 validate_event_contract_parts( 688 KIND_CLASSIFIED_LISTING, 689 &malformed_operational, 690 "Carrots", 691 operational.id, 692 ), 693 Err(ContractValidationError::MissingTag { 694 contract_id: "radroots.operational_listing.published.v1", 695 name: "d", 696 }) 697 ); 698 699 let focused = vec![owned_tag(&["radroots:price_unit", "lb"])]; 700 assert_eq!( 701 identify_event_contract(KIND_CLASSIFIED_LISTING, &focused, "Carrots"), 702 Err(ContractMatchError::UnsupportedShape( 703 KIND_CLASSIFIED_LISTING 704 )) 705 ); 706 assert_eq!( 707 validate_event_contract_parts(KIND_CLASSIFIED_LISTING, &focused, "Carrots", food.id,), 708 Err(ContractValidationError::AdmissionRequired { 709 contract_id: "radroots.food.availability.v1", 710 }) 711 ); 712 assert_eq!( 713 validate_event_contract_parts(KIND_CLASSIFIED_LISTING, &focused, "Carrots", operational.id,), 714 Err(ContractValidationError::ContractMatch { 715 error: ContractMatchError::UnsupportedShape(KIND_CLASSIFIED_LISTING), 716 }) 717 ); 718 719 let generic = vec![owned_tag(&["d", "carrots"])]; 720 assert_eq!( 721 identify_event_contract(KIND_CLASSIFIED_LISTING, &generic, "Carrots"), 722 Err(ContractMatchError::UnsupportedShape( 723 KIND_CLASSIFIED_LISTING 724 )) 725 ); 726 727 let ambiguous = vec![ 728 owned_tag(&["radroots:price_unit"]), 729 owned_tag(&["radroots:primary_bin"]), 730 ]; 731 assert_eq!( 732 identify_event_contract(KIND_CLASSIFIED_LISTING, &ambiguous, "Carrots"), 733 Err(ContractMatchError::UnsupportedShape( 734 KIND_CLASSIFIED_LISTING 735 )) 736 ); 737 } 738 739 #[test] 740 fn event_contract_lookup_supports_many_contracts_per_kind() { 741 let contracts = event_contracts_for_kind(KIND_LIST_SET_GENERIC).collect::<Vec<_>>(); 742 assert_eq!(contracts.len(), 6); 743 assert!( 744 contracts 745 .iter() 746 .any(|contract| contract.id == "radroots.list_set.farm.members.v1") 747 ); 748 assert_eq!( 749 event_contract("radroots.list_set.member_of.farms.v1").map(|contract| contract.kind), 750 Some(KIND_LIST_SET_GENERIC) 751 ); 752 assert!(event_contracts_for_kind(999_999).next().is_none()); 753 } 754 755 #[test] 756 fn event_contract_lookup_supports_knowledge_contract_kinds() { 757 let contracts = event_contracts_for_kind(KIND_WIKI_ARTICLE).collect::<Vec<_>>(); 758 assert_eq!(contracts.len(), 1); 759 assert_eq!(contracts[0].id, "radroots.wiki.article.v1"); 760 assert_eq!( 761 identify_event_contract(KIND_WIKI_ARTICLE, &[], "# Soil") 762 .expect("wiki article contract") 763 .id, 764 "radroots.wiki.article.v1" 765 ); 766 } 767 768 #[test] 769 fn exposes_contract_family_metadata() { 770 assert!( 771 contract_families() 772 .iter() 773 .any(|family| family.family == ContractFamily::Knowledge && family.id == "knowledge") 774 ); 775 assert_eq!( 776 event_contract_family(event_contract("radroots.wiki.article.v1").expect("wiki")), 777 Some(ContractFamily::Knowledge) 778 ); 779 assert_eq!( 780 kind_contract_family(kind_contract(KIND_KNOWLEDGE_CLAIM).expect("claim kind")), 781 Some(ContractFamily::Knowledge) 782 ); 783 assert_eq!( 784 kind_contract_family(kind_contract(KIND_LIST_SET_GENERIC).expect("list kind")), 785 Some(ContractFamily::List) 786 ); 787 } 788 789 #[test] 790 fn contract_family_helpers_cover_prefixes_and_kind_branches() { 791 for (id, family) in [ 792 ("radroots.account.test.v1", Some(ContractFamily::Account)), 793 ( 794 "radroots.application.test.v1", 795 Some(ContractFamily::Application), 796 ), 797 ("radroots.calendar.test.v1", Some(ContractFamily::Calendar)), 798 ("radroots.farm.test.v1", Some(ContractFamily::Farm)), 799 ("radroots.group.test.v1", Some(ContractFamily::Group)), 800 ("radroots.http.test.v1", Some(ContractFamily::Http)), 801 ("radroots.job.test.v1", Some(ContractFamily::Job)), 802 ( 803 "radroots.knowledge.test.v1", 804 Some(ContractFamily::Knowledge), 805 ), 806 ("radroots.wiki.test.v1", Some(ContractFamily::Knowledge)), 807 ("radroots.list.test.v1", Some(ContractFamily::List)), 808 ("radroots.list_set.test.v1", Some(ContractFamily::List)), 809 ( 810 "radroots.operational_listing.test.v1", 811 Some(ContractFamily::Market), 812 ), 813 ("radroots.food.test.v1", Some(ContractFamily::Market)), 814 ("radroots.message.test.v1", Some(ContractFamily::Message)), 815 ("radroots.profile.test.v1", Some(ContractFamily::Profile)), 816 ("radroots.relay.test.v1", Some(ContractFamily::Relay)), 817 ("radroots.social.test.v1", Some(ContractFamily::Social)), 818 ("radroots.trade.test.v1", Some(ContractFamily::Trade)), 819 ("radroots.order.test.v1", None), 820 ("radroots.test.unknown.v1", None), 821 ] { 822 assert_eq!(contract_family_for_id(id), family, "{id}"); 823 } 824 825 for (kind, family) in [ 826 (KIND_PROFILE, ContractFamily::Profile), 827 (KIND_MESSAGE, ContractFamily::Message), 828 (KIND_POST, ContractFamily::Social), 829 (KIND_RELAY_AUTH, ContractFamily::Relay), 830 (KIND_GROUP_ROLES, ContractFamily::Group), 831 (KIND_LIST_SET_GENERIC, ContractFamily::List), 832 (KIND_CALENDAR_EVENT_RSVP, ContractFamily::Calendar), 833 (KIND_FARM_CRDT_CHANGE, ContractFamily::Farm), 834 (KIND_CLASSIFIED_LISTING, ContractFamily::Market), 835 (KIND_TRADE_CANCELLATION, ContractFamily::Trade), 836 (KIND_KNOWLEDGE_CLAIM, ContractFamily::Knowledge), 837 (KIND_JOB_FEEDBACK, ContractFamily::Job), 838 (KIND_JOB_REQUEST_MIN, ContractFamily::Job), 839 (KIND_JOB_RESULT_MIN, ContractFamily::Job), 840 ] { 841 assert_eq!( 842 kind_contract_family(&synthetic_kind_contract(kind)), 843 Some(family), 844 "{kind}" 845 ); 846 } 847 848 assert_eq!( 849 kind_contract_family(&synthetic_kind_contract(999_999)), 850 None 851 ); 852 } 853 854 #[test] 855 fn scalar_contract_validators_cover_canonical_boundaries() { 856 assert_eq!(canonical_u64("0"), Some(0)); 857 assert_eq!(canonical_u64(u64::MAX.to_string().as_str()), Some(u64::MAX)); 858 for invalid in ["", "00", "01", "+1", "18446744073709551616"] { 859 assert_eq!(canonical_u64(invalid), None, "{invalid}"); 860 } 861 862 for valid in ["0", "u4pruydqqvj", "U4PRUYDQQVJ"] { 863 assert!(geohash_is_valid(valid), "{valid}"); 864 } 865 for invalid in ["", "u4pruydqqvjz0x", "a", "u4pruydqqv-i"] { 866 assert!(!geohash_is_valid(invalid), "{invalid}"); 867 } 868 } 869 870 #[test] 871 fn exposes_knowledge_contracts() { 872 let wiki_article = event_contract("radroots.wiki.article.v1").expect("wiki article"); 873 assert_eq!(wiki_article.kind, KIND_WIKI_ARTICLE); 874 assert_eq!(wiki_article.stability, EventStability::Experimental); 875 assert_eq!( 876 kind_contract(KIND_WIKI_ARTICLE) 877 .expect("wiki kind") 878 .standard, 879 NostrStandard::Nip54 880 ); 881 assert_eq!(wiki_article.content_schema, ContentSchema::Djot); 882 883 let wiki_merge_request = 884 event_contract("radroots.wiki.merge_request.v1").expect("wiki merge request"); 885 assert_eq!(wiki_merge_request.stability, EventStability::Experimental); 886 assert_eq!(wiki_merge_request.content_schema, ContentSchema::PlainText); 887 888 let wiki_redirect = event_contract("radroots.wiki.redirect.v1").expect("wiki redirect"); 889 assert_eq!(wiki_redirect.kind, KIND_WIKI_REDIRECT); 890 assert_eq!(wiki_redirect.stability, EventStability::Experimental); 891 assert_eq!(wiki_redirect.content_schema, ContentSchema::Empty); 892 893 for id in [ 894 "radroots.knowledge.source.v1", 895 "radroots.knowledge.evidence_bounty.v1", 896 "radroots.knowledge.claim.v1", 897 "radroots.knowledge.relation.v1", 898 "radroots.knowledge.review.v1", 899 "radroots.knowledge.field_report.v1", 900 "radroots.knowledge.change_proposal.v1", 901 "radroots.knowledge.contribution_attestation.v1", 902 ] { 903 let contract = event_contract(id).expect(id); 904 assert_eq!(contract.stability, EventStability::Experimental); 905 assert_eq!( 906 event_contract_family(contract), 907 Some(ContractFamily::Knowledge) 908 ); 909 let contract_tag = contract 910 .tags 911 .iter() 912 .find(|tag| tag.name == "contract") 913 .expect("contract tag"); 914 assert_eq!(contract_tag.semantic, TagSemantic::Contract); 915 assert_eq!(contract_tag.value_type, TagValueType::ContractId); 916 } 917 } 918 919 #[test] 920 fn custom_knowledge_schema_lookup_covers_registered_ids() { 921 for id in [ 922 "radroots.knowledge.source.v1", 923 "radroots.knowledge.evidence_bounty.v1", 924 "radroots.knowledge.claim.v1", 925 "radroots.knowledge.relation.v1", 926 "radroots.knowledge.review.v1", 927 "radroots.knowledge.field_report.v1", 928 "radroots.knowledge.change_proposal.v1", 929 "radroots.knowledge.contribution_attestation.v1", 930 ] { 931 assert_eq!(custom_knowledge_schema(id), Some(id), "{id}"); 932 } 933 assert_eq!(custom_knowledge_schema("radroots.wiki.article.v1"), None); 934 } 935 936 #[test] 937 fn post_subtype_contracts_require_verified_admission() { 938 let tags = vec![vec!["t".to_owned(), "radroots-ask".to_owned()]]; 939 let generic = identify_event_contract(KIND_POST, &tags, "Question") 940 .expect("unsigned kind-1 identification remains generic"); 941 assert_eq!(generic.id, "radroots.social.post.v1"); 942 assert_eq!(generic.authoring_policy(), EventAuthoringPolicy::ReadOnly); 943 944 for id in [ 945 "radroots.social.update.v1", 946 "radroots.social.photo_update.v1", 947 "radroots.social.ask.v1", 948 "radroots.social.reply.v1", 949 ] { 950 let contract = event_contract(id).expect(id); 951 assert_eq!( 952 event_contract_family(contract), 953 Some(ContractFamily::Social) 954 ); 955 assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly); 956 assert_eq!( 957 validate_event_contract_parts(KIND_POST, &tags, "Question", id), 958 Err(ContractValidationError::AdmissionRequired { contract_id: id }) 959 ); 960 } 961 962 assert_eq!( 963 event_contract("radroots.profile.metadata.v1") 964 .expect("strict authored profile contract") 965 .authoring_policy(), 966 EventAuthoringPolicy::TypedOnly 967 ); 968 assert_eq!( 969 event_contract("radroots.social.geochat.v1") 970 .expect("generic-draft control contract") 971 .authoring_policy(), 972 EventAuthoringPolicy::GenericDraft 973 ); 974 } 975 976 #[test] 977 fn nip22_comment_contract_is_typed_and_admission_only() { 978 assert_eq!(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, 7); 979 980 let kind = kind_contract(KIND_COMMENT).expect("Comment kind"); 981 assert_eq!(kind.canonical_constant, "KIND_COMMENT"); 982 assert_eq!(kind.kind, 1111); 983 assert_eq!(kind.class, EventClass::Regular); 984 assert_eq!(kind.standard, NostrStandard::Nip22); 985 assert_eq!( 986 kind.accepted_event_contracts, 987 &["radroots.social.comment.v1"] 988 ); 989 990 let contract = event_contract("radroots.social.comment.v1").expect("NIP-22 Comment contract"); 991 assert_eq!( 992 contract.payload_type, 993 "RadrootsAuthoredNip22Comment / RadrootsInboundNip22CommentProjection" 994 ); 995 assert_eq!(contract.kind, KIND_COMMENT); 996 assert_eq!(contract.class, EventClass::Regular); 997 assert_eq!(contract.privacy, EventPrivacy::Public); 998 assert_eq!(contract.required_author_role(), AuthorRole::Any); 999 assert_eq!(contract.content_schema, ContentSchema::PlainText); 1000 assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly); 1001 assert_eq!(contract.discriminator, EventDiscriminator::AdmissionOnly); 1002 assert_eq!(contract.reducers, &[Reducer::SocialProjection]); 1003 assert_eq!( 1004 event_contract_family(contract), 1005 Some(ContractFamily::Social) 1006 ); 1007 assert_eq!( 1008 contract.tags.iter().map(|tag| tag.name).collect::<Vec<_>>(), 1009 vec!["E", "A", "K", "P", "a", "e", "k", "p"] 1010 ); 1011 assert!( 1012 contract.tags.iter().all(|tag| tag.relay_indexed), 1013 "every single-letter NIP-22 tag must remain relay-indexed" 1014 ); 1015 assert_eq!( 1016 validate_event_contract_parts(KIND_COMMENT, &[], "Comment", contract.id), 1017 Err(ContractValidationError::AdmissionRequired { 1018 contract_id: "radroots.social.comment.v1", 1019 }) 1020 ); 1021 } 1022 1023 #[test] 1024 fn nip09_deletion_request_contract_is_typed_and_admission_only() { 1025 use crate::post::deletion::{ 1026 RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES, RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES, 1027 RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 1028 RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT, 1029 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, RADROOTS_NIP09_DELETION_TARGET_KIND_MAX, 1030 }; 1031 1032 assert_eq!(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, 7); 1033 1034 let kind = kind_contract(KIND_DELETION_REQUEST).expect("deletion request kind"); 1035 assert_eq!(kind.canonical_constant, "KIND_DELETION_REQUEST"); 1036 assert_eq!(kind.kind, 5); 1037 assert_eq!(kind.class, EventClass::Regular); 1038 assert_eq!(kind.standard, NostrStandard::Nip09); 1039 assert_eq!( 1040 kind.accepted_event_contracts, 1041 &["radroots.social.deletion_request.v1"] 1042 ); 1043 1044 let contract = event_contract("radroots.social.deletion_request.v1") 1045 .expect("NIP-09 deletion request contract"); 1046 assert_eq!( 1047 contract.payload_type, 1048 "RadrootsAuthoredNip09DeletionRequest / RadrootsInboundNip09DeletionProjection" 1049 ); 1050 assert_eq!(contract.kind, KIND_DELETION_REQUEST); 1051 assert_eq!(contract.class, EventClass::Regular); 1052 assert_eq!(contract.privacy, EventPrivacy::Public); 1053 assert_eq!(contract.required_author_role(), AuthorRole::Any); 1054 assert_eq!(contract.content_schema, ContentSchema::PlainText); 1055 assert_eq!(contract.authoring_policy(), EventAuthoringPolicy::TypedOnly); 1056 assert_eq!(contract.discriminator, EventDiscriminator::AdmissionOnly); 1057 assert_eq!(contract.reducers, &[Reducer::SocialProjection]); 1058 assert_eq!( 1059 event_contract_family(contract), 1060 Some(ContractFamily::Social) 1061 ); 1062 assert_eq!( 1063 contract 1064 .tags 1065 .iter() 1066 .map(|tag| ( 1067 tag.name, 1068 tag.cardinality, 1069 tag.semantic, 1070 tag.value_type, 1071 tag.relay_indexed, 1072 )) 1073 .collect::<Vec<_>>(), 1074 vec![ 1075 ( 1076 "e", 1077 TagCardinality::OptionalMany, 1078 TagSemantic::EventPointer, 1079 TagValueType::EventId, 1080 true, 1081 ), 1082 ( 1083 "a", 1084 TagCardinality::OptionalMany, 1085 TagSemantic::Nip01Coordinate, 1086 TagValueType::Nip01Coordinate, 1087 true, 1088 ), 1089 ( 1090 "k", 1091 TagCardinality::OptionalMany, 1092 TagSemantic::Kind, 1093 TagValueType::Kind, 1094 true, 1095 ), 1096 ] 1097 ); 1098 assert_eq!( 1099 ( 1100 RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES, 1101 RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 1102 RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT, 1103 RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, 1104 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, 1105 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES, 1106 RADROOTS_NIP09_DELETION_TARGET_KIND_MAX, 1107 ), 1108 (131_072, 1_024, 4_096, 4_096, 131_072, 262_144, 65_535) 1109 ); 1110 assert_eq!( 1111 validate_event_contract_parts(KIND_DELETION_REQUEST, &[], "superseded", contract.id,), 1112 Err(ContractValidationError::AdmissionRequired { 1113 contract_id: "radroots.social.deletion_request.v1", 1114 }) 1115 ); 1116 let selected = unsigned_event( 1117 KIND_DELETION_REQUEST, 1118 vec![vec![ 1119 "e", 1120 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1121 ]], 1122 "superseded", 1123 ); 1124 assert_eq!( 1125 validate_event_contract_for_admission(&selected, contract.id), 1126 Ok(contract) 1127 ); 1128 assert_eq!( 1129 validate_event_contract_for_admission( 1130 &unsigned_event(KIND_DELETION_REQUEST, vec![], "superseded"), 1131 contract.id, 1132 ), 1133 Err(ContractValidationError::ContractMatch { 1134 error: ContractMatchError::UnsupportedShape(KIND_DELETION_REQUEST), 1135 }) 1136 ); 1137 } 1138 1139 #[test] 1140 fn identifies_exact_list_set_shape() { 1141 let tags = vec![vec!["d".to_owned(), "member_of.farms".to_owned()]]; 1142 let contract = identify_event_contract(KIND_LIST_SET_GENERIC, &tags, "{}") 1143 .expect("member_of farms contract"); 1144 assert_eq!(contract.id, "radroots.list_set.member_of.farms.v1"); 1145 } 1146 1147 #[test] 1148 fn identifies_composite_list_set_shape() { 1149 let tags = vec![vec![ 1150 "d".to_owned(), 1151 "farm:farm_01:members.workers".to_owned(), 1152 ]]; 1153 let contract = 1154 identify_event_contract(KIND_LIST_SET_GENERIC, &tags, "{}").expect("farm workers contract"); 1155 assert_eq!(contract.id, "radroots.list_set.farm.members.workers.v1"); 1156 } 1157 1158 #[test] 1159 fn rejects_unknown_or_unsupported_shapes() { 1160 assert_eq!( 1161 identify_event_contract(999_999, &[], "{}"), 1162 Err(ContractMatchError::UnsupportedKind(999_999)) 1163 ); 1164 assert_eq!( 1165 validate_event_contract(&unsigned_event(999_999, Vec::new(), "{}")), 1166 Err(ContractValidationError::ContractMatch { 1167 error: ContractMatchError::UnsupportedKind(999_999), 1168 }) 1169 ); 1170 1171 let tags = vec![vec!["d".to_owned(), "unknown".to_owned()]]; 1172 assert_eq!( 1173 identify_event_contract(KIND_LIST_SET_GENERIC, &tags, "{}"), 1174 Err(ContractMatchError::UnsupportedShape(KIND_LIST_SET_GENERIC)) 1175 ); 1176 } 1177 1178 #[test] 1179 fn rejects_ambiguous_shapes() { 1180 assert_eq!( 1181 identify_from_contracts(AMBIGUOUS_TEST_CONTRACTS.iter(), KIND_POST, &[], ""), 1182 Err(ContractMatchError::AmbiguousShape(KIND_POST)) 1183 ); 1184 } 1185 1186 #[test] 1187 fn supports_content_field_discriminators() { 1188 assert!(discriminator_matches( 1189 &EventDiscriminator::EnvelopeType("proposal"), 1190 &[], 1191 r#"{"domain":"radroots.trade","type":"proposal"}"# 1192 )); 1193 assert!(discriminator_matches( 1194 &EventDiscriminator::ContentJsonFieldEquals { 1195 field: "domain", 1196 value: "radroots.trade" 1197 }, 1198 &[], 1199 r#"{"domain": "radroots.trade", "type": "proposal"}"# 1200 )); 1201 1202 let base = *event_contract("radroots.trade.proposal.v1").expect("trade proposal"); 1203 for discriminator in [ 1204 EventDiscriminator::ContentJsonFieldEquals { 1205 field: "type", 1206 value: "proposal", 1207 }, 1208 EventDiscriminator::EnvelopeType("proposal"), 1209 ] { 1210 let contract = EventContract { 1211 discriminator, 1212 ..base 1213 }; 1214 assert!(validate_discriminator_parts(r#"{"type":"proposal"}"#, &contract, false).is_ok()); 1215 assert!(matches!( 1216 validate_discriminator_parts(r#"{"type":"decision"}"#, &contract, false), 1217 Err(ContractValidationError::ContentFieldMismatch { .. }) 1218 )); 1219 assert!(matches!( 1220 validate_discriminator_parts("{}", &contract, false), 1221 Err(ContractValidationError::MissingContentField { .. }) 1222 )); 1223 } 1224 let contract = EventContract { 1225 discriminator: EventDiscriminator::KindOnly, 1226 ..base 1227 }; 1228 assert!(validate_discriminator_parts("not-json", &contract, false).is_ok()); 1229 } 1230 1231 #[test] 1232 fn supports_tag_equals_discriminators() { 1233 let tags = vec![vec!["status".to_owned(), "accepted".to_owned()]]; 1234 1235 assert!(discriminator_matches( 1236 &EventDiscriminator::TagEquals { 1237 name: "status", 1238 value: "accepted", 1239 }, 1240 &tags, 1241 "{}" 1242 )); 1243 assert!(!discriminator_matches( 1244 &EventDiscriminator::TagEquals { 1245 name: "status", 1246 value: "declined", 1247 }, 1248 &tags, 1249 "{}" 1250 )); 1251 } 1252 1253 #[test] 1254 fn validates_custom_knowledge_contract_shape() { 1255 let event = unsigned_event( 1256 KIND_KNOWLEDGE_CLAIM, 1257 vec![vec!["contract", "radroots.knowledge.claim.v1"]], 1258 r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1,"text":"soil improves with cover crops"}"#, 1259 ); 1260 1261 assert_eq!( 1262 validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"), 1263 Ok(()) 1264 ); 1265 assert_eq!( 1266 validate_event_contract(&event).expect("validated").id, 1267 "radroots.knowledge.claim.v1" 1268 ); 1269 } 1270 1271 #[test] 1272 fn rejects_custom_knowledge_contract_tag_mismatch() { 1273 let event = unsigned_event( 1274 KIND_KNOWLEDGE_CLAIM, 1275 vec![vec!["contract", "radroots.knowledge.relation.v1"]], 1276 r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#, 1277 ); 1278 1279 assert_eq!( 1280 validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"), 1281 Err(ContractValidationError::TagValueMismatch { 1282 contract_id: "radroots.knowledge.claim.v1", 1283 name: "contract", 1284 expected: "radroots.knowledge.claim.v1".to_owned(), 1285 actual: Some("radroots.knowledge.relation.v1".to_owned()), 1286 }) 1287 ); 1288 } 1289 1290 #[test] 1291 fn rejects_custom_knowledge_schema_mismatch() { 1292 let event = unsigned_event( 1293 KIND_KNOWLEDGE_CLAIM, 1294 vec![vec!["contract", "radroots.knowledge.claim.v1"]], 1295 r#"{"schema":"radroots.knowledge.relation.v1","schema_version":1}"#, 1296 ); 1297 1298 assert_eq!( 1299 validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"), 1300 Err(ContractValidationError::ContentFieldMismatch { 1301 contract_id: "radroots.knowledge.claim.v1", 1302 field: "schema", 1303 expected: "radroots.knowledge.claim.v1".to_owned(), 1304 }) 1305 ); 1306 } 1307 1308 #[test] 1309 fn rejects_custom_knowledge_missing_schema_version() { 1310 let event = unsigned_event( 1311 KIND_KNOWLEDGE_CLAIM, 1312 vec![vec!["contract", "radroots.knowledge.claim.v1"]], 1313 r#"{"schema":"radroots.knowledge.claim.v1"}"#, 1314 ); 1315 1316 assert_eq!( 1317 validate_event_contract_shape(&event, "radroots.knowledge.claim.v1"), 1318 Err(ContractValidationError::MissingContentField { 1319 contract_id: "radroots.knowledge.claim.v1", 1320 field: "schema_version", 1321 }) 1322 ); 1323 } 1324 1325 #[test] 1326 fn rejects_authoritative_knowledge_status_fields() { 1327 let event = unsigned_event_owned( 1328 KIND_KNOWLEDGE_REVIEW, 1329 vec![ 1330 vec![ 1331 "contract".to_owned(), 1332 "radroots.knowledge.review.v1".to_owned(), 1333 ], 1334 vec![ 1335 "review_target".to_owned(), 1336 "0".repeat(64), 1337 crate::test_valid_hex_64('1'), 1338 "30818".to_owned(), 1339 "soil".to_owned(), 1340 ], 1341 ], 1342 r#"{"schema":"radroots.knowledge.review.v1","schema_version":1,"canon_status":"approved"}"#, 1343 ); 1344 1345 assert_eq!( 1346 validate_event_contract_shape(&event, "radroots.knowledge.review.v1"), 1347 Err(ContractValidationError::ForbiddenContentField { 1348 contract_id: "radroots.knowledge.review.v1", 1349 field: "canon_status", 1350 }) 1351 ); 1352 } 1353 1354 #[test] 1355 fn validate_event_contract_shape_reports_registry_kind_and_content_errors() { 1356 let event = unsigned_event(KIND_POST, Vec::new(), "hello"); 1357 assert_eq!( 1358 validate_event_contract_shape(&event, "missing.contract.v1"), 1359 Err(ContractValidationError::UnknownContract { 1360 contract_id: "missing.contract.v1".to_owned(), 1361 }) 1362 ); 1363 assert_eq!( 1364 validate_event_contract_shape(&event, "radroots.profile.metadata.v1"), 1365 Err(ContractValidationError::KindMismatch { 1366 expected: KIND_PROFILE, 1367 actual: KIND_POST, 1368 }) 1369 ); 1370 1371 let invalid_json = unsigned_event( 1372 KIND_KNOWLEDGE_CLAIM, 1373 vec![vec!["contract", "radroots.knowledge.claim.v1"]], 1374 "not-json", 1375 ); 1376 assert_eq!( 1377 validate_event_contract_shape(&invalid_json, "radroots.knowledge.claim.v1"), 1378 Err(ContractValidationError::InvalidJsonContent { 1379 contract_id: "radroots.knowledge.claim.v1", 1380 }) 1381 ); 1382 1383 assert_eq!( 1384 validate_event_contract_shape( 1385 &unsigned_event(KIND_POST, Vec::new(), "plain text"), 1386 "radroots.social.post.v1", 1387 ), 1388 Ok(()) 1389 ); 1390 } 1391 1392 #[test] 1393 fn validate_contract_tags_reports_cardinality_errors() { 1394 let missing_required_one = unsigned_event( 1395 KIND_KNOWLEDGE_CLAIM, 1396 Vec::new(), 1397 r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#, 1398 ); 1399 assert_eq!( 1400 validate_event_contract_shape(&missing_required_one, "radroots.knowledge.claim.v1"), 1401 Err(ContractValidationError::MissingTag { 1402 contract_id: "radroots.knowledge.claim.v1", 1403 name: "contract", 1404 }) 1405 ); 1406 1407 let duplicate_required_one = unsigned_event( 1408 KIND_KNOWLEDGE_CLAIM, 1409 vec![ 1410 vec!["contract", "radroots.knowledge.claim.v1"], 1411 vec!["contract", "radroots.knowledge.claim.v1"], 1412 ], 1413 r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#, 1414 ); 1415 assert_eq!( 1416 validate_event_contract_shape(&duplicate_required_one, "radroots.knowledge.claim.v1"), 1417 Err(ContractValidationError::TagCardinalityMismatch { 1418 contract_id: "radroots.knowledge.claim.v1", 1419 name: "contract", 1420 }) 1421 ); 1422 1423 let required_many = 1424 synthetic_event_contract("radroots.test.required_many.v1", REQUIRED_MANY_TEST_TAGS); 1425 assert_eq!( 1426 validate_contract_tags_parts(&[], &required_many), 1427 Err(ContractValidationError::MissingTag { 1428 contract_id: "radroots.test.required_many.v1", 1429 name: "test_many", 1430 }) 1431 ); 1432 assert_eq!( 1433 validate_contract_tags_parts( 1434 &[vec!["test_many".to_owned(), "one".to_owned()]], 1435 &required_many 1436 ), 1437 Ok(()) 1438 ); 1439 1440 let optional_one = 1441 synthetic_event_contract("radroots.test.optional_one.v1", OPTIONAL_ONE_TEST_TAGS); 1442 assert_eq!(validate_contract_tags_parts(&[], &optional_one), Ok(())); 1443 assert_eq!( 1444 validate_contract_tags_parts( 1445 &[ 1446 vec!["test_optional".to_owned(), "one".to_owned()], 1447 vec!["test_optional".to_owned(), "two".to_owned()], 1448 ], 1449 &optional_one, 1450 ), 1451 Err(ContractValidationError::TagCardinalityMismatch { 1452 contract_id: "radroots.test.optional_one.v1", 1453 name: "test_optional", 1454 }) 1455 ); 1456 1457 let duplicate_required = synthetic_event_contract( 1458 "radroots.test.duplicate_required.v1", 1459 DUPLICATE_REQUIRED_TEST_TAGS, 1460 ); 1461 assert_eq!( 1462 validate_contract_tags_parts( 1463 &[ 1464 vec!["test_required".to_owned(), "one".to_owned()], 1465 vec!["test_required".to_owned(), "two".to_owned()], 1466 ], 1467 &duplicate_required, 1468 ), 1469 Ok(()) 1470 ); 1471 1472 let duplicate_optional = synthetic_event_contract( 1473 "radroots.test.duplicate_optional.v1", 1474 DUPLICATE_OPTIONAL_TEST_TAGS, 1475 ); 1476 assert_eq!( 1477 validate_contract_tags_parts( 1478 &[ 1479 vec!["test_optional".to_owned(), "one".to_owned()], 1480 vec!["test_optional".to_owned(), "two".to_owned()], 1481 ], 1482 &duplicate_optional, 1483 ), 1484 Ok(()) 1485 ); 1486 } 1487 1488 #[test] 1489 fn validate_contract_tags_enforces_declared_value_types() { 1490 let claim_content = r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#; 1491 let invalid_source = unsigned_event_owned( 1492 KIND_KNOWLEDGE_CLAIM, 1493 vec![ 1494 vec![ 1495 "contract".to_owned(), 1496 "radroots.knowledge.claim.v1".to_owned(), 1497 ], 1498 vec!["source".to_owned(), "not-an-event-id".to_owned()], 1499 ], 1500 claim_content, 1501 ); 1502 assert_eq!( 1503 validate_event_contract_shape(&invalid_source, "radroots.knowledge.claim.v1"), 1504 Err(ContractValidationError::TagValueMismatch { 1505 contract_id: "radroots.knowledge.claim.v1", 1506 name: "source", 1507 expected: "event_pointer".to_owned(), 1508 actual: Some("not-an-event-id".to_owned()), 1509 }) 1510 ); 1511 1512 let invalid_citation = unsigned_event( 1513 KIND_KNOWLEDGE_CLAIM, 1514 vec![ 1515 vec!["contract", "radroots.knowledge.claim.v1"], 1516 vec!["citation", "not-hex"], 1517 ], 1518 claim_content, 1519 ); 1520 assert_eq!( 1521 validate_event_contract_shape(&invalid_citation, "radroots.knowledge.claim.v1"), 1522 Err(ContractValidationError::TagValueMismatch { 1523 contract_id: "radroots.knowledge.claim.v1", 1524 name: "citation", 1525 expected: "sha256".to_owned(), 1526 actual: Some("not-hex".to_owned()), 1527 }) 1528 ); 1529 1530 let invalid_review = unsigned_event( 1531 KIND_KNOWLEDGE_REVIEW, 1532 vec![ 1533 vec!["contract", "radroots.knowledge.review.v1"], 1534 vec!["review_target", "not-an-event-id"], 1535 ], 1536 r#"{"schema":"radroots.knowledge.review.v1","schema_version":1}"#, 1537 ); 1538 assert_eq!( 1539 validate_event_contract_shape(&invalid_review, "radroots.knowledge.review.v1"), 1540 Err(ContractValidationError::TagValueMismatch { 1541 contract_id: "radroots.knowledge.review.v1", 1542 name: "review_target", 1543 expected: "event_pointer".to_owned(), 1544 actual: Some("not-an-event-id".to_owned()), 1545 }) 1546 ); 1547 1548 let invalid_geohash = unsigned_event( 1549 KIND_KNOWLEDGE_FIELD_REPORT, 1550 vec![ 1551 vec!["contract", "radroots.knowledge.field_report.v1"], 1552 vec!["g", "invalid-a"], 1553 ], 1554 r#"{"schema":"radroots.knowledge.field_report.v1","schema_version":1}"#, 1555 ); 1556 assert_eq!( 1557 validate_event_contract_shape(&invalid_geohash, "radroots.knowledge.field_report.v1"), 1558 Err(ContractValidationError::TagValueMismatch { 1559 contract_id: "radroots.knowledge.field_report.v1", 1560 name: "g", 1561 expected: "geohash".to_owned(), 1562 actual: Some("invalid-a".to_owned()), 1563 }) 1564 ); 1565 1566 let invalid_address = unsigned_event( 1567 KIND_WIKI_REDIRECT, 1568 vec![vec!["d", "soil"], vec!["a", "30818:not-hex:soil"]], 1569 "", 1570 ); 1571 assert_eq!( 1572 validate_event_contract_shape(&invalid_address, "radroots.wiki.redirect.v1"), 1573 Err(ContractValidationError::TagValueMismatch { 1574 contract_id: "radroots.wiki.redirect.v1", 1575 name: "a", 1576 expected: "addressable_coordinate".to_owned(), 1577 actual: Some("30818:not-hex:soil".to_owned()), 1578 }) 1579 ); 1580 1581 let invalid_event_id = unsigned_event_owned( 1582 KIND_WIKI_MERGE_REQUEST, 1583 vec![ 1584 vec![ 1585 "a".to_owned(), 1586 format!("30818:{}:soil", crate::test_valid_hex_64('0')), 1587 ], 1588 vec!["p".to_owned(), crate::test_valid_hex_64('1')], 1589 vec!["e".to_owned(), "not-hex".to_owned()], 1590 ], 1591 "", 1592 ); 1593 assert_eq!( 1594 validate_event_contract_shape(&invalid_event_id, "radroots.wiki.merge_request.v1"), 1595 Err(ContractValidationError::TagValueMismatch { 1596 contract_id: "radroots.wiki.merge_request.v1", 1597 name: "e", 1598 expected: "event_id".to_owned(), 1599 actual: Some("not-hex".to_owned()), 1600 }) 1601 ); 1602 1603 let valid_source = unsigned_event_owned( 1604 KIND_KNOWLEDGE_CLAIM, 1605 vec![ 1606 vec![ 1607 "contract".to_owned(), 1608 "radroots.knowledge.claim.v1".to_owned(), 1609 ], 1610 event_ref_tag( 1611 "source", 1612 hex_64('a').as_str(), 1613 hex_64('b').as_str(), 1614 KIND_KNOWLEDGE_SOURCE, 1615 ), 1616 vec!["citation".to_owned(), hex_64('c')], 1617 ], 1618 claim_content, 1619 ); 1620 assert_eq!( 1621 validate_event_contract_shape(&valid_source, "radroots.knowledge.claim.v1"), 1622 Ok(()) 1623 ); 1624 } 1625 1626 #[test] 1627 fn tag_value_shape_helpers_cover_contract_registry_value_types() { 1628 let event_id = hex_64('a'); 1629 let public_key = hex_64('b'); 1630 let coordinate = format!("{KIND_WIKI_ARTICLE}:{public_key}:soil"); 1631 let replaceable_coordinate = format!("0:{public_key}:"); 1632 let valid_pointer = vec![ 1633 "source".to_owned(), 1634 event_id.clone(), 1635 public_key.clone(), 1636 KIND_KNOWLEDGE_SOURCE.to_string(), 1637 "soil".to_owned(), 1638 "ws://relay.example.com".to_owned(), 1639 "wss://relay.example.net".to_owned(), 1640 ]; 1641 let empty_d_pointer = vec![ 1642 "source".to_owned(), 1643 event_id.clone(), 1644 public_key.clone(), 1645 KIND_KNOWLEDGE_SOURCE.to_string(), 1646 String::new(), 1647 ]; 1648 1649 assert!(!tag_value_is_valid( 1650 &owned_tag(&["source"]), 1651 TagValueType::EventPointer 1652 )); 1653 assert!(tag_value_is_valid( 1654 &owned_tag(&["a", coordinate.as_str()]), 1655 TagValueType::AddressableCoordinate 1656 )); 1657 assert!(!tag_value_is_valid( 1658 &owned_tag(&["a", "30818:not-hex:soil"]), 1659 TagValueType::AddressableCoordinate 1660 )); 1661 assert!(tag_value_is_valid( 1662 &owned_tag(&["a", replaceable_coordinate.as_str()]), 1663 TagValueType::Nip01Coordinate 1664 )); 1665 assert!(!tag_value_is_valid( 1666 &owned_tag(&["a", format!("0:{public_key}:profile").as_str()]), 1667 TagValueType::Nip01Coordinate 1668 )); 1669 assert!(tag_value_is_valid( 1670 &owned_tag(&["contract", "radroots.knowledge.claim.v1"]), 1671 TagValueType::ContractId 1672 )); 1673 assert!(!tag_value_is_valid( 1674 &owned_tag(&["contract", "radroots.unknown.v1"]), 1675 TagValueType::ContractId 1676 )); 1677 assert!(tag_value_is_valid( 1678 &owned_tag(&["d", "soil"]), 1679 TagValueType::DTag 1680 )); 1681 assert!(!tag_value_is_valid( 1682 &owned_tag(&["d", ""]), 1683 TagValueType::DTag 1684 )); 1685 assert!(tag_value_is_valid( 1686 &owned_tag(&["e", event_id.as_str()]), 1687 TagValueType::EventId 1688 )); 1689 assert!(tag_value_is_valid( 1690 &owned_tag(&["citation", event_id.as_str()]), 1691 TagValueType::Sha256 1692 )); 1693 assert!(!tag_value_is_valid( 1694 &owned_tag(&["e", "not-hex"]), 1695 TagValueType::EventId 1696 )); 1697 assert!(tag_value_is_valid( 1698 &valid_pointer, 1699 TagValueType::EventPointer 1700 )); 1701 assert!(tag_value_is_valid( 1702 &empty_d_pointer, 1703 TagValueType::EventPointer 1704 )); 1705 assert!(!event_pointer_tag_is_valid(&owned_tag(&[ 1706 "source", 1707 "not-hex", 1708 public_key.as_str(), 1709 "1", 1710 "" 1711 ]))); 1712 assert!(!event_pointer_tag_is_valid(&owned_tag(&[ 1713 "source", 1714 event_id.as_str(), 1715 "not-hex", 1716 "1", 1717 "" 1718 ]))); 1719 assert!(!event_pointer_tag_is_valid(&owned_tag(&[ 1720 "source", 1721 event_id.as_str(), 1722 public_key.as_str(), 1723 "not-a-kind", 1724 "" 1725 ]))); 1726 assert!(!event_pointer_tag_is_valid(&owned_tag(&[ 1727 "source", 1728 event_id.as_str(), 1729 public_key.as_str(), 1730 "1" 1731 ]))); 1732 assert!(!event_pointer_tag_is_valid(&owned_tag(&[ 1733 "source", 1734 event_id.as_str(), 1735 public_key.as_str(), 1736 "1", 1737 "bad tag" 1738 ]))); 1739 assert!(!event_pointer_tag_is_valid(&owned_tag(&[ 1740 "source", 1741 event_id.as_str(), 1742 public_key.as_str(), 1743 "1", 1744 "", 1745 "https://relay.example.com" 1746 ]))); 1747 assert!(tag_value_is_valid( 1748 &owned_tag(&["g", "9q8yy"]), 1749 TagValueType::Geohash 1750 )); 1751 assert!(tag_value_is_valid( 1752 &owned_tag(&["g", "9Q8YY"]), 1753 TagValueType::Geohash 1754 )); 1755 assert!(!tag_value_is_valid( 1756 &owned_tag(&["g", ""]), 1757 TagValueType::Geohash 1758 )); 1759 assert!(!tag_value_is_valid( 1760 &owned_tag(&["g", "1234567890123"]), 1761 TagValueType::Geohash 1762 )); 1763 assert!(!tag_value_is_valid( 1764 &owned_tag(&["g", "aaaaa"]), 1765 TagValueType::Geohash 1766 )); 1767 assert!(tag_value_is_valid( 1768 &owned_tag(&["k", "30818"]), 1769 TagValueType::Kind 1770 )); 1771 assert!(!tag_value_is_valid( 1772 &owned_tag(&["k", "not-a-kind"]), 1773 TagValueType::Kind 1774 )); 1775 assert!(tag_value_is_valid( 1776 &owned_tag(&["p", public_key.as_str()]), 1777 TagValueType::PublicKey 1778 )); 1779 assert!(!tag_value_is_valid( 1780 &owned_tag(&["p", "not-hex"]), 1781 TagValueType::PublicKey 1782 )); 1783 assert!(tag_value_is_valid( 1784 &owned_tag(&["relay", "ws://relay.example.com"]), 1785 TagValueType::RelayUrl 1786 )); 1787 assert!(tag_value_is_valid( 1788 &owned_tag(&["relay", "wss://relay.example.com"]), 1789 TagValueType::RelayUrl 1790 )); 1791 assert!(!tag_value_is_valid( 1792 &owned_tag(&["relay", "http://relay.example.com"]), 1793 TagValueType::RelayUrl 1794 )); 1795 assert!(relay_url_is_valid("ws://relay.example.com")); 1796 assert!(relay_url_is_valid("wss://relay.example.com")); 1797 assert!(!relay_url_is_valid("ws://")); 1798 assert!(!relay_url_is_valid("http://relay.example.com")); 1799 assert!(!relay_url_is_valid(" wss://relay.example.com")); 1800 assert!(!relay_url_is_valid("wss://relay.example.com ")); 1801 assert!(!relay_url_is_valid("wss://relay.example.com\nmiddle")); 1802 assert!(tag_value_is_valid( 1803 &owned_tag(&["title", "Soil Guide"]), 1804 TagValueType::Text 1805 )); 1806 assert!(!tag_value_is_valid( 1807 &owned_tag(&["title", " "]), 1808 TagValueType::Text 1809 )); 1810 assert!(!tag_value_is_valid( 1811 &owned_tag(&["title", "Soil\nGuide"]), 1812 TagValueType::Text 1813 )); 1814 assert!(tag_value_is_valid( 1815 &owned_tag(&["expiration", "1700000000"]), 1816 TagValueType::UnixTimestamp 1817 )); 1818 assert!(!tag_value_is_valid( 1819 &owned_tag(&["expiration", "not-time"]), 1820 TagValueType::UnixTimestamp 1821 )); 1822 assert!(tag_value_is_valid( 1823 &owned_tag(&["image", "https://example.com"]), 1824 TagValueType::Url 1825 )); 1826 assert!(!tag_value_is_valid( 1827 &owned_tag(&["image", "wss://example.com"]), 1828 TagValueType::Url 1829 )); 1830 assert!(url_is_valid("http://example.com")); 1831 assert!(url_is_valid("https://example.com")); 1832 assert!(!url_is_valid("http://")); 1833 assert!(!url_is_valid("wss://example.com")); 1834 assert!(!url_is_valid(" https://example.com")); 1835 assert!(!url_is_valid("https://example.com ")); 1836 assert!(!url_is_valid("https://example.com\nmiddle")); 1837 assert!(tag_value_is_valid( 1838 &owned_tag(&["uuid", "123e4567-e89b-12d3-a456-426614174000"]), 1839 TagValueType::Uuid 1840 )); 1841 assert!(!tag_value_is_valid( 1842 &owned_tag(&["uuid", "123e4567-e89b-12d3-a456-42661417400"]), 1843 TagValueType::Uuid 1844 )); 1845 assert!(uuid_is_valid("123e4567-e89b-12d3-a456-426614174000")); 1846 assert!(!uuid_is_valid("123e4567-e89b-12d3-a456-42661417400")); 1847 assert!(!uuid_is_valid("123e4567xe89b-12d3-a456-426614174000")); 1848 assert!(!uuid_is_valid("123e4567-e89b-12d3-a456-42661417400x")); 1849 1850 let expectations = [ 1851 ( 1852 TagValueType::AddressableCoordinate, 1853 "addressable_coordinate", 1854 ), 1855 (TagValueType::ContractId, "contract_id"), 1856 (TagValueType::DTag, "d_tag"), 1857 (TagValueType::EventId, "event_id"), 1858 (TagValueType::EventPointer, "event_pointer"), 1859 (TagValueType::Geohash, "geohash"), 1860 (TagValueType::Kind, "kind"), 1861 (TagValueType::Nip01Coordinate, "nip01_coordinate"), 1862 (TagValueType::PublicKey, "public_key"), 1863 (TagValueType::RelayUrl, "relay_url"), 1864 (TagValueType::Sha256, "sha256"), 1865 (TagValueType::Text, "text"), 1866 (TagValueType::UnixTimestamp, "unix_timestamp"), 1867 (TagValueType::Url, "url"), 1868 (TagValueType::Uuid, "uuid"), 1869 ]; 1870 for (value_type, expected) in expectations { 1871 assert_eq!(tag_value_type_expectation(value_type), expected); 1872 } 1873 } 1874 1875 #[test] 1876 fn validate_custom_knowledge_contract_rejects_missing_schema_and_bad_version() { 1877 let missing_schema = unsigned_event( 1878 KIND_KNOWLEDGE_CLAIM, 1879 vec![vec!["contract", "radroots.knowledge.claim.v1"]], 1880 r#"{"schema_version":1}"#, 1881 ); 1882 assert_eq!( 1883 validate_event_contract_shape(&missing_schema, "radroots.knowledge.claim.v1"), 1884 Err(ContractValidationError::MissingContentField { 1885 contract_id: "radroots.knowledge.claim.v1", 1886 field: "schema", 1887 }) 1888 ); 1889 1890 let bad_version = unsigned_event( 1891 KIND_KNOWLEDGE_CLAIM, 1892 vec![vec!["contract", "radroots.knowledge.claim.v1"]], 1893 r#"{"schema":"radroots.knowledge.claim.v1","schema_version":2}"#, 1894 ); 1895 assert_eq!( 1896 validate_event_contract_shape(&bad_version, "radroots.knowledge.claim.v1"), 1897 Err(ContractValidationError::ContentFieldMismatch { 1898 contract_id: "radroots.knowledge.claim.v1", 1899 field: "schema_version", 1900 expected: "1".to_owned(), 1901 }) 1902 ); 1903 } 1904 1905 #[test] 1906 fn validates_nip54_empty_redirect_content() { 1907 let event = unsigned_event_owned( 1908 KIND_WIKI_REDIRECT, 1909 vec![ 1910 vec!["d".to_owned(), "soil".to_owned()], 1911 vec![ 1912 "a".to_owned(), 1913 format!("30818:{}:soil", crate::test_valid_hex_64('0')), 1914 ], 1915 ], 1916 "", 1917 ); 1918 1919 assert_eq!( 1920 validate_event_contract_shape(&event, "radroots.wiki.redirect.v1"), 1921 Ok(()) 1922 ); 1923 1924 let invalid = unsigned_event_owned( 1925 KIND_WIKI_REDIRECT, 1926 vec![ 1927 vec!["d".to_owned(), "soil".to_owned()], 1928 vec![ 1929 "a".to_owned(), 1930 format!("30818:{}:soil", crate::test_valid_hex_64('0')), 1931 ], 1932 ], 1933 "{}", 1934 ); 1935 assert_eq!( 1936 validate_event_contract_shape(&invalid, "radroots.wiki.redirect.v1"), 1937 Err(ContractValidationError::ContentMustBeEmpty { 1938 contract_id: "radroots.wiki.redirect.v1", 1939 }) 1940 ); 1941 } 1942 1943 #[test] 1944 fn exposes_validation_error_codes() { 1945 for (error, code) in [ 1946 ( 1947 ContractValidationError::UnknownContract { 1948 contract_id: "missing".to_owned(), 1949 }, 1950 "unknown_contract", 1951 ), 1952 ( 1953 ContractValidationError::AdmissionRequired { 1954 contract_id: "radroots.social.ask.v1", 1955 }, 1956 "admission_required", 1957 ), 1958 ( 1959 ContractValidationError::ContractMatch { 1960 error: ContractMatchError::UnsupportedKind(999_999), 1961 }, 1962 "contract_match", 1963 ), 1964 ( 1965 ContractValidationError::KindMismatch { 1966 expected: KIND_PROFILE, 1967 actual: KIND_POST, 1968 }, 1969 "kind_mismatch", 1970 ), 1971 ( 1972 ContractValidationError::ContentMustBeEmpty { 1973 contract_id: "radroots.wiki.redirect.v1", 1974 }, 1975 "content_must_be_empty", 1976 ), 1977 ( 1978 ContractValidationError::InvalidJsonContent { 1979 contract_id: "radroots.knowledge.claim.v1", 1980 }, 1981 "invalid_json_content", 1982 ), 1983 ( 1984 ContractValidationError::MissingTag { 1985 contract_id: "radroots.knowledge.claim.v1", 1986 name: "contract", 1987 }, 1988 "missing_tag", 1989 ), 1990 ( 1991 ContractValidationError::TagCardinalityMismatch { 1992 contract_id: "radroots.knowledge.claim.v1", 1993 name: "contract", 1994 }, 1995 "tag_cardinality_mismatch", 1996 ), 1997 ( 1998 ContractValidationError::TagValueMismatch { 1999 contract_id: "radroots.knowledge.claim.v1", 2000 name: "contract", 2001 expected: "radroots.knowledge.claim.v1".to_owned(), 2002 actual: None, 2003 }, 2004 "tag_value_mismatch", 2005 ), 2006 ( 2007 ContractValidationError::MissingContentField { 2008 contract_id: "radroots.knowledge.claim.v1", 2009 field: "schema", 2010 }, 2011 "missing_content_field", 2012 ), 2013 ( 2014 ContractValidationError::ContentFieldMismatch { 2015 contract_id: "radroots.knowledge.claim.v1", 2016 field: "schema", 2017 expected: "radroots.knowledge.claim.v1".to_owned(), 2018 }, 2019 "content_field_mismatch", 2020 ), 2021 ( 2022 ContractValidationError::ForbiddenContentField { 2023 contract_id: "radroots.knowledge.claim.v1", 2024 field: "review_status", 2025 }, 2026 "forbidden_content_field", 2027 ), 2028 ] { 2029 assert_eq!(error.code(), code); 2030 } 2031 } 2032 2033 #[test] 2034 fn tag_helpers_cover_missing_names_and_cardinality_mismatches() { 2035 let tags = vec![ 2036 vec!["p".to_owned(), "counterparty".to_owned()], 2037 vec!["d".to_owned()], 2038 ]; 2039 2040 assert_eq!(tag_value(&tags, "d"), None); 2041 assert_eq!(tag_value(&tags, "p"), Some("counterparty")); 2042 2043 let malformed = [ 2044 tag( 2045 "d", 2046 TagCardinality::OptionalOne, 2047 TagSemantic::Identifier, 2048 TagValueType::DTag, 2049 true, 2050 ), 2051 tag( 2052 "p", 2053 TagCardinality::RequiredOne, 2054 TagSemantic::Counterparty, 2055 TagValueType::PublicKey, 2056 true, 2057 ), 2058 ]; 2059 2060 assert!( 2061 !malformed 2062 .iter() 2063 .any(|tag| tag.name == "d" && tag.cardinality == TagCardinality::RequiredOne) 2064 ); 2065 } 2066 2067 #[test] 2068 fn relay_indexed_tags_are_single_letter() { 2069 for contract in all_event_contracts() { 2070 for tag in contract.tags { 2071 if tag.relay_indexed { 2072 assert_eq!(tag.name.len(), 1, "{}:{}", contract.id, tag.name); 2073 } 2074 } 2075 } 2076 } 2077 2078 #[test] 2079 fn addressable_event_contracts_require_d_tags() { 2080 for contract in all_event_contracts() { 2081 if contract.class == EventClass::Addressable { 2082 let d_tag_cardinality = contract 2083 .tags 2084 .iter() 2085 .find(|tag| tag.name == "d") 2086 .map(|tag| tag.cardinality); 2087 assert_eq!( 2088 d_tag_cardinality, 2089 Some(TagCardinality::RequiredOne), 2090 "{}", 2091 contract.id 2092 ); 2093 } 2094 } 2095 }