knowledge_fixtures.rs (27642B)
1 #![cfg(feature = "knowledge")] 2 3 mod support; 4 5 use std::collections::BTreeSet; 6 7 use radroots_event::contract::{ 8 ContractValidationError, EventClass, all_event_contracts, validate_event_contract_shape, 9 }; 10 use radroots_event::envelope::kind::{ 11 KIND_CONTRIBUTION_ATTESTATION, KIND_KNOWLEDGE_CHANGE_PROPOSAL, KIND_KNOWLEDGE_CLAIM, 12 KIND_KNOWLEDGE_FIELD_REPORT, KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, KIND_WIKI_ARTICLE, 13 }; 14 use radroots_event::knowledge::{ 15 RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, WikiArticle, 16 }; 17 use radroots_event::wire::Nip01EventWireParts; 18 use radroots_event::{envelope::EventEnvelope, envelope::EventEnvelopeParts}; 19 use radroots_event_codec::decode::EventParseError; 20 use radroots_event_codec::decode::knowledge::{ 21 wiki_article_from_event, wiki_merge_request_from_event, 22 }; 23 use radroots_event_codec::encode::EventEncodeError; 24 use radroots_event_codec::encode::knowledge::{ 25 contribution_attestation_to_wire_parts, evidence_bounty_to_wire_parts, 26 knowledge_change_proposal_to_wire_parts, knowledge_claim_to_wire_parts, 27 knowledge_field_report_to_wire_parts, knowledge_relation_to_wire_parts, 28 knowledge_review_to_wire_parts, knowledge_source_to_wire_parts, wiki_article_to_wire_parts, 29 wiki_merge_request_to_wire_parts, wiki_redirect_to_wire_parts, 30 }; 31 #[cfg(feature = "manifests")] 32 use radroots_event_codec::manifest::{contract_manifest_json, knowledge_contract_manifest}; 33 use radroots_event_codec::verify::{ 34 RadrootsDecodeError, RadrootsDecodedEvent, RadrootsNip01VerificationError, 35 verify_and_decode_radroots_event, 36 }; 37 use support::RELAY_PRIMARY_WSS; 38 use support::knowledge::{ 39 RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES, RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS, 40 RadrootsKnowledgeFixture, RadrootsKnowledgeFixtureCase, hex_64, knowledge_claim, 41 knowledge_field_report, knowledge_valid_fixtures, wiki_article, wiki_merge_request, 42 wiki_redirect, 43 }; 44 45 fn event_from_parts(parts: Nip01EventWireParts) -> EventEnvelope { 46 EventEnvelope::new(EventEnvelopeParts { 47 id: hex_64('0'), 48 author: hex_64('a'), 49 created_at: 1_800_000_000, 50 kind: parts.kind, 51 tags: parts.tags, 52 content: parts.content, 53 sig: "1".repeat(128), 54 }) 55 .unwrap() 56 } 57 58 fn sign_parts(parts: Nip01EventWireParts) -> EventEnvelope { 59 let tags = parts 60 .tags 61 .into_iter() 62 .map(nostr::Tag::parse) 63 .collect::<Result<Vec<_>, _>>() 64 .expect("tags"); 65 let keys = 66 nostr::Keys::parse("0101010101010101010101010101010101010101010101010101010101010101") 67 .expect("keys"); 68 let kind = u16::try_from(parts.kind).expect("knowledge event kind must fit NIP-01"); 69 let event = nostr::EventBuilder::new(nostr::Kind::Custom(kind), parts.content) 70 .tags(tags) 71 .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) 72 .sign_with_keys(&keys) 73 .expect("signed event"); 74 EventEnvelope::new(EventEnvelopeParts { 75 id: event.id.to_hex(), 76 author: event.pubkey.to_hex(), 77 created_at: event.created_at.as_secs(), 78 kind: u32::from(event.kind.as_u16()), 79 tags: event 80 .tags 81 .as_slice() 82 .iter() 83 .map(|tag| tag.as_slice().to_vec()) 84 .collect(), 85 content: event.content, 86 sig: event.sig.to_string(), 87 }) 88 .unwrap() 89 } 90 91 fn event_with_parts( 92 event: &EventEnvelope, 93 tags: Vec<Vec<String>>, 94 content: String, 95 sig: String, 96 ) -> EventEnvelope { 97 EventEnvelope::new(EventEnvelopeParts { 98 id: event.id_hex().to_string(), 99 author: event.author().to_hex().to_string(), 100 created_at: event.created_at_u64(), 101 kind: event.kind_u32(), 102 tags, 103 content, 104 sig, 105 }) 106 .unwrap() 107 } 108 109 fn mutate_tags(event: &mut EventEnvelope, update: impl FnOnce(&mut Vec<Vec<String>>)) { 110 let mut tags = event.tags_as_vec(); 111 update(&mut tags); 112 *event = event_with_parts( 113 event, 114 tags, 115 event.content().to_string(), 116 event.signature_hex().to_string(), 117 ); 118 } 119 120 fn replace_content(event: &mut EventEnvelope, content: String) { 121 *event = event_with_parts( 122 event, 123 event.tags_as_vec(), 124 content, 125 event.signature_hex().to_string(), 126 ); 127 } 128 129 fn replace_sig(event: &mut EventEnvelope, sig: String) { 130 *event = event_with_parts(event, event.tags_as_vec(), event.content().to_string(), sig); 131 } 132 133 fn parts_for_fixture(fixture: &RadrootsKnowledgeFixture) -> Nip01EventWireParts { 134 match fixture { 135 RadrootsKnowledgeFixture::WikiArticle(value) => wiki_article_to_wire_parts(value).unwrap(), 136 RadrootsKnowledgeFixture::WikiRedirect(value) => { 137 wiki_redirect_to_wire_parts(value).unwrap() 138 } 139 RadrootsKnowledgeFixture::WikiMergeRequest(value) => { 140 wiki_merge_request_to_wire_parts(value).unwrap() 141 } 142 RadrootsKnowledgeFixture::KnowledgeSource(value) => { 143 knowledge_source_to_wire_parts(value).unwrap() 144 } 145 RadrootsKnowledgeFixture::KnowledgeClaim(value) => { 146 knowledge_claim_to_wire_parts(value).unwrap() 147 } 148 RadrootsKnowledgeFixture::KnowledgeRelation(value) => { 149 knowledge_relation_to_wire_parts(value).unwrap() 150 } 151 RadrootsKnowledgeFixture::KnowledgeReview(value) => { 152 knowledge_review_to_wire_parts(value).unwrap() 153 } 154 RadrootsKnowledgeFixture::KnowledgeFieldReport(value) => { 155 knowledge_field_report_to_wire_parts(value).unwrap() 156 } 157 RadrootsKnowledgeFixture::EvidenceBounty(value) => { 158 evidence_bounty_to_wire_parts(value).unwrap() 159 } 160 RadrootsKnowledgeFixture::KnowledgeChangeProposal(value) => { 161 knowledge_change_proposal_to_wire_parts(value).unwrap() 162 } 163 RadrootsKnowledgeFixture::ContributionAttestation(value) => { 164 contribution_attestation_to_wire_parts(value).unwrap() 165 } 166 } 167 } 168 169 fn fixture_by_id<'a>( 170 fixtures: &'a [RadrootsKnowledgeFixtureCase], 171 id: &str, 172 ) -> &'a RadrootsKnowledgeFixtureCase { 173 fixtures 174 .iter() 175 .find(|fixture| fixture.id == id) 176 .unwrap_or_else(|| panic!("missing fixture {id}")) 177 } 178 179 fn has_exact_tag(tags: &[Vec<String>], expected: &[&str]) -> bool { 180 tags.iter().any(|tag| { 181 tag.iter() 182 .map(|entry| entry.as_str()) 183 .eq(expected.iter().copied()) 184 }) 185 } 186 187 #[test] 188 fn golden_knowledge_fixtures_cover_every_contract() { 189 let fixtures = knowledge_valid_fixtures(); 190 let fixture_contracts = fixtures 191 .iter() 192 .map(|fixture| fixture.contract_id) 193 .collect::<BTreeSet<_>>(); 194 let registry_contracts = all_event_contracts() 195 .iter() 196 .filter(|contract| RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS.contains(&contract.id)) 197 .map(|contract| contract.id) 198 .collect::<BTreeSet<_>>(); 199 200 assert_eq!(fixture_contracts, registry_contracts); 201 202 for fixture in &fixtures { 203 let event = event_from_parts(parts_for_fixture(&fixture.data)); 204 validate_event_contract_shape(&event, fixture.contract_id).unwrap(); 205 assert_eq!(event.kind_u32(), fixture.kind, "{}", fixture.id); 206 } 207 208 let article_parts = parts_for_fixture(&fixture_by_id(&fixtures, "wiki_article_valid").data); 209 let fork_address = format!("{}:{}:soil-health", KIND_WIKI_ARTICLE, hex_64('a')); 210 let defer_address = format!("{}:{}:soil-health-v2", KIND_WIKI_ARTICLE, hex_64('a')); 211 assert!(has_exact_tag( 212 &article_parts.tags, 213 &["a", fork_address.as_str(), RELAY_PRIMARY_WSS, "fork",] 214 )); 215 assert!(has_exact_tag( 216 &article_parts.tags, 217 &["e", hex_64('b').as_str(), RELAY_PRIMARY_WSS, "fork"] 218 )); 219 assert!(has_exact_tag( 220 &article_parts.tags, 221 &["a", defer_address.as_str(), RELAY_PRIMARY_WSS, "defer",] 222 )); 223 assert!(has_exact_tag( 224 &article_parts.tags, 225 &["e", hex_64('c').as_str(), RELAY_PRIMARY_WSS, "defer"] 226 )); 227 228 let merge_without_base = 229 parts_for_fixture(&fixture_by_id(&fixtures, "wiki_merge_request_without_base_valid").data); 230 assert!(has_exact_tag( 231 &merge_without_base.tags, 232 &["e", hex_64('f').as_str(), "", "source"] 233 )); 234 assert!( 235 !merge_without_base 236 .tags 237 .iter() 238 .any(|tag| tag == &vec!["e".to_string(), hex_64('e'), String::new()]) 239 ); 240 } 241 242 #[test] 243 fn adversarial_knowledge_fixtures_reject_at_expected_stages() { 244 let malformed = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 245 .iter() 246 .find(|fixture| fixture.id == "malformed_tags") 247 .unwrap(); 248 let mut malformed_event = 249 event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); 250 mutate_tags(&mut malformed_event, |tags| { 251 tags.push(vec![ 252 "contract".to_string(), 253 RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), 254 ]); 255 }); 256 let error = validate_event_contract_shape(&malformed_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) 257 .unwrap_err(); 258 assert_eq!(malformed.pipeline_stage, "contract_validation"); 259 assert_eq!(malformed.expected_error_code, error.code()); 260 261 let wrong_schema = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 262 .iter() 263 .find(|fixture| fixture.id == "wrong_schema") 264 .unwrap(); 265 let mut wrong_schema_event = 266 event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); 267 let mut wrong_schema_value: serde_json::Value = 268 serde_json::from_str(wrong_schema_event.content()).unwrap(); 269 wrong_schema_value["schema"] = serde_json::Value::from("radroots.knowledge.relation.v1"); 270 replace_content( 271 &mut wrong_schema_event, 272 serde_json::to_string(&wrong_schema_value).unwrap(), 273 ); 274 let error = validate_event_contract_shape(&wrong_schema_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) 275 .unwrap_err(); 276 assert_eq!(wrong_schema.pipeline_stage, "contract_validation"); 277 assert_eq!(wrong_schema.expected_error_code, error.code()); 278 279 let missing_contract = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 280 .iter() 281 .find(|fixture| fixture.id == "missing_contract_id") 282 .unwrap(); 283 let mut missing_contract_event = 284 event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); 285 mutate_tags(&mut missing_contract_event, |tags| { 286 tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); 287 }); 288 let signed = sign_parts(Nip01EventWireParts { 289 kind: missing_contract_event.kind_u32(), 290 content: missing_contract_event.content().to_string(), 291 tags: missing_contract_event.tags_as_vec(), 292 }); 293 let error = verify_and_decode_radroots_event(signed).unwrap_err(); 294 assert_eq!(missing_contract.pipeline_stage, error.code()); 295 assert!(matches!(error, RadrootsDecodeError::ContractValidation(_))); 296 297 let private_coordinates = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 298 .iter() 299 .find(|fixture| fixture.id == "private_coordinate_leakage") 300 .unwrap(); 301 let mut private_event = 302 event_from_parts(knowledge_field_report_to_wire_parts(&knowledge_field_report()).unwrap()); 303 let mut private_value: serde_json::Value = 304 serde_json::from_str(private_event.content()).unwrap(); 305 private_value["context"]["latitude"] = serde_json::Value::from("45.0000"); 306 replace_content( 307 &mut private_event, 308 serde_json::to_string(&private_value).unwrap(), 309 ); 310 let signed = sign_parts(Nip01EventWireParts { 311 kind: private_event.kind_u32(), 312 content: private_event.content().to_string(), 313 tags: private_event.tags_as_vec(), 314 }); 315 let error = verify_and_decode_radroots_event(signed).unwrap_err(); 316 assert_eq!(private_coordinates.pipeline_stage, error.code()); 317 match error { 318 RadrootsDecodeError::EventParse(error) => { 319 assert_eq!(private_coordinates.expected_error_code, error.code()); 320 } 321 error => panic!("{error:?}"), 322 } 323 324 let unsupported = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 325 .iter() 326 .find(|fixture| fixture.id == "unsupported_contract_shape") 327 .unwrap(); 328 let mut unsupported_event = 329 event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); 330 mutate_tags(&mut unsupported_event, |tags| { 331 for tag in tags { 332 if tag.first().map(|value| value.as_str()) == Some("contract") { 333 tag[1] = "radroots.knowledge.unsupported.v1".to_string(); 334 } 335 } 336 }); 337 let signed = sign_parts(Nip01EventWireParts { 338 kind: unsupported_event.kind_u32(), 339 content: unsupported_event.content().to_string(), 340 tags: unsupported_event.tags_as_vec(), 341 }); 342 let error = verify_and_decode_radroots_event(signed).unwrap_err(); 343 assert_eq!(unsupported.pipeline_stage, error.code()); 344 assert!(matches!(error, RadrootsDecodeError::ContractValidation(_))); 345 } 346 347 #[test] 348 fn nip54_and_signature_adversarial_fixtures_are_rejected() { 349 let invalid_d_tag = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 350 .iter() 351 .find(|fixture| fixture.id == "invalid_nip54_d_tag") 352 .unwrap(); 353 let mut article: WikiArticle = wiki_article(); 354 article.d_tag = "Soil Health".to_string(); 355 let error = wiki_article_to_wire_parts(&article).unwrap_err(); 356 assert_eq!(invalid_d_tag.pipeline_stage, "encode"); 357 assert_eq!(invalid_d_tag.expected_error_code, error.code()); 358 assert!(matches!(error, EventEncodeError::InvalidField("d_tag"))); 359 360 let invalid_redirect = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 361 .iter() 362 .find(|fixture| fixture.id == "invalid_redirect_target_kind") 363 .unwrap(); 364 let mut redirect = wiki_redirect(); 365 redirect.target.kind = 30023; 366 let error = wiki_redirect_to_wire_parts(&redirect).unwrap_err(); 367 assert_eq!(invalid_redirect.pipeline_stage, "encode"); 368 assert_eq!(invalid_redirect.expected_error_code, error.code()); 369 assert!(matches!( 370 error, 371 EventEncodeError::InvalidField("wiki_redirect.target") 372 )); 373 374 let missing_source = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 375 .iter() 376 .find(|fixture| fixture.id == "merge_request_missing_source_marker") 377 .unwrap(); 378 let mut missing_source_event = 379 event_from_parts(wiki_merge_request_to_wire_parts(&wiki_merge_request()).unwrap()); 380 mutate_tags(&mut missing_source_event, |tags| { 381 tags.retain(|tag| { 382 !(tag.first().map(|value| value.as_str()) == Some("e") 383 && tag.last().map(|value| value.as_str()) == Some("source")) 384 }); 385 }); 386 let error = wiki_merge_request_from_event(missing_source_event).unwrap_err(); 387 assert_eq!(missing_source.pipeline_stage, "event_parse"); 388 assert_eq!(missing_source.expected_error_code, error.code()); 389 assert!(matches!(error, EventParseError::InvalidTag("e"))); 390 391 let json_guard = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 392 .iter() 393 .find(|fixture| fixture.id == "merge_request_json_content_guard") 394 .unwrap(); 395 let merge_parts = wiki_merge_request_to_wire_parts(&wiki_merge_request()).unwrap(); 396 assert_eq!(json_guard.pipeline_stage, "wire_shape"); 397 assert_eq!(json_guard.expected_error_code, "plain_text_content"); 398 assert_eq!(merge_parts.content, "Merge synthetic soil article updates"); 399 assert!(serde_json::from_str::<serde_json::Value>(&merge_parts.content).is_err()); 400 assert!(!merge_parts.content.contains("target_article")); 401 402 let orphan_fork = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 403 .iter() 404 .find(|fixture| fixture.id == "orphan_fork_marker") 405 .unwrap(); 406 let mut orphan_fork_event = 407 event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 408 let mut removed_fork_event = false; 409 mutate_tags(&mut orphan_fork_event, |tags| { 410 tags.retain(|tag| { 411 if !removed_fork_event 412 && tag.first().map(|value| value.as_str()) == Some("e") 413 && tag.last().map(|value| value.as_str()) == Some("fork") 414 { 415 removed_fork_event = true; 416 false 417 } else { 418 true 419 } 420 }); 421 }); 422 let error = wiki_article_from_event(orphan_fork_event).unwrap_err(); 423 assert_eq!(orphan_fork.pipeline_stage, "event_parse"); 424 assert_eq!(orphan_fork.expected_error_code, error.code()); 425 assert!(matches!(error, EventParseError::InvalidTag("a"))); 426 427 let orphan_defer = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 428 .iter() 429 .find(|fixture| fixture.id == "orphan_defer_marker") 430 .unwrap(); 431 let mut orphan_defer_event = 432 event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); 433 let mut removed_defer_address = false; 434 mutate_tags(&mut orphan_defer_event, |tags| { 435 tags.retain(|tag| { 436 if !removed_defer_address 437 && tag.first().map(|value| value.as_str()) == Some("a") 438 && tag.last().map(|value| value.as_str()) == Some("defer") 439 { 440 removed_defer_address = true; 441 false 442 } else { 443 true 444 } 445 }); 446 }); 447 let error = wiki_article_from_event(orphan_defer_event).unwrap_err(); 448 assert_eq!(orphan_defer.pipeline_stage, "event_parse"); 449 assert_eq!(orphan_defer.expected_error_code, error.code()); 450 assert!(matches!(error, EventParseError::InvalidTag("e"))); 451 452 let id_mismatch = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 453 .iter() 454 .find(|fixture| fixture.id == "id_mismatch") 455 .unwrap(); 456 let signed = sign_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); 457 let mut mutated = signed.clone(); 458 let mutated_content = mutated.content().replace("Cover crops", "Compost"); 459 replace_content(&mut mutated, mutated_content); 460 let error = verify_and_decode_radroots_event(mutated).unwrap_err(); 461 assert_eq!(id_mismatch.pipeline_stage, error.code()); 462 match error { 463 RadrootsDecodeError::Nip01Verification(RadrootsNip01VerificationError::IdMismatch { 464 .. 465 }) => {} 466 error => panic!("{error:?}"), 467 } 468 469 let signature_invalidity = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 470 .iter() 471 .find(|fixture| fixture.id == "signature_invalidity") 472 .unwrap(); 473 let mut bad_signature = signed; 474 replace_sig(&mut bad_signature, "0".repeat(128)); 475 let error = verify_and_decode_radroots_event(bad_signature).unwrap_err(); 476 assert_eq!(signature_invalidity.pipeline_stage, error.code()); 477 match error { 478 RadrootsDecodeError::Nip01Verification( 479 RadrootsNip01VerificationError::SignatureInvalid, 480 ) => {} 481 error => panic!("{error:?}"), 482 } 483 } 484 485 #[test] 486 fn authoritative_knowledge_status_fields_are_rejected() { 487 for field in [ 488 "review_status", 489 "canon_status", 490 "approved_for_canon", 491 "rights_status", 492 "trust_status", 493 "trusted", 494 ] { 495 let mut event = 496 event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); 497 let mut value: serde_json::Value = serde_json::from_str(event.content()).unwrap(); 498 value[field] = serde_json::Value::from("approved"); 499 replace_content(&mut event, serde_json::to_string(&value).unwrap()); 500 let error = 501 validate_event_contract_shape(&event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA).unwrap_err(); 502 assert_eq!( 503 error, 504 ContractValidationError::ForbiddenContentField { 505 contract_id: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, 506 field, 507 } 508 ); 509 } 510 } 511 512 #[test] 513 fn immutable_knowledge_contracts_are_regular_events() { 514 let regular_ids = [ 515 (RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, KIND_KNOWLEDGE_CLAIM), 516 ("radroots.knowledge.relation.v1", KIND_KNOWLEDGE_RELATION), 517 ("radroots.knowledge.review.v1", KIND_KNOWLEDGE_REVIEW), 518 ( 519 RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, 520 KIND_KNOWLEDGE_FIELD_REPORT, 521 ), 522 ( 523 "radroots.knowledge.change_proposal.v1", 524 KIND_KNOWLEDGE_CHANGE_PROPOSAL, 525 ), 526 ( 527 "radroots.knowledge.contribution_attestation.v1", 528 KIND_CONTRIBUTION_ATTESTATION, 529 ), 530 ]; 531 for (contract_id, kind) in regular_ids { 532 let contract = all_event_contracts() 533 .iter() 534 .find(|contract| contract.id == contract_id) 535 .unwrap(); 536 assert_eq!(contract.kind, kind); 537 assert_eq!(contract.class, EventClass::Regular); 538 } 539 } 540 541 #[test] 542 fn verified_decode_exposes_representative_public_surface_events() { 543 let fixture_ids = [ 544 "wiki_article_valid", 545 "wiki_redirect_valid", 546 "wiki_merge_request_without_base_valid", 547 "knowledge_source_valid", 548 "knowledge_claim_valid", 549 "knowledge_review_valid", 550 "knowledge_field_report_valid", 551 ]; 552 for fixture in knowledge_valid_fixtures() 553 .into_iter() 554 .filter(|fixture| fixture_ids.contains(&fixture.id)) 555 { 556 let signed = sign_parts(parts_for_fixture(&fixture.data)); 557 let decoded = verify_and_decode_radroots_event(signed).unwrap(); 558 match decoded { 559 RadrootsDecodedEvent::WikiArticle(_) 560 | RadrootsDecodedEvent::WikiRedirect(_) 561 | RadrootsDecodedEvent::WikiMergeRequest(_) 562 | RadrootsDecodedEvent::KnowledgeSource(_) 563 | RadrootsDecodedEvent::KnowledgeClaim(_) 564 | RadrootsDecodedEvent::KnowledgeReview(_) 565 | RadrootsDecodedEvent::KnowledgeFieldReport(_) => {} 566 decoded => panic!("{decoded:?}"), 567 } 568 } 569 } 570 571 #[cfg(feature = "manifests")] 572 #[test] 573 fn checked_in_manifest_and_decode_vectors_execute_every_declared_case() { 574 const PACKAGED_VECTORS: &str = include_str!("fixtures/knowledge_manifest_and_decode.v1.json"); 575 let workspace_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 576 .join("../../contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json"); 577 if let Ok(workspace_vectors) = std::fs::read_to_string(workspace_path) { 578 assert_eq!(PACKAGED_VECTORS, workspace_vectors); 579 } 580 581 let suite: serde_json::Value = serde_json::from_str(PACKAGED_VECTORS).expect("vector suite"); 582 assert_eq!(suite["suite"], "knowledge_manifest_and_decode"); 583 assert_eq!(suite["contract_version"], "1.0.0"); 584 let vectors = suite["vectors"].as_array().expect("vectors"); 585 assert_eq!(vectors.len(), 7); 586 587 let fixtures = knowledge_valid_fixtures(); 588 for vector in vectors { 589 let id = vector["id"].as_str().expect("vector id"); 590 let kind = vector["kind"].as_str().expect("vector kind"); 591 let expected = vector["expected"].as_object().expect("expected object"); 592 match kind { 593 "knowledge.contract_manifest_json.valid" => { 594 let manifest_json = contract_manifest_json().expect("manifest JSON"); 595 let manifest: serde_json::Value = 596 serde_json::from_str(manifest_json.as_str()).expect("manifest value"); 597 assert_eq!( 598 manifest["schema_version"], expected["schema_version"], 599 "{id}" 600 ); 601 assert_eq!( 602 manifest["registry_version"], expected["registry_version"], 603 "{id}" 604 ); 605 let contracts = manifest["contracts"].as_array().expect("contracts"); 606 assert_eq!( 607 contracts.len(), 608 knowledge_contract_manifest().contracts.len(), 609 "{id}" 610 ); 611 for field in expected["required_fields"] 612 .as_array() 613 .expect("required fields") 614 { 615 let field = field.as_str().expect("field name"); 616 assert!( 617 manifest.get(field).is_some() 618 || contracts 619 .iter() 620 .all(|contract| contract.get(field).is_some()), 621 "{id}: missing required manifest field {field}" 622 ); 623 } 624 } 625 "knowledge.verify_and_decode_event.valid" => { 626 let fixture_id = vector["input"]["fixture"].as_str().expect("fixture id"); 627 let signed = sign_parts(parts_for_fixture( 628 &fixture_by_id(&fixtures, fixture_id).data, 629 )); 630 let decoded = verify_and_decode_radroots_event(signed).expect("decoded event"); 631 assert!( 632 matches!(decoded, RadrootsDecodedEvent::KnowledgeClaim(_)), 633 "{id}" 634 ); 635 assert_eq!(expected["decoded_variant"], "KnowledgeClaim", "{id}"); 636 assert_eq!( 637 expected["contract_id"], RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, 638 "{id}" 639 ); 640 } 641 "knowledge.verify_and_decode_event.invalid" => { 642 let mut parts = knowledge_claim_to_wire_parts(&knowledge_claim()).expect("parts"); 643 parts 644 .tags 645 .retain(|tag| tag.first().map(String::as_str) != Some("contract")); 646 let error = verify_and_decode_radroots_event(sign_parts(parts)) 647 .expect_err("missing contract must fail"); 648 assert!( 649 matches!(error, RadrootsDecodeError::ContractValidation(_)), 650 "{id}" 651 ); 652 assert_eq!(expected["error_class"], "decode_error", "{id}"); 653 assert_eq!(expected["stage"], error.code(), "{id}"); 654 } 655 "knowledge.nip54.wiki_article_tags.valid" 656 | "knowledge.nip54.wiki_redirect_tags.valid" 657 | "knowledge.nip54.wiki_merge_request.valid" => { 658 let fixture_id = vector["input"]["fixture"].as_str().expect("fixture id"); 659 let parts = parts_for_fixture(&fixture_by_id(&fixtures, fixture_id).data); 660 if let Some(content) = expected.get("content") { 661 assert_eq!(content, parts.content.as_str(), "{id}"); 662 } 663 for (field, value) in expected { 664 if field.ends_with("_tag") { 665 let expected_tag = value 666 .as_array() 667 .expect("expected tag") 668 .iter() 669 .map(|value| value.as_str().expect("tag value").to_owned()) 670 .collect::<Vec<_>>(); 671 assert!(parts.tags.contains(&expected_tag), "{id}: missing {field}"); 672 } 673 } 674 if expected.get("base_version_event_id") == Some(&serde_json::Value::Null) { 675 assert!( 676 !parts.tags.iter().any(|tag| { 677 tag == &vec!["e".to_owned(), hex_64('e'), String::new()] 678 }), 679 "{id}: unexpected base event tag" 680 ); 681 } 682 } 683 kind => panic!("{id}: unsupported vector kind {kind}"), 684 } 685 } 686 }