generic_builder_boundary.rs (4654B)
1 use std::fs; 2 use std::path::{Path, PathBuf}; 3 4 #[test] 5 fn public_source_does_not_expose_the_upstream_event_builder() { 6 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) 7 .parent() 8 .and_then(Path::parent) 9 .expect("lib repo root"); 10 let mut findings = Vec::new(); 11 12 for path in rust_source_files(repo_root.join("crates").as_path()) { 13 if path.file_name().and_then(|name| name.to_str()) == Some("generic_builder_boundary.rs") { 14 continue; 15 } 16 let source = fs::read_to_string(&path).expect("read Rust source"); 17 let normalized = source.split_whitespace().collect::<Vec<_>>().join(" "); 18 for statement in normalized.split(';') { 19 let statement = statement.trim(); 20 let exposes_alias = 21 statement.starts_with("pub type ") && statement.contains("EventBuilder"); 22 let exposes_reexport = 23 statement.starts_with("pub use nostr") && statement.contains("EventBuilder"); 24 if exposes_alias || exposes_reexport { 25 findings.push(format!( 26 "{} exposes upstream EventBuilder: {statement}", 27 relative_path(repo_root, &path) 28 )); 29 } 30 } 31 32 for forbidden in [ 33 "impl Deref for GenericBuilder", 34 "impl AsRef<nostr::EventBuilder> for GenericBuilder", 35 "impl From<GenericBuilder> for nostr::EventBuilder", 36 "impl Into<nostr::EventBuilder> for GenericBuilder", 37 "impl Deref for ExternalSigningRequest", 38 "impl AsRef<nostr::UnsignedEvent> for ExternalSigningRequest", 39 "impl From<ExternalSigningRequest> for nostr::UnsignedEvent", 40 "impl Into<nostr::UnsignedEvent> for ExternalSigningRequest", 41 "impl Deserialize for ExternalSigningRequest", 42 "fn into_unsigned_event", 43 "fn as_unsigned_event", 44 "fn unsigned_event_mut", 45 ] { 46 if normalized.contains(forbidden) { 47 findings.push(format!( 48 "{} contains forbidden raw-builder escape `{forbidden}`", 49 relative_path(repo_root, &path) 50 )); 51 } 52 } 53 } 54 55 assert!( 56 findings.is_empty(), 57 "public generic-builder boundary violations:\n{}", 58 findings.join("\n") 59 ); 60 } 61 62 #[test] 63 fn every_strict_builder_is_plan_backed_and_has_no_unchecked_mapping_path() { 64 let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); 65 for (module, expected_body_conversions) in [ 66 ("metadata.rs", 1), 67 ("post.rs", 3), 68 ("reply.rs", 1), 69 ("deletion.rs", 1), 70 ("comment.rs", 1), 71 ("food_availability.rs", 1), 72 ] { 73 let source = fs::read_to_string(crate_root.join("src/events").join(module)) 74 .unwrap_or_else(|error| panic!("read {module}: {error}")); 75 assert!( 76 source.contains("SealedBuilderCore"), 77 "strict builder module {module} does not use the shared plan core" 78 ); 79 assert_eq!( 80 source.matches("AuthoredEventBody::from_").count(), 81 expected_body_conversions, 82 "strict builder module {module} has an incomplete body-conversion inventory" 83 ); 84 for forbidden in [ 85 "build_event_unchecked", 86 "RadrootsNostrEventBuilderUnchecked", 87 "_to_wire_parts", 88 ] { 89 assert!( 90 !source.contains(forbidden), 91 "strict builder module {module} retains unchecked mapping `{forbidden}`" 92 ); 93 } 94 for required in ["sign_with_keys", "into_external_signing_request"] { 95 assert!( 96 source.contains(required), 97 "strict builder module {module} is missing `{required}`" 98 ); 99 } 100 } 101 } 102 103 fn rust_source_files(root: &Path) -> Vec<PathBuf> { 104 let mut paths = Vec::new(); 105 collect_rust_source_files(root, &mut paths); 106 paths.sort(); 107 paths 108 } 109 110 fn collect_rust_source_files(root: &Path, paths: &mut Vec<PathBuf>) { 111 for entry in fs::read_dir(root).expect("read source directory") { 112 let path = entry.expect("source entry").path(); 113 if path.is_dir() { 114 collect_rust_source_files(&path, paths); 115 } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { 116 paths.push(path); 117 } 118 } 119 } 120 121 fn relative_path(root: &Path, path: &Path) -> String { 122 path.strip_prefix(root) 123 .expect("source path is under repo root") 124 .to_string_lossy() 125 .replace('\\', "/") 126 }