package_boundary.rs (5775B)
1 use std::collections::BTreeSet; 2 use std::fs; 3 use std::path::{Path, PathBuf}; 4 5 const MANIFEST: &str = include_str!("../Cargo.toml"); 6 const ROOT: &str = include_str!("../src/lib.rs"); 7 const README: &str = include_str!("../README.md"); 8 const EXAMPLE: &str = include_str!("../examples/explicit_memory_provider.rs"); 9 10 #[test] 11 fn manifest_has_final_identity_features_and_no_radroots_dependencies() { 12 for required in [ 13 "name = \"radroots_secrets\"", 14 "version = \"0.1.0-alpha\"", 15 "publish = [\"crates-io\"]", 16 "documentation = \"https://docs.rs/radroots_secrets\"", 17 "[lib]\nname = \"radroots_secrets\"", 18 "default = [\"std\", \"serde\"]", 19 "memory = [\"std\"]", 20 "file = [\"std\", \"dep:tempfile\"]", 21 "keyring = [\"std\", \"dep:keyring\"]", 22 ] { 23 assert!( 24 MANIFEST.contains(required), 25 "manifest is missing `{required}`" 26 ); 27 } 28 29 assert_eq!( 30 table_keys(MANIFEST, "[features]"), 31 BTreeSet::from(["default", "file", "keyring", "memory", "serde", "std"]) 32 ); 33 assert!( 34 table_keys(MANIFEST, "[dependencies]") 35 .into_iter() 36 .all(|dependency| !dependency.starts_with("radroots_")), 37 "security SPI must not depend on another Radroots package" 38 ); 39 assert_eq!( 40 table_keys(MANIFEST, "[dependencies]"), 41 BTreeSet::from([ 42 "chacha20poly1305", 43 "keyring", 44 "serde", 45 "sha2", 46 "subtle", 47 "tempfile", 48 "zeroize" 49 ]) 50 ); 51 assert_eq!( 52 table_keys(MANIFEST, "[dev-dependencies]"), 53 BTreeSet::from(["futures-executor", "hex", "serde_json"]) 54 ); 55 } 56 57 #[test] 58 fn production_sources_publish_only_the_approved_traits() { 59 let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); 60 let mut paths = Vec::new(); 61 collect_rust_sources(&source_root, &mut paths); 62 let mut public_traits = BTreeSet::new(); 63 64 for path in paths { 65 let source = fs::read_to_string(&path).expect("read secrets source"); 66 let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); 67 for line in production.lines() { 68 if let Some(name) = line 69 .trim_start() 70 .strip_prefix("pub trait ") 71 .and_then(|rest| rest.split([':', '<', ' ']).next()) 72 { 73 public_traits.insert(name.to_owned()); 74 } 75 } 76 } 77 78 assert_eq!( 79 public_traits, 80 ["KeyWrapping", "SecretProvider"] 81 .into_iter() 82 .map(str::to_owned) 83 .collect() 84 ); 85 } 86 87 #[test] 88 fn crate_root_contains_only_the_approved_module_skeleton() { 89 assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]")); 90 assert_eq!( 91 declarations(ROOT, "pub mod "), 92 BTreeSet::from([ 93 "context", "envelope", "error", "file", "id", "keyring", "memory", "provider", 94 "wrapping", 95 ]) 96 ); 97 assert_eq!( 98 ROOT.lines() 99 .map(str::trim) 100 .filter(|line| line.starts_with("pub use ")) 101 .collect::<BTreeSet<_>>(), 102 BTreeSet::from([ 103 "pub use envelope::EncryptedEnvelope;", 104 "pub use error::Error;", 105 "pub use id::{SecretId, SecretRef};", 106 "pub use provider::SecretProvider;", 107 "pub use wrapping::KeyWrapping;" 108 ]) 109 ); 110 } 111 112 #[test] 113 fn package_documentation_covers_the_security_and_host_contract() { 114 for required in [ 115 "## Canonical surface", 116 "## Explicit provider and envelope flow", 117 "## Features and supported targets", 118 "## Security and serialization contract", 119 "## Side effects, cancellation, and commit points", 120 "## Intended consumers", 121 "public API baseline", 122 "implicit retry or fallback", 123 ] { 124 assert!(README.contains(required), "README is missing `{required}`"); 125 } 126 assert!(ROOT.contains("#![doc = include_str!(\"../README.md\")]")); 127 assert!(MANIFEST.contains("name = \"explicit_memory_provider\"")); 128 assert!(MANIFEST.contains("required-features = [\"memory\"]")); 129 for required in [ 130 "MemoryProvider::new()", 131 "provider.provision(", 132 "EncryptedEnvelope::seal", 133 "EncryptedEnvelope::decode", 134 "decoded.open(&provider, &context)", 135 ] { 136 assert!( 137 EXAMPLE.contains(required), 138 "example is missing `{required}`" 139 ); 140 } 141 } 142 143 fn table_keys<'a>(source: &'a str, table: &str) -> BTreeSet<&'a str> { 144 let Some((_, body)) = source.split_once(table) else { 145 return BTreeSet::new(); 146 }; 147 body.lines() 148 .skip(1) 149 .take_while(|line| !line.starts_with('[')) 150 .filter_map(|line| line.split_once('=').map(|(key, _)| key.trim())) 151 .filter(|key| { 152 !key.is_empty() 153 && key.chars().all(|character| { 154 character.is_ascii_alphanumeric() || matches!(character, '_' | '-') 155 }) 156 }) 157 .collect() 158 } 159 160 fn declarations<'a>(source: &'a str, prefix: &str) -> BTreeSet<&'a str> { 161 source 162 .lines() 163 .map(str::trim) 164 .filter_map(|line| line.strip_prefix(prefix)) 165 .filter_map(|line| line.strip_suffix(';')) 166 .collect() 167 } 168 169 fn collect_rust_sources(root: &Path, paths: &mut Vec<PathBuf>) { 170 for entry in fs::read_dir(root).expect("read source directory") { 171 let path = entry.expect("source entry").path(); 172 if path.is_dir() { 173 collect_rust_sources(&path, paths); 174 } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { 175 paths.push(path); 176 } 177 } 178 }