package_boundary.rs (9836B)
1 use std::{collections::BTreeSet, fs, path::Path}; 2 3 #[allow(unused_imports)] 4 use radroots_signing::{ 5 Actor, Error, SignReceipt, SignRequest, Signer, SignerStatus, actor as _, authorization as _, 6 capability as _, error as _, identity as _, receipt as _, recovery as _, request as _, 7 signer as _, status as _, 8 }; 9 10 const MANIFEST: &str = include_str!("../Cargo.toml"); 11 const EXAMPLE: &str = include_str!("../examples/host_signer.rs"); 12 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_signing.txt"); 13 const README: &str = include_str!("../README.md"); 14 const ROOT: &str = include_str!("../src/lib.rs"); 15 16 #[test] 17 fn manifest_has_final_identity_features_and_dependencies() { 18 for required in [ 19 "name = \"radroots_signing\"", 20 "version = \"0.1.0-alpha\"", 21 "publish = [\"crates-io\"]", 22 "default = [\"std\", \"serde\"]", 23 "radroots_event = { workspace = true, default-features = false }", 24 "radroots_event_codec = { workspace = true, default-features = false }", 25 "radroots_identity = { workspace = true, default-features = false }", 26 "radroots_protocol = { workspace = true, default-features = false }", 27 ] { 28 assert!( 29 MANIFEST.contains(required), 30 "manifest is missing {required}" 31 ); 32 } 33 assert_eq!( 34 table_keys(MANIFEST, "[features]"), 35 BTreeSet::from(["default", "serde", "std"]) 36 ); 37 assert_eq!( 38 table_keys(MANIFEST, "[dependencies]"), 39 BTreeSet::from([ 40 "hex", 41 "radroots_event", 42 "radroots_event_codec", 43 "radroots_identity", 44 "radroots_protocol", 45 "serde", 46 "sha2", 47 ]) 48 ); 49 assert_eq!( 50 table_keys(MANIFEST, "[dev-dependencies]"), 51 BTreeSet::from(["nostr", "radroots_blossom", "serde_json"]) 52 ); 53 for forbidden in [ 54 "async-trait", 55 "keyring", 56 "nostr", 57 "nostr-sdk", 58 "reqwest", 59 "sqlx", 60 "tokio", 61 ] { 62 assert!( 63 !table_keys(MANIFEST, "[dependencies]").contains(forbidden), 64 "signing runtime must not depend on {forbidden}" 65 ); 66 } 67 } 68 69 #[test] 70 fn crate_root_declares_the_approved_module_skeleton() { 71 assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]")); 72 for module in [ 73 "actor", 74 "authorization", 75 "capability", 76 "error", 77 "identity", 78 "recovery", 79 "request", 80 "receipt", 81 "signer", 82 "status", 83 ] { 84 let declaration = format!("pub mod {module};"); 85 assert!( 86 ROOT.contains(&declaration), 87 "crate root is missing {module}" 88 ); 89 } 90 assert_eq!( 91 root_declarations("pub mod "), 92 BTreeSet::from([ 93 "actor", 94 "authorization", 95 "capability", 96 "error", 97 "identity", 98 "receipt", 99 "recovery", 100 "request", 101 "signer", 102 "status", 103 ]) 104 ); 105 let _ = core::mem::size_of::<Actor>(); 106 let _ = core::mem::size_of::<SignRequest>(); 107 let _ = core::mem::size_of::<SignReceipt>(); 108 let _ = core::mem::size_of::<SignerStatus>(); 109 let _ = core::mem::size_of::<Error>(); 110 fn assert_object_safe(_: &dyn Signer) {} 111 let _ = assert_object_safe; 112 for root_export in [ 113 "pub use actor::Actor;", 114 "pub use authorization::{CurrentAuthoringAuthority, CurrentAuthoringDecision};", 115 "pub use error::Error;", 116 "pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId};", 117 "pub use receipt::{AuthoredSignEvidence, SignReceipt};", 118 "pub use request::{SignRequest, SigningPurpose};", 119 "pub use signer::Signer;", 120 "pub use status::SignerStatus;", 121 ] { 122 assert!(ROOT.contains(root_export), "missing {root_export}"); 123 } 124 assert_eq!( 125 ROOT.lines() 126 .map(str::trim) 127 .filter(|line| line.starts_with("pub use ")) 128 .collect::<BTreeSet<_>>(), 129 BTreeSet::from([ 130 "pub use actor::Actor;", 131 "pub use authorization::{CurrentAuthoringAuthority, CurrentAuthoringDecision};", 132 "pub use error::Error;", 133 "pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId};", 134 "pub use receipt::{AuthoredSignEvidence, SignReceipt};", 135 "pub use request::{SignRequest, SigningPurpose};", 136 "pub use signer::Signer;", 137 "pub use status::SignerStatus;", 138 ]) 139 ); 140 assert!(!ROOT.contains("prelude")); 141 } 142 143 #[test] 144 fn package_documentation_and_reviewed_api_baseline_are_complete() { 145 for required in [ 146 "## Typical flow", 147 "## Host SPI contract", 148 "## Deadlines, cancellation, and commit points", 149 "## Serialization contract", 150 "## Security and side effects", 151 "## Features", 152 "## Intended consumers", 153 "radroots_crates_release_v1.toml", 154 "examples/host_signer.rs", 155 ] { 156 assert!(README.contains(required), "README is missing {required}"); 157 } 158 for required in [ 159 "impl Signer for HostSigner", 160 "fn status(&self) -> BoxFuture", 161 "fn sign(&self, _request: SignRequest) -> BoxFuture", 162 "let signer: &dyn Signer", 163 "drop(future)", 164 ] { 165 assert!(EXAMPLE.contains(required), "example is missing {required}"); 166 } 167 for required in [ 168 "pub mod radroots_signing::actor", 169 "pub mod radroots_signing::capability", 170 "pub mod radroots_signing::error", 171 "pub mod radroots_signing::receipt", 172 "pub mod radroots_signing::request", 173 "pub mod radroots_signing::signer", 174 "pub mod radroots_signing::status", 175 "pub enum radroots_signing::request::SigningPurpose", 176 "pub radroots_signing::request::SigningPurpose::BlossomUploadAuthorization", 177 "pub trait radroots_signing::Signer", 178 ] { 179 assert!( 180 PUBLIC_API.contains(required), 181 "public API baseline is missing {required}" 182 ); 183 } 184 } 185 186 #[test] 187 fn every_public_module_has_crate_level_documentation() { 188 let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); 189 for module in [ 190 "actor", 191 "authorization", 192 "capability", 193 "error", 194 "identity", 195 "recovery", 196 "request", 197 "receipt", 198 "signer", 199 "status", 200 ] { 201 let path = source_root.join(format!("{module}.rs")); 202 let source = fs::read_to_string(&path).expect("read module source"); 203 assert!( 204 source.starts_with("//! "), 205 "public module {module} must start with module documentation" 206 ); 207 } 208 } 209 210 #[test] 211 fn production_sources_publish_only_the_approved_traits_and_no_host_stack() { 212 let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); 213 let mut sources = Vec::new(); 214 collect_rust_sources(&source_root, &mut sources); 215 assert!(!sources.is_empty()); 216 let mut public_traits = BTreeSet::new(); 217 218 for path in sources { 219 let source = fs::read_to_string(&path).expect("read signing source"); 220 let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); 221 for line in production.lines() { 222 let trimmed = line.trim_start(); 223 if let Some(name) = trimmed 224 .strip_prefix("pub trait ") 225 .and_then(|rest| rest.split([':', '<', ' ']).next()) 226 { 227 public_traits.insert(name.to_owned()); 228 } 229 for forbidden in [ 230 "nostr::", 231 "nostr_sdk::", 232 "reqwest::", 233 "sqlx::", 234 "tokio::", 235 "keyring::", 236 "std::fs", 237 "std::path", 238 "SecretKey", 239 "PrivateKey", 240 ] { 241 assert!( 242 !line.contains(forbidden), 243 "signing production source must not contain {forbidden}: {}: {trimmed}", 244 path.display() 245 ); 246 } 247 } 248 } 249 250 assert_eq!( 251 public_traits, 252 ["CurrentAuthoringAuthority", "ProgressObserver", "Signer"] 253 .into_iter() 254 .map(str::to_owned) 255 .collect() 256 ); 257 } 258 259 fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { 260 let table = manifest 261 .split_once(heading) 262 .unwrap_or_else(|| panic!("missing manifest table {heading}")) 263 .1; 264 table 265 .lines() 266 .skip(1) 267 .take_while(|line| !line.trim_start().starts_with('[')) 268 .filter_map(|line| { 269 let line = line.trim(); 270 (line 271 .bytes() 272 .next() 273 .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_') 274 && !line.starts_with('#')) 275 .then(|| line.split_once('=').map(|(key, _)| key.trim())) 276 .flatten() 277 }) 278 .collect() 279 } 280 281 fn root_declarations(prefix: &str) -> BTreeSet<&str> { 282 ROOT.lines() 283 .map(str::trim) 284 .filter_map(|line| line.strip_prefix(prefix)) 285 .filter_map(|name| name.strip_suffix(';')) 286 .collect() 287 } 288 289 fn collect_rust_sources(directory: &Path, paths: &mut Vec<std::path::PathBuf>) { 290 for entry in fs::read_dir(directory).expect("read signing source directory") { 291 let path = entry.expect("source directory entry").path(); 292 if path.is_dir() { 293 collect_rust_sources(&path, paths); 294 } else if path.extension().and_then(|value| value.to_str()) == Some("rs") { 295 paths.push(path); 296 } 297 } 298 }