package_boundary.rs (10911B)
1 use std::collections::BTreeSet; 2 3 use radroots_nostr_connect::{ 4 BunkerUri, Client, ClientUri, Error, Method, Permission, Request, Response, Server, 5 client::Transport, 6 }; 7 8 const MANIFEST: &str = include_str!("../Cargo.toml"); 9 const README: &str = include_str!("../README.md"); 10 const EXAMPLE: &str = include_str!("../examples/prepare_request.rs"); 11 const PUBLIC_API: &str = 12 include_str!("../../../contracts/api_baselines/radroots_nostr_connect.txt"); 13 const CLIENT: &str = include_str!("../src/client.rs"); 14 const METHOD: &str = include_str!("../src/method.rs"); 15 const PERMISSION: &str = include_str!("../src/permission.rs"); 16 const ROOT: &str = include_str!("../src/lib.rs"); 17 const SERVER: &str = include_str!("../src/server.rs"); 18 const URI: &str = include_str!("../src/uri.rs"); 19 20 #[test] 21 fn manifest_has_final_identity_feature_vocabulary_and_radroots_dependencies() { 22 for required in [ 23 "name = \"radroots_nostr_connect\"", 24 "version = \"0.1.0-alpha\"", 25 "publish = [\"crates-io\"]", 26 "[lib]\nname = \"radroots_nostr_connect\"", 27 "default = [\"serde\"]", 28 "radroots_event = { workspace = true, default-features = false }", 29 "radroots_identity = { workspace = true, default-features = false }", 30 "radroots_nostr = { workspace = true, default-features = false }", 31 "radroots_protocol = { workspace = true, default-features = false }", 32 ] { 33 assert!( 34 MANIFEST.contains(required), 35 "manifest is missing `{required}`" 36 ); 37 } 38 39 assert_eq!( 40 table_keys(MANIFEST, "[features]"), 41 BTreeSet::from(["default", "serde"]) 42 ); 43 assert_eq!( 44 table_keys(MANIFEST, "[dependencies]") 45 .into_iter() 46 .filter(|dependency| dependency.starts_with("radroots_")) 47 .collect::<BTreeSet<_>>(), 48 BTreeSet::from([ 49 "radroots_event", 50 "radroots_identity", 51 "radroots_nostr", 52 "radroots_protocol", 53 ]) 54 ); 55 assert_eq!( 56 table_keys(MANIFEST, "[dependencies]"), 57 BTreeSet::from([ 58 "nostr", 59 "radroots_event", 60 "radroots_identity", 61 "radroots_nostr", 62 "radroots_protocol", 63 "serde", 64 "serde_json", 65 "thiserror", 66 "url", 67 ]) 68 ); 69 assert_eq!( 70 table_keys(MANIFEST, "[dev-dependencies]"), 71 BTreeSet::from(["tokio"]) 72 ); 73 for forbidden in [ 74 "keyring", 75 "nostr-sdk", 76 "reqwest", 77 "sqlx", 78 "radroots_secrets", 79 "radroots_storage", 80 "radroots_transport_nostr", 81 ] { 82 assert!( 83 !table_keys(MANIFEST, "[dependencies]").contains(forbidden), 84 "protocol package acquired forbidden production dependency `{forbidden}`" 85 ); 86 } 87 } 88 89 #[test] 90 fn crate_root_contains_the_approved_module_skeleton() { 91 assert_eq!( 92 declarations(ROOT, "pub mod "), 93 BTreeSet::from([ 94 "client", 95 "error", 96 "message", 97 "method", 98 "permission", 99 "server", 100 "uri", 101 ]) 102 ); 103 assert_eq!( 104 ROOT.lines() 105 .map(str::trim) 106 .filter(|line| line.starts_with("pub use ")) 107 .collect::<BTreeSet<_>>(), 108 BTreeSet::from([ 109 "pub use client::Client;", 110 "pub use error::RadrootsNostrConnectError as Error;", 111 "pub use message::{Request, Response};", 112 "pub use method::Method;", 113 "pub use permission::Permission;", 114 "pub use server::Server;", 115 "pub use uri::{BunkerUri, ClientUri};", 116 ]) 117 ); 118 119 assert!(SERVER.starts_with("//! Relay- and persistence-independent NIP-46 server state.")); 120 assert!(ROOT.contains("pub use server::Server;")); 121 assert!(!ROOT.contains("pub mod prelude")); 122 } 123 124 #[test] 125 fn approved_root_exports_and_transport_trait_compile() { 126 fn assert_value<T: Send + Sync>() {} 127 fn assert_error<T: core::error::Error + Send + Sync>() {} 128 fn accept_transport(_: &mut dyn Transport) {} 129 130 assert_value::<BunkerUri>(); 131 assert_value::<Client>(); 132 assert_value::<ClientUri>(); 133 assert_value::<Method>(); 134 assert_value::<Permission>(); 135 assert_value::<Request>(); 136 assert_value::<Response>(); 137 assert_value::<Server>(); 138 assert_error::<Error>(); 139 let _ = accept_transport; 140 141 let public_traits = CLIENT 142 .lines() 143 .map(str::trim) 144 .filter(|line| line.starts_with("pub trait ")) 145 .collect::<BTreeSet<_>>(); 146 assert_eq!( 147 public_traits, 148 BTreeSet::from(["pub trait Transport: Send {"]), 149 "only the final host transport SPI may remain" 150 ); 151 for forbidden in [ 152 "Relay", "Runtime", "Session", "Storage", "Secret", "Approval", 153 ] { 154 assert!( 155 public_traits 156 .iter() 157 .all(|trait_line| !trait_line.contains(forbidden)), 158 "protocol package exposes forbidden owner trait containing `{forbidden}`" 159 ); 160 } 161 } 162 163 #[test] 164 fn uri_method_and_permission_types_use_canonical_owners_and_names() { 165 for root_export in [ 166 "pub use method::Method;", 167 "pub use permission::Permission;", 168 "pub use uri::{BunkerUri, ClientUri};", 169 ] { 170 assert!(ROOT.contains(root_export), "missing `{root_export}`"); 171 } 172 for forbidden in [ 173 "pub enum RadrootsNostrConnectMethod", 174 "pub struct RadrootsNostrConnectPermission", 175 "pub struct RadrootsNostrConnectPermissions", 176 ] { 177 assert!(!METHOD.contains(forbidden)); 178 assert!(!PERMISSION.contains(forbidden)); 179 } 180 for forbidden in [ 181 "pub struct RadrootsNostrConnectBunkerUri", 182 "pub struct RadrootsNostrConnectClientUri", 183 "pub enum RadrootsNostrConnectUri", 184 "use nostr::{PublicKey", 185 ] { 186 assert!(!URI.contains(forbidden), "URI source retains `{forbidden}`"); 187 } 188 assert!(URI.contains("use radroots_identity::PublicKey;")); 189 assert!(URI.contains("radroots_nostr::key::parse_public_key")); 190 } 191 192 #[test] 193 fn client_root_and_transport_use_package_owned_state_machine_types() { 194 assert!(ROOT.contains("pub use client::Client;")); 195 for required in [ 196 "pub struct Client {", 197 "pub struct ClientEvent(Event);", 198 "pub struct Target {", 199 "pub trait Transport: Send {", 200 "pub enum CancellationPhase {", 201 ] { 202 assert!(CLIENT.contains(required), "client is missing `{required}`"); 203 } 204 for forbidden in [ 205 "pub struct Client {\n pub ", 206 "pub struct ClientEvent(pub ", 207 "pub struct Target {\n pub ", 208 ] { 209 assert!( 210 !CLIENT.contains(forbidden), 211 "client exposes representation through `{forbidden}`" 212 ); 213 } 214 } 215 216 #[test] 217 fn separate_repository_compatibility_surface_is_removed() { 218 assert!(MANIFEST.contains("publish = [\"crates-io\"]")); 219 for item in [ 220 "pub type RadrootsNostrConnectClientTransportFuture", 221 "pub struct RadrootsNostrConnectClientTarget", 222 "pub struct RadrootsNostrConnectClientRequest", 223 "pub enum RadrootsNostrConnectClientProgress", 224 "pub enum RadrootsNostrConnectClientEventOutcome", 225 "pub trait RadrootsNostrConnectClientTransport", 226 "pub fn build_request_event", 227 "pub fn parse_response_event", 228 "pub async fn execute_request_with_transport", 229 ] { 230 assert!( 231 !CLIENT.contains(item), 232 "compatibility item `{item}` must be absent" 233 ); 234 } 235 for legacy in [ 236 "RadrootsNostrConnectClient", 237 "radroots_nostr_connect::prelude", 238 ] { 239 assert!( 240 !PUBLIC_API.contains(legacy) && !ROOT.contains(legacy), 241 "compatibility identity `{legacy}` remains" 242 ); 243 } 244 } 245 246 #[test] 247 fn protocol_transport_boundary_has_no_relay_pool_or_runtime_owner() { 248 for forbidden in [ 249 "nostr-sdk", 250 "nostr_sdk", 251 "tokio::runtime", 252 "RelayPool", 253 "Client::start", 254 ] { 255 assert!( 256 !CLIENT.contains(forbidden) && !MANIFEST.contains(forbidden), 257 "protocol transport retains forbidden owner `{forbidden}`" 258 ); 259 } 260 assert!(CLIENT.contains("pub trait Transport: Send")); 261 assert!(CLIENT.contains("T: Transport + ?Sized")); 262 } 263 264 #[test] 265 fn predecessor_signer_package_is_removed() { 266 let crate_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); 267 assert!( 268 !crate_root 269 .parent() 270 .expect("crates root") 271 .join("nostr_signer") 272 .exists() 273 ); 274 } 275 276 #[test] 277 fn package_documentation_and_reviewed_api_baseline_cover_the_public_contract() { 278 for section in [ 279 "## Canonical surface", 280 "## Features and supported targets", 281 "## Serialization and compatibility", 282 "## Security, side effects, and commit points", 283 "## Intended consumers", 284 "## Package charter", 285 ] { 286 assert!(README.contains(section), "README is missing `{section}`"); 287 } 288 for required in [ 289 "Client::generate", 290 "Target::try_new", 291 "Request::Ping", 292 "operation.publication()", 293 ] { 294 assert!( 295 EXAMPLE.contains(required), 296 "example is missing `{required}`" 297 ); 298 } 299 for export in [ 300 "pub struct radroots_nostr_connect::Client", 301 "pub struct radroots_nostr_connect::Server", 302 "pub enum radroots_nostr_connect::Method", 303 "pub struct radroots_nostr_connect::Permission", 304 "pub enum radroots_nostr_connect::Request", 305 "pub enum radroots_nostr_connect::Response", 306 "pub struct radroots_nostr_connect::BunkerUri", 307 "pub struct radroots_nostr_connect::ClientUri", 308 "pub enum radroots_nostr_connect::Error", 309 ] { 310 assert!( 311 PUBLIC_API.contains(export), 312 "reviewed API baseline is missing `{export}`" 313 ); 314 } 315 } 316 317 fn table_keys<'a>(source: &'a str, header: &str) -> BTreeSet<&'a str> { 318 let Some((_, tail)) = source.split_once(header) else { 319 panic!("manifest is missing {header}"); 320 }; 321 322 tail.lines() 323 .skip(1) 324 .take_while(|line| !line.trim_start().starts_with('[')) 325 .filter_map(|line| { 326 let line = line.trim(); 327 if line.is_empty() || line.starts_with('#') || line.starts_with('"') { 328 return None; 329 } 330 line.split_once('=').map(|(key, _)| key.trim()) 331 }) 332 .collect() 333 } 334 335 fn declarations<'a>(source: &'a str, prefix: &str) -> BTreeSet<&'a str> { 336 source 337 .lines() 338 .map(str::trim) 339 .filter_map(|line| { 340 line.strip_prefix(prefix) 341 .and_then(|name| name.strip_suffix(';')) 342 }) 343 .collect() 344 }