package_boundary.rs (13615B)
1 use std::collections::BTreeSet; 2 3 use radroots_event::id::{ 4 CandidateId, DTag, EventId, EventSignature, MutationId, Nip01Coordinate, TradeId, 5 }; 6 #[cfg(feature = "knowledge")] 7 #[allow(unused_imports)] 8 use radroots_event::knowledge as _; 9 #[allow(unused_imports)] 10 use radroots_event::{ 11 admission as _, calendar as _, contract as _, draft as _, envelope as _, farm as _, food as _, 12 id as _, listing as _, media as _, post as _, profile as _, social as _, tag as _, trade as _, 13 wire as _, 14 }; 15 use radroots_identity::PublicKey; 16 17 const MANIFEST: &str = include_str!("../Cargo.toml"); 18 const ROOT: &str = include_str!("../src/lib.rs"); 19 const IDENTIFIERS: &str = include_str!("../src/id.rs"); 20 const CONTRACT_REGISTRY: &str = include_str!("../src/contract/registry_v7.rs"); 21 const RELAY_HINT: &str = include_str!("../src/relay_hint.rs"); 22 const TRADE: &str = include_str!("../src/trade.rs"); 23 const ADMISSION: &str = include_str!("../src/admission.rs"); 24 const VERIFICATION: &str = include_str!("../src/verification.rs"); 25 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_event.txt"); 26 const CODEC_MANIFEST: &str = include_str!("../../event_codec/Cargo.toml"); 27 28 #[test] 29 fn manifest_has_final_identity_and_required_radroots_dependencies() { 30 assert!(MANIFEST.contains("name = \"radroots_event\"")); 31 assert!(MANIFEST.contains("version = \"0.1.0-alpha\"")); 32 assert!(MANIFEST.contains("publish = [\"crates-io\"]")); 33 assert!(MANIFEST.contains("[lib]\nname = \"radroots_event\"")); 34 assert!(MANIFEST.contains("default = [\"std\", \"serde\"]")); 35 assert_eq!( 36 table_keys(MANIFEST, "[dependencies]") 37 .into_iter() 38 .filter(|dependency| dependency.starts_with("radroots_")) 39 .collect::<BTreeSet<_>>(), 40 BTreeSet::from([ 41 "radroots_blossom", 42 "radroots_core", 43 "radroots_identity", 44 "radroots_protocol", 45 ]) 46 ); 47 assert!( 48 MANIFEST.contains("radroots_protocol = { workspace = true, default-features = false }") 49 ); 50 assert_eq!( 51 table_keys(MANIFEST, "[features]"), 52 BTreeSet::from(["default", "knowledge", "serde", "std"]), 53 "radroots_event must expose only its approved user-visible feature vocabulary" 54 ); 55 for implementation_feature in ["dto-bindgen", "fixture", "knowledge-nip54", "signature"] { 56 assert!( 57 !table_keys(MANIFEST, "[features]").contains(implementation_feature), 58 "implementation feature `{implementation_feature}` must remain private" 59 ); 60 } 61 assert!(!table_keys(MANIFEST, "[dependencies]").contains("dto_bindgen")); 62 assert!(!table_keys(MANIFEST, "[dependencies]").contains("secp256k1")); 63 for forbidden_dependency in [ 64 "libsqlite3-sys", 65 "nostr", 66 "nostr-sdk", 67 "reqwest", 68 "sqlx", 69 "tokio", 70 ] { 71 assert!( 72 !table_keys(MANIFEST, "[dependencies]").contains(forbidden_dependency), 73 "forbidden implementation dependency `{forbidden_dependency}`" 74 ); 75 } 76 } 77 78 #[test] 79 fn public_traits_are_exact_and_classified_as_host_spis() { 80 let declarations = [ADMISSION, VERIFICATION] 81 .into_iter() 82 .flat_map(|source| source.lines()) 83 .map(str::trim) 84 .filter_map(|line| line.strip_prefix("pub trait ")) 85 .filter_map(|declaration| declaration.split([':', ' ', '{']).next()) 86 .collect::<BTreeSet<_>>(); 87 88 assert_eq!( 89 declarations, 90 BTreeSet::from(["AdmissionPolicy", "SignatureVerifier", "VisibilityPolicy"]) 91 ); 92 for declaration in [ 93 "pub trait AdmissionPolicy: Send + Sync", 94 "pub trait VisibilityPolicy: Send + Sync", 95 "pub trait SignatureVerifier: Send + Sync", 96 ] { 97 assert!( 98 ADMISSION.contains(declaration) || VERIFICATION.contains(declaration), 99 "missing native Host SPI contract `{declaration}`" 100 ); 101 } 102 assert_eq!( 103 ADMISSION.matches("/// **Host SPI:**").count() 104 + VERIFICATION.matches("/// **Host SPI:**").count(), 105 3, 106 "every public event trait must carry one explicit Host SPI classification" 107 ); 108 } 109 110 #[test] 111 fn crate_root_declares_every_approved_module() { 112 let declared = root_declarations("pub mod "); 113 let approved = [ 114 "admission", 115 "calendar", 116 "contract", 117 "draft", 118 "envelope", 119 "farm", 120 "food", 121 "generic_draft", 122 "id", 123 "knowledge", 124 "listing", 125 "media", 126 "post", 127 "profile", 128 "social", 129 "tag", 130 "trade", 131 "wire", 132 ]; 133 assert_eq!( 134 declared, 135 approved.into_iter().collect(), 136 "the crate root must expose exactly the approved singular module vocabulary" 137 ); 138 } 139 140 #[test] 141 fn crate_root_exposes_the_curated_native_event_vocabulary() { 142 type CuratedRootTypes = ( 143 radroots_event::Event, 144 radroots_event::GenericEventDraft, 145 radroots_event::SignedEvent, 146 radroots_event::VerifiedEvent, 147 radroots_event::EventId, 148 radroots_event::EventKind, 149 radroots_event::EventTag, 150 radroots_event::Error, 151 ); 152 let _: Option<CuratedRootTypes> = None; 153 for root_export in [ 154 "Event", 155 "GenericEventDraft", 156 "SignedEvent", 157 "VerifiedEvent", 158 "EventId", 159 "EventKind", 160 "EventTag", 161 "Error", 162 ] { 163 assert!( 164 ROOT.contains(root_export), 165 "missing root export {root_export}" 166 ); 167 } 168 assert!(!ROOT.contains("pub use *")); 169 assert!(!ROOT.contains("pub use crate::*")); 170 } 171 172 #[test] 173 fn public_native_items_do_not_retain_the_legacy_radroots_prefix() { 174 let prefixed_items = PUBLIC_API 175 .lines() 176 .filter(|line| { 177 line.split("::").any(|segment| { 178 segment 179 .strip_prefix("Radroots") 180 .and_then(|suffix| suffix.chars().next()) 181 .is_some_and(|character| character.is_ascii_uppercase()) 182 }) 183 }) 184 .collect::<Vec<_>>(); 185 186 assert!( 187 prefixed_items.is_empty(), 188 "legacy-prefixed native items remain public: {prefixed_items:#?}" 189 ); 190 for retired in [ 191 "radroots_event::post::Post", 192 "radroots_event::profile::Profile", 193 ] { 194 let declaration = format!("pub struct {retired}"); 195 let member_prefix = format!("pub {retired}::"); 196 assert!( 197 !PUBLIC_API 198 .lines() 199 .any(|line| line == declaration || line.starts_with(&member_prefix)), 200 "retired compatibility type remains public: {retired}" 201 ); 202 } 203 } 204 205 #[test] 206 fn event_codec_has_no_lossy_post_or_profile_projection_surface() { 207 assert!(CODEC_MANIFEST.contains("publish = [\"crates-io\"]")); 208 let codec_root = include_str!("../../event_codec/src/lib.rs"); 209 let post_module = include_str!("../../event_codec/src/post/mod.rs"); 210 let profile_module = include_str!("../../event_codec/src/profile/mod.rs"); 211 for source in [codec_root, post_module, profile_module] { 212 assert!(!source.contains("LegacyPost")); 213 assert!(!source.contains("LegacyProfile")); 214 assert!(!source.contains("RadrootsProfileData")); 215 } 216 assert!(!post_module.contains("pub mod decode;")); 217 assert!(!profile_module.contains("pub mod decode;")); 218 } 219 220 #[test] 221 fn verification_typestates_are_native_private_and_policy_gated() { 222 let admission = include_str!("../src/admission.rs"); 223 let verification = include_str!("../src/verification.rs"); 224 225 for state in [ 226 "RawEvent", 227 "IdVerifiedEvent", 228 "SignatureVerifiedEvent", 229 "ContractValidatedEvent", 230 "AdmittedEvent", 231 "VisibleEvent", 232 ] { 233 assert!( 234 admission.contains(state) || verification.contains(state), 235 "missing native typestate {state}" 236 ); 237 } 238 assert!(ROOT.contains("mod verification;")); 239 assert!(!ROOT.contains("pub mod verification;")); 240 assert!(verification.contains("pub struct IdVerifiedEvent(EventEnvelope);")); 241 assert!(verification.contains("pub struct SignatureVerifiedEvent(EventEnvelope);")); 242 assert!(admission.contains("policy.admit(&self)?;")); 243 assert!(admission.contains("policy.make_visible(&self)?;")); 244 assert!(verification.contains("pub trait SignatureVerifier: Send + Sync")); 245 assert!(verification.contains("verifier.verify_signature(&self.0)?;")); 246 assert!(!verification.contains("secp256k1")); 247 assert!(!verification.contains("SignatureVerificationUnavailable")); 248 assert!(!verification.contains("impl From<RawEvent> for IdVerifiedEvent")); 249 assert!(!verification.contains("impl From<IdVerifiedEvent> for SignatureVerifiedEvent")); 250 assert!(!admission.contains("impl From<ContractValidatedEvent> for AdmittedEvent")); 251 assert!(!admission.contains("impl From<AdmittedEvent> for VisibleEvent")); 252 } 253 254 #[test] 255 fn contract_author_roles_are_event_owned_requirements_not_signer_provenance() { 256 let seller = 257 radroots_event::contract::event_contract("radroots.operational_listing.published.v1") 258 .expect("operational listing contract"); 259 260 assert_eq!( 261 seller.required_author_role(), 262 radroots_event::contract::AuthorRole::Seller 263 ); 264 assert!(!CONTRACT_REGISTRY.contains("RadrootsActorRole")); 265 assert!(!CONTRACT_REGISTRY.contains("pub author_role:")); 266 assert!(!CONTRACT_REGISTRY.contains("radroots_signing")); 267 assert!(!CONTRACT_REGISTRY.contains("RadrootsActorContext")); 268 assert!(!CONTRACT_REGISTRY.contains("AccountId")); 269 } 270 271 #[test] 272 fn canonical_identifier_api_owns_bytes_and_requires_explicit_text_encoding() { 273 let event_id = EventId::parse("A".repeat(64)).expect("event id"); 274 let signature = EventSignature::parse("B".repeat(128)).expect("event signature"); 275 let d_tag = DTag::parse("listing-1").expect("d tag"); 276 let coordinate = Nip01Coordinate::parse(format!("30000:{}:listing-1", event_id.to_hex())) 277 .expect("coordinate"); 278 279 assert_eq!(core::mem::size_of::<EventId>(), 32); 280 assert_eq!(core::mem::size_of::<EventSignature>(), 64); 281 assert_eq!(event_id.to_hex(), "a".repeat(64)); 282 assert_eq!(EventId::from_bytes(event_id.into_bytes()), event_id); 283 assert_eq!(signature.to_hex(), "b".repeat(128)); 284 assert_eq!(d_tag.as_str(), "listing-1"); 285 assert_eq!(coordinate.identifier(), "listing-1"); 286 assert!(!IDENTIFIERS.contains("impl Deref")); 287 assert!(!IDENTIFIERS.contains("impl Borrow<str>")); 288 assert!(!RELAY_HINT.contains("impl Deref")); 289 assert!(!RELAY_HINT.contains("impl Borrow<str>")); 290 } 291 292 #[test] 293 fn event_references_use_the_identity_owned_public_key() { 294 let author = 295 PublicKey::from_hex("585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df") 296 .expect("valid public key"); 297 let reference = radroots_event::tag::EventRef { 298 id: "a".repeat(64), 299 author, 300 kind: 1, 301 d_tag: None, 302 relays: None, 303 }; 304 305 assert_eq!(reference.author, author); 306 assert!(!IDENTIFIERS.contains("pub(crate) use radroots_identity::PublicKey")); 307 } 308 309 #[test] 310 fn trade_protocol_identifiers_have_one_definition_and_deliberate_facades() { 311 let trade_id = TradeId::parse("11".repeat(16)).expect("trade id"); 312 let candidate_id = CandidateId::parse("22".repeat(32)).expect("candidate id"); 313 let mutation_id = MutationId::parse("33".repeat(32)).expect("mutation id"); 314 315 let trade_surface_id: radroots_event::trade::TradeId = trade_id; 316 let trade_surface_candidate: radroots_event::trade::CandidateId = candidate_id; 317 let trade_surface_mutation: radroots_event::trade::MutationId = mutation_id; 318 319 assert_eq!(trade_surface_id, trade_id); 320 assert_eq!(trade_surface_candidate, candidate_id); 321 assert_eq!(trade_surface_mutation, mutation_id); 322 assert_eq!(core::mem::size_of::<TradeId>(), 16); 323 assert_eq!(core::mem::size_of::<CandidateId>(), 32); 324 assert_eq!(core::mem::size_of::<MutationId>(), 32); 325 assert!(TradeId::parse("order-1").is_err()); 326 assert!(radroots_event::id::OrderId::parse("order-1").is_ok()); 327 328 for definition in [ 329 "validated_hex_id!(TradeId, 16);", 330 "validated_hex_id!(CandidateId, 32);", 331 "validated_hex_id!(MutationId, 32);", 332 ] { 333 assert_eq!(IDENTIFIERS.matches(definition).count(), 1, "{definition}"); 334 } 335 for duplicate in [ 336 "pub struct TradeId", 337 "pub struct CandidateId", 338 "pub struct MutationId", 339 ] { 340 assert!( 341 !TRADE.contains(duplicate), 342 "duplicate definition: {duplicate}" 343 ); 344 } 345 } 346 347 fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { 348 let Some((_, table)) = manifest.split_once(heading) else { 349 return BTreeSet::new(); 350 }; 351 table 352 .lines() 353 .skip(1) 354 .take_while(|line| !line.trim_start().starts_with('[')) 355 .filter_map(|line| { 356 let line = line.trim(); 357 (line 358 .bytes() 359 .next() 360 .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_') 361 && !line.starts_with('#')) 362 .then(|| line.split_once('=').map(|(key, _)| key.trim())) 363 .flatten() 364 }) 365 .collect() 366 } 367 368 fn root_declarations(prefix: &str) -> BTreeSet<&str> { 369 ROOT.lines() 370 .map(str::trim) 371 .filter_map(|line| line.strip_prefix(prefix)) 372 .filter_map(|name| name.strip_suffix(';')) 373 .collect() 374 }