package_boundary.rs (7143B)
1 use std::{collections::BTreeSet, fs, path::Path}; 2 3 use radroots_blossom::{ 4 AuthorizationClaim, BlobDescriptor, BlobUrl, ByteVerifiedDescriptor, Error, MediaType, Sha256, 5 authorization::{ 6 AuthoredUploadClaim, AuthorizationAction, AuthorizationContent, AuthorizationTarget, 7 AuthorizationValidation, AuthorizationWireParts, ServerDomain, ServerScopeRequirement, 8 ValidatedAuthorizationClaim, 9 }, 10 descriptor::{ApprovedDescriptor, ByteCommitment}, 11 hash::{FileExtension, HashPath}, 12 url::ApprovedBlobUrl, 13 }; 14 15 const MANIFEST: &str = include_str!("../Cargo.toml"); 16 const ROOT: &str = include_str!("../src/lib.rs"); 17 18 #[test] 19 fn manifest_has_final_identity_features_and_no_radroots_dependencies() { 20 assert!(MANIFEST.contains("name = \"radroots_blossom\"")); 21 assert!(MANIFEST.contains("version = \"0.1.0-alpha\"")); 22 assert!(MANIFEST.contains("publish = [\"crates-io\"]")); 23 assert!(MANIFEST.contains("[lib]\nname = \"radroots_blossom\"")); 24 assert!(MANIFEST.contains("default = [\"std\", \"serde\"]")); 25 assert_eq!( 26 table_keys(MANIFEST, "[features]"), 27 BTreeSet::from(["default", "serde", "std"]) 28 ); 29 30 for heading in ["[dependencies]", "[dev-dependencies]"] { 31 assert!( 32 table_keys(MANIFEST, heading) 33 .iter() 34 .all(|dependency| !dependency.starts_with("radroots_")), 35 "{heading} must not contain Radroots dependencies" 36 ); 37 } 38 } 39 40 #[test] 41 fn manifest_dependency_surface_is_protocol_only() { 42 assert_eq!( 43 table_keys(MANIFEST, "[dependencies]"), 44 BTreeSet::from([ 45 "mediatype", 46 "serde", 47 "sha2", 48 "unicode-general-category", 49 "url_nostd", 50 ]) 51 ); 52 assert_eq!( 53 table_keys(MANIFEST, "[dev-dependencies]"), 54 BTreeSet::from(["hex", "serde_json"]) 55 ); 56 } 57 58 #[test] 59 fn crate_root_matches_the_approved_module_skeleton() { 60 assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]")); 61 assert_eq!( 62 root_declarations("pub mod "), 63 BTreeSet::from(["authorization", "descriptor", "hash", "media_type", "url"]) 64 ); 65 assert_eq!(root_declarations("mod "), BTreeSet::from(["error"])); 66 assert_eq!( 67 ROOT.lines() 68 .map(str::trim) 69 .filter(|line| line.starts_with("pub use ")) 70 .collect::<BTreeSet<_>>(), 71 BTreeSet::from([ 72 "pub use authorization::AuthorizationClaim;", 73 "pub use descriptor::BlobDescriptor;", 74 "pub use descriptor::ByteVerifiedDescriptor;", 75 "pub use error::Error;", 76 "pub use hash::Sha256;", 77 "pub use media_type::MediaType;", 78 "pub use url::BlobUrl;", 79 ]) 80 ); 81 } 82 83 #[test] 84 fn final_root_and_module_paths_compile() { 85 fn assert_public_value<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {} 86 87 assert_public_value::<ApprovedBlobUrl>(); 88 assert_public_value::<ApprovedDescriptor>(); 89 assert_public_value::<AuthoredUploadClaim>(); 90 assert_public_value::<AuthorizationAction>(); 91 assert_public_value::<AuthorizationClaim>(); 92 assert_public_value::<AuthorizationContent>(); 93 assert_public_value::<AuthorizationTarget>(); 94 assert_public_value::<AuthorizationValidation>(); 95 assert_public_value::<AuthorizationWireParts>(); 96 assert_public_value::<BlobDescriptor>(); 97 assert_public_value::<BlobUrl>(); 98 assert_public_value::<ByteCommitment>(); 99 assert_public_value::<ByteVerifiedDescriptor>(); 100 assert_public_value::<Error>(); 101 assert_public_value::<FileExtension>(); 102 assert_public_value::<HashPath>(); 103 assert_public_value::<MediaType>(); 104 assert_public_value::<ServerDomain>(); 105 assert_public_value::<ServerScopeRequirement>(); 106 assert_public_value::<Sha256>(); 107 assert_public_value::<ValidatedAuthorizationClaim>(); 108 } 109 110 #[test] 111 fn production_types_do_not_repeat_the_crate_name() { 112 for (path, production) in production_sources() { 113 assert!( 114 !production.contains("RadrootsBlossom"), 115 "Blossom production type repeats its crate name: {}", 116 path.display() 117 ); 118 } 119 } 120 121 #[test] 122 fn production_surface_exposes_no_public_traits() { 123 for (path, production) in production_sources() { 124 assert!( 125 !production 126 .lines() 127 .map(str::trim_start) 128 .any(|line| line.starts_with("pub trait ")), 129 "Blossom must expose concrete protocol values, not public traits: {}", 130 path.display() 131 ); 132 } 133 } 134 135 #[test] 136 fn production_surface_owns_no_io_or_application_media_policy() { 137 const FORBIDDEN_MARKERS: &[&str] = &[ 138 "std::fs", 139 "std::io", 140 "std::net", 141 "std::path", 142 "std::process", 143 "std::thread", 144 "std::time", 145 "tokio::", 146 "reqwest::", 147 "hyper::", 148 "ureq::", 149 "tower::", 150 "HttpClient", 151 "BlobClient", 152 "UploadClient", 153 "DownloadClient", 154 "UploadQueue", 155 "DownloadQueue", 156 "CachePolicy", 157 "FileCache", 158 "RetryPolicy", 159 "BackoffPolicy", 160 "MediaPolicy", 161 "Filesystem", 162 "FileSystem", 163 ]; 164 165 for (path, production) in production_sources() { 166 for marker in FORBIDDEN_MARKERS { 167 assert!( 168 !production.contains(marker), 169 "Blossom production source owns forbidden I/O or application policy marker `{marker}`: {}", 170 path.display() 171 ); 172 } 173 } 174 } 175 176 fn production_sources() -> Vec<(std::path::PathBuf, String)> { 177 let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); 178 fs::read_dir(source_root) 179 .expect("read Blossom source directory") 180 .map(|entry| entry.expect("source directory entry").path()) 181 .filter(|path| path.extension().and_then(|value| value.to_str()) == Some("rs")) 182 .map(|path| { 183 let source = fs::read_to_string(&path).expect("read Blossom source"); 184 let production = source 185 .split("\n#[cfg(test)]") 186 .next() 187 .unwrap_or(&source) 188 .to_owned(); 189 (path, production) 190 }) 191 .collect() 192 } 193 194 fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { 195 let table = manifest 196 .split_once(heading) 197 .unwrap_or_else(|| panic!("missing manifest table {heading}")) 198 .1; 199 table 200 .lines() 201 .skip(1) 202 .take_while(|line| !line.trim_start().starts_with('[')) 203 .filter_map(|line| { 204 let line = line.trim(); 205 (!line.is_empty() && !line.starts_with('#')) 206 .then(|| line.split_once('=').map(|(key, _)| key.trim())) 207 .flatten() 208 }) 209 .collect() 210 } 211 212 fn root_declarations(prefix: &str) -> BTreeSet<&str> { 213 ROOT.lines() 214 .map(str::trim) 215 .filter_map(|line| { 216 line.strip_prefix(prefix) 217 .and_then(|name| name.strip_suffix(';')) 218 }) 219 .collect() 220 }