From d1b881143279a27a44bb1ee6a3cbf8f326c6d6ad Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Sun, 28 Jun 2026 21:27:42 +0000 Subject: [PATCH] feat(core): add PeerEntry struct and replace AuthPolicy.authorized_fingerprints with peers (core/peer-entry-model) --- crates/alknet-core/src/auth.rs | 37 ++-- crates/alknet-core/src/config.rs | 304 +++++++++++++++++++++++++---- crates/alknet-core/src/endpoint.rs | 40 ++-- crates/alknet-core/src/types.rs | 45 +++-- 4 files changed, 349 insertions(+), 77 deletions(-) diff --git a/crates/alknet-core/src/auth.rs b/crates/alknet-core/src/auth.rs index 08f6e3b..75d872b 100644 --- a/crates/alknet-core/src/auth.rs +++ b/crates/alknet-core/src/auth.rs @@ -55,14 +55,14 @@ impl IdentityProvider for ConfigIdentityProvider { fn resolve_from_token(&self, token: &AuthToken) -> Option { let config = self.dynamic.load(); let token_str = String::from_utf8_lossy(&token.raw); - config.auth.resolve_api_key(&token_str) + config.auth.resolve_identity_from_token(&token_str) } } #[cfg(test)] mod tests { use super::*; - use crate::config::{ApiKeyEntry, AuthPolicy, DynamicConfig, RateLimitConfig}; + use crate::config::{ApiKeyEntry, AuthPolicy, DynamicConfig, PeerEntry, RateLimitConfig}; fn compute_api_key_hash(token: &str) -> String { use sha2::{Digest, Sha256}; @@ -80,12 +80,22 @@ mod tests { (provider, arc_swap) } - fn config_with_fingerprint(fingerprint: &str) -> DynamicConfig { - let mut fingerprints = std::collections::HashSet::new(); - fingerprints.insert(fingerprint.to_string()); + fn peer_entry_with_fingerprint(peer_id: &str, fingerprint: &str) -> PeerEntry { + PeerEntry { + peer_id: peer_id.to_string(), + fingerprints: vec![fingerprint.to_string()], + auth_token_hash: None, + scopes: vec!["relay:connect".to_string()], + resources: std::collections::HashMap::new(), + display_name: None, + enabled: true, + } + } + + fn config_with_fingerprint(peer_id: &str, fingerprint: &str) -> DynamicConfig { DynamicConfig { auth: AuthPolicy { - authorized_fingerprints: fingerprints, + peers: vec![peer_entry_with_fingerprint(peer_id, fingerprint)], api_keys: Vec::new(), }, rate_limits: RateLimitConfig::default(), @@ -95,7 +105,7 @@ mod tests { fn config_with_api_key(entry: ApiKeyEntry) -> DynamicConfig { DynamicConfig { auth: AuthPolicy { - authorized_fingerprints: std::collections::HashSet::new(), + peers: Vec::new(), api_keys: vec![entry], }, rate_limits: RateLimitConfig::default(), @@ -143,18 +153,18 @@ mod tests { #[test] fn fingerprint_resolution_known_returns_some() { - let (provider, _) = make_provider(config_with_fingerprint("SHA256:abc123")); + let (provider, _) = make_provider(config_with_fingerprint("worker-a", "SHA256:abc123")); let identity = provider .resolve_from_fingerprint("SHA256:abc123") .expect("known fingerprint resolves"); - assert_eq!(identity.id, "SHA256:abc123"); + assert_eq!(identity.id, "worker-a"); assert_eq!(identity.scopes, vec!["relay:connect".to_string()]); assert!(identity.resources.is_empty()); } #[test] fn fingerprint_resolution_unknown_returns_none() { - let (provider, _) = make_provider(config_with_fingerprint("SHA256:abc123")); + let (provider, _) = make_provider(config_with_fingerprint("worker-a", "SHA256:abc123")); assert!(provider .resolve_from_fingerprint("SHA256:unknown") .is_none()); @@ -256,7 +266,7 @@ mod tests { let (provider, arc_swap) = make_provider(DynamicConfig::default()); assert!(provider.resolve_from_fingerprint("SHA256:abc123").is_none()); - let new_config = config_with_fingerprint("SHA256:abc123"); + let new_config = config_with_fingerprint("worker-a", "SHA256:abc123"); arc_swap.store(Arc::new(new_config)); assert!(provider.resolve_from_fingerprint("SHA256:abc123").is_some()); @@ -264,7 +274,8 @@ mod tests { #[test] fn config_reload_removes_fingerprint_access_immediately() { - let (provider, arc_swap) = make_provider(config_with_fingerprint("SHA256:abc123")); + let (provider, arc_swap) = + make_provider(config_with_fingerprint("worker-a", "SHA256:abc123")); assert!(provider.resolve_from_fingerprint("SHA256:abc123").is_some()); arc_swap.store(Arc::new(DynamicConfig::default())); @@ -281,7 +292,7 @@ mod tests { assert!(provider.resolve_from_fingerprint("SHA256:abc123").is_none()); - handle.reload(config_with_fingerprint("SHA256:abc123")); + handle.reload(config_with_fingerprint("worker-a", "SHA256:abc123")); assert!(provider.resolve_from_fingerprint("SHA256:abc123").is_some()); } diff --git a/crates/alknet-core/src/config.rs b/crates/alknet-core/src/config.rs index c82adfa..3e1f506 100644 --- a/crates/alknet-core/src/config.rs +++ b/crates/alknet-core/src/config.rs @@ -7,7 +7,7 @@ //! `auth::ConfigIdentityProvider`. The remaining types (`StaticConfig`, //! `TlsIdentity`, `ConfigError`) are filled in by the core/config task. -use std::collections::HashSet; +use std::collections::HashMap; use std::io; use std::net::SocketAddr; use std::path::PathBuf; @@ -103,9 +103,20 @@ pub struct DynamicConfig { pub rate_limits: RateLimitConfig, } +#[derive(Debug, Clone, PartialEq)] +pub struct PeerEntry { + pub peer_id: String, + pub fingerprints: Vec, + pub auth_token_hash: Option, + pub scopes: Vec, + pub resources: HashMap>, + pub display_name: Option, + pub enabled: bool, +} + #[derive(Debug, Clone, Default)] pub struct AuthPolicy { - pub authorized_fingerprints: HashSet, + pub peers: Vec, pub api_keys: Vec, } @@ -124,15 +135,39 @@ impl AuthPolicy { } pub fn resolve_identity_from_fingerprint(&self, fingerprint: &str) -> Option { - if self.authorized_fingerprints.contains(fingerprint) { - Some(Identity { - id: fingerprint.to_string(), - scopes: vec!["relay:connect".to_string()], - resources: std::collections::HashMap::new(), + self.peers + .iter() + .find(|p| p.enabled && p.fingerprints.iter().any(|f| f == fingerprint)) + .map(|p| Identity { + id: p.peer_id.clone(), + scopes: p.scopes.clone(), + resources: p.resources.clone(), }) - } else { - None + } + + pub fn resolve_identity_from_token(&self, token: &str) -> Option { + let token_hash = sha256_hex(token); + self.peers + .iter() + .find(|p| p.enabled && p.auth_token_hash.as_deref() == Some(&token_hash)) + .map(|p| Identity { + id: p.peer_id.clone(), + scopes: p.scopes.clone(), + resources: p.resources.clone(), + }) + .or_else(|| self.resolve_api_key(token)) + } + + pub fn validate_peer_ids(&self) -> Result<(), DuplicatePeerId> { + let mut seen = std::collections::HashSet::new(); + for peer in &self.peers { + if !seen.insert(peer.peer_id.as_str()) { + return Err(DuplicatePeerId { + peer_id: peer.peer_id.clone(), + }); + } } + Ok(()) } pub fn resolve_api_key(&self, token: &str) -> Option { @@ -147,11 +182,7 @@ impl AuthPolicy { .iter() .find(|e| prefix_part.starts_with(&e.prefix))?; - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(token.as_bytes()); - let result = hasher.finalize(); - let expected_hash = format!("sha256:{}", hex::encode(result)); + let expected_hash = sha256_hex(token); if entry.hash != expected_hash { return None; @@ -175,6 +206,20 @@ impl AuthPolicy { } } +fn sha256_hex(input: &str) -> String { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + format!("sha256:{}", hex::encode(result)) +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("duplicate peer_id: {peer_id}")] +pub struct DuplicatePeerId { + pub peer_id: String, +} + #[derive(Debug, Clone)] pub struct RateLimitConfig { pub max_connections_per_ip: usize, @@ -249,7 +294,7 @@ mod tests { #[test] fn dynamic_config_default() { let cfg = DynamicConfig::default(); - assert!(cfg.auth.authorized_fingerprints.is_empty()); + assert!(cfg.auth.peers.is_empty()); assert!(cfg.auth.api_keys.is_empty()); assert_eq!(cfg.rate_limits.max_connections_per_ip, 100); assert_eq!(cfg.rate_limits.max_auth_attempts, 5); @@ -258,7 +303,7 @@ mod tests { #[test] fn auth_policy_default() { let policy = AuthPolicy::default(); - assert!(policy.authorized_fingerprints.is_empty()); + assert!(policy.peers.is_empty()); assert!(policy.api_keys.is_empty()); } @@ -311,12 +356,20 @@ mod tests { let handle = ConfigReloadHandle::new(dynamic.clone()); let initial = handle.dynamic(); - assert!(initial.auth.authorized_fingerprints.is_empty()); + assert!(initial.auth.peers.is_empty()); - let mut new_auth = AuthPolicy::default(); - new_auth - .authorized_fingerprints - .insert("aa:bb:cc".to_string()); + let new_auth = AuthPolicy { + peers: vec![PeerEntry { + peer_id: "worker-a".to_string(), + fingerprints: vec!["aa:bb:cc".to_string()], + auth_token_hash: None, + scopes: vec!["relay:connect".to_string()], + resources: HashMap::new(), + display_name: None, + enabled: true, + }], + api_keys: Vec::new(), + }; let new_config = DynamicConfig { auth: new_auth, rate_limits: RateLimitConfig::default(), @@ -324,8 +377,9 @@ mod tests { handle.reload(new_config); let after = handle.dynamic(); - assert!(after.auth.authorized_fingerprints.contains("aa:bb:cc")); - assert!(initial.auth.authorized_fingerprints.is_empty()); + assert_eq!(after.auth.peers.len(), 1); + assert_eq!(after.auth.peers[0].peer_id, "worker-a"); + assert!(initial.auth.peers.is_empty()); } #[test] @@ -378,11 +432,8 @@ mod tests { #[test] fn resolve_api_key_returns_empty_resources() { - use sha2::{Digest, Sha256}; let token = "alk_test_secret"; - let mut hasher = Sha256::new(); - hasher.update(token.as_bytes()); - let hash = format!("sha256:{}", hex::encode(hasher.finalize())); + let hash = sha256_hex(token); let entry = ApiKeyEntry { prefix: "alk_tes".to_string(), @@ -392,12 +443,15 @@ mod tests { expires_at: None, }; let policy = AuthPolicy { - authorized_fingerprints: HashSet::new(), + peers: Vec::new(), api_keys: vec![entry], }; let identity = policy.resolve_api_key(token); - assert!(identity.is_some(), "api key with matching prefix and hash should resolve"); + assert!( + identity.is_some(), + "api key with matching prefix and hash should resolve" + ); let identity = identity.unwrap(); assert_eq!(identity.id, "alk_tes"); assert_eq!(identity.scopes, vec!["admin"]); @@ -408,20 +462,198 @@ mod tests { } #[test] - fn resolve_identity_from_fingerprint_returns_empty_resources() { + fn resolve_identity_from_fingerprint_uses_peer_id() { let policy = AuthPolicy { - authorized_fingerprints: HashSet::from(["SHA256:known".to_string()]), + peers: vec![PeerEntry { + peer_id: "worker-a".to_string(), + fingerprints: vec!["SHA256:known".to_string()], + auth_token_hash: None, + scopes: vec!["relay:connect".to_string()], + resources: HashMap::new(), + display_name: None, + enabled: true, + }], api_keys: vec![], }; let identity = policy .resolve_identity_from_fingerprint("SHA256:known") .expect("known fingerprint should resolve"); - assert_eq!(identity.id, "SHA256:known"); - assert!( - identity.resources.is_empty(), - "fingerprint-resolved identities must have empty resources (Option B — scopes only)" - ); + assert_eq!(identity.id, "worker-a"); + assert_eq!(identity.scopes, vec!["relay:connect"]); + } + + // --- PeerEntry model (ADR-030) --------------------------------------- + + fn peer_entry(peer_id: &str, fingerprints: &[&str]) -> PeerEntry { + PeerEntry { + peer_id: peer_id.to_string(), + fingerprints: fingerprints.iter().map(|s| s.to_string()).collect(), + auth_token_hash: None, + scopes: vec!["relay:connect".to_string()], + resources: HashMap::new(), + display_name: None, + enabled: true, + } + } + + #[test] + fn fingerprint_resolution_known_returns_some_with_peer_id() { + let policy = AuthPolicy { + peers: vec![peer_entry("worker-a", &["ed25519:abc"])], + api_keys: vec![], + }; + let identity = policy + .resolve_identity_from_fingerprint("ed25519:abc") + .expect("known fingerprint resolves"); + assert_eq!(identity.id, "worker-a"); + assert_eq!(identity.scopes, vec!["relay:connect"]); + } + + #[test] + fn fingerprint_resolution_unknown_returns_none() { + let policy = AuthPolicy { + peers: vec![peer_entry("worker-a", &["ed25519:abc"])], + api_keys: vec![], + }; + assert!(policy + .resolve_identity_from_fingerprint("ed25519:unknown") + .is_none()); + } + + #[test] + fn fingerprint_resolution_disabled_returns_none() { + let mut entry = peer_entry("worker-a", &["ed25519:abc"]); + entry.enabled = false; + let policy = AuthPolicy { + peers: vec![entry], + api_keys: vec![], + }; + assert!(policy + .resolve_identity_from_fingerprint("ed25519:abc") + .is_none()); + } + + #[test] + fn token_resolution_matching_peer_returns_some_with_peer_id() { + let token = "bearer-secret"; + let mut entry = peer_entry("worker-a", &["ed25519:abc"]); + entry.auth_token_hash = Some(sha256_hex(token)); + let policy = AuthPolicy { + peers: vec![entry], + api_keys: vec![], + }; + let identity = policy + .resolve_identity_from_token(token) + .expect("matching auth_token_hash resolves"); + assert_eq!(identity.id, "worker-a"); + } + + #[test] + fn token_resolution_non_matching_falls_through_to_api_key() { + let api_token = "alk_test_secret"; + let mut entry = peer_entry("worker-a", &["ed25519:abc"]); + entry.auth_token_hash = Some(sha256_hex("different-token")); + let api_entry = ApiKeyEntry { + prefix: "alk_tes".to_string(), + hash: sha256_hex(api_token), + scopes: vec!["admin".to_string()], + description: "test key".to_string(), + expires_at: None, + }; + let policy = AuthPolicy { + peers: vec![entry], + api_keys: vec![api_entry], + }; + let identity = policy + .resolve_identity_from_token(api_token) + .expect("api key fall-through resolves"); + assert_eq!(identity.id, "alk_tes"); + assert_eq!(identity.scopes, vec!["admin"]); + } + + #[test] + fn token_resolution_no_match_returns_none() { + let policy = AuthPolicy { + peers: vec![peer_entry("worker-a", &["ed25519:abc"])], + api_keys: vec![], + }; + assert!(policy.resolve_identity_from_token("unknown").is_none()); + } + + #[test] + fn multi_fingerprint_peer_any_resolves_to_same_peer_id() { + let policy = AuthPolicy { + peers: vec![peer_entry("worker-a", &["ed25519:abc", "SHA256:def"])], + api_keys: vec![], + }; + let id1 = policy + .resolve_identity_from_fingerprint("ed25519:abc") + .expect("first fingerprint resolves"); + let id2 = policy + .resolve_identity_from_fingerprint("SHA256:def") + .expect("second fingerprint resolves"); + assert_eq!(id1.id, "worker-a"); + assert_eq!(id2.id, "worker-a"); + } + + #[test] + fn resources_populated_on_fingerprint_path() { + let mut resources = HashMap::new(); + resources.insert("service".to_string(), vec!["gitea".to_string()]); + let mut entry = peer_entry("worker-a", &["ed25519:abc"]); + entry.resources = resources.clone(); + let policy = AuthPolicy { + peers: vec![entry], + api_keys: vec![], + }; + let identity = policy + .resolve_identity_from_fingerprint("ed25519:abc") + .expect("known fingerprint resolves"); + assert_eq!(identity.resources, resources); + } + + #[test] + fn resources_populated_on_token_path() { + let token = "bearer-secret"; + let mut resources = HashMap::new(); + resources.insert("service".to_string(), vec!["gitea".to_string()]); + let mut entry = peer_entry("worker-a", &["ed25519:abc"]); + entry.auth_token_hash = Some(sha256_hex(token)); + entry.resources = resources.clone(); + let policy = AuthPolicy { + peers: vec![entry], + api_keys: vec![], + }; + let identity = policy + .resolve_identity_from_token(token) + .expect("matching token resolves"); + assert_eq!(identity.resources, resources); + } + + #[test] + fn duplicate_peer_id_validation_rejects() { + let policy = AuthPolicy { + peers: vec![ + peer_entry("worker-a", &["ed25519:abc"]), + peer_entry("worker-a", &["ed25519:def"]), + ], + api_keys: vec![], + }; + let err = policy.validate_peer_ids().expect_err("duplicate detected"); + assert_eq!(err.peer_id, "worker-a"); + } + + #[test] + fn unique_peer_ids_validate_ok() { + let policy = AuthPolicy { + peers: vec![ + peer_entry("worker-a", &["ed25519:abc"]), + peer_entry("worker-b", &["ed25519:def"]), + ], + api_keys: vec![], + }; + assert!(policy.validate_peer_ids().is_ok()); } // --- Ed25519SecretKey ------------------------------------------------- diff --git a/crates/alknet-core/src/endpoint.rs b/crates/alknet-core/src/endpoint.rs index eb7070e..fc1e9f2 100644 --- a/crates/alknet-core/src/endpoint.rs +++ b/crates/alknet-core/src/endpoint.rs @@ -140,8 +140,7 @@ impl AlknetEndpoint { )) })?; let tls_setup = TlsSetup::new(tls_identity, &alpns).await?; - let server_config = - build_quinn_server_config_from_rustls(tls_setup.server_config)?; + let server_config = build_quinn_server_config_from_rustls(tls_setup.server_config)?; let endpoint = quinn::Endpoint::server(server_config, listen_addr) .map_err(EndpointError::BindFailed)?; #[cfg(feature = "acme")] @@ -482,10 +481,7 @@ struct TlsSetup { } #[cfg(feature = "quinn")] impl TlsSetup { - async fn new( - tls_identity: &TlsIdentity, - alpns: &[Vec], - ) -> Result { + async fn new(tls_identity: &TlsIdentity, alpns: &[Vec]) -> Result { match tls_identity { TlsIdentity::Acme { domains, @@ -1084,7 +1080,9 @@ mod tests { async fn endpoint_constructs_with_iroh_raw_key_identity() { let static_config = StaticConfig { listen_addr: None, - tls_identity: Some(TlsIdentity::RawKey(crate::config::Ed25519SecretKey::generate())), + tls_identity: Some(TlsIdentity::RawKey( + crate::config::Ed25519SecretKey::generate(), + )), iroh_relay: None, drain_timeout: Duration::from_millis(10), }; @@ -1265,10 +1263,7 @@ mod tests { fn acme_directory_production_url() { use crate::config::AcmeDirectory; let dir = AcmeDirectory::Production; - assert_eq!( - dir.url(), - "https://acme-v02.api.letsencrypt.org/directory" - ); + assert_eq!(dir.url(), "https://acme-v02.api.letsencrypt.org/directory"); } #[test] @@ -1340,7 +1335,9 @@ mod tests { fn has_iroh_identity_true_for_raw_key() { let cfg = StaticConfig { listen_addr: None, - tls_identity: Some(TlsIdentity::RawKey(crate::config::Ed25519SecretKey::generate())), + tls_identity: Some(TlsIdentity::RawKey( + crate::config::Ed25519SecretKey::generate(), + )), iroh_relay: None, drain_timeout: Duration::from_millis(10), }; @@ -1437,7 +1434,9 @@ mod tests { #[cfg(feature = "quinn")] #[test] fn load_private_key_returns_error_when_file_missing() { - let err = load_private_key(std::path::Path::new("/nonexistent/alknet-coverage/missing.key")); + let err = load_private_key(std::path::Path::new( + "/nonexistent/alknet-coverage/missing.key", + )); assert!( matches!(err, Err(EndpointError::TlsConfig(_))), "missing key file must yield TlsConfig error, got {err:?}" @@ -1447,7 +1446,9 @@ mod tests { #[cfg(feature = "quinn")] #[test] fn load_cert_chain_returns_error_when_file_missing() { - let err = load_cert_chain(std::path::Path::new("/nonexistent/alknet-coverage/missing.pem")); + let err = load_cert_chain(std::path::Path::new( + "/nonexistent/alknet-coverage/missing.pem", + )); assert!( matches!(err, Err(EndpointError::TlsConfig(_))), "missing cert file must yield TlsConfig error, got {err:?}" @@ -1474,7 +1475,10 @@ mod tests { let verifier = AcceptAnyCertVerifier; let cert = CertificateDer::from(b"fake-cert-der".to_vec()); let result = verifier.verify_client_cert(&cert, &[], UnixTime::now()); - assert!(result.is_ok(), "AcceptAnyCertVerifier must accept any client cert"); + assert!( + result.is_ok(), + "AcceptAnyCertVerifier must accept any client cert" + ); } #[cfg(feature = "quinn")] @@ -1505,7 +1509,10 @@ mod tests { let sk = crate::config::Ed25519SecretKey::generate(); let signing_key = Ed25519SigningKey::new(sk); let signer = signing_key.choose_scheme(&[rustls::SignatureScheme::ED25519]); - assert!(signer.is_some(), "must produce a signer when ED25519 is offered"); + assert!( + signer.is_some(), + "must produce a signer when ED25519 is offered" + ); } #[cfg(feature = "quinn")] @@ -1581,6 +1588,7 @@ mod tests { let static_config = StaticConfig { listen_addr: None, tls_identity: Some(TlsIdentity::RawKey(sk)), + #[cfg(feature = "iroh")] iroh_relay: None, drain_timeout: Duration::from_millis(10), }; diff --git a/crates/alknet-core/src/types.rs b/crates/alknet-core/src/types.rs index 88cac39..2391dc8 100644 --- a/crates/alknet-core/src/types.rs +++ b/crates/alknet-core/src/types.rs @@ -691,11 +691,17 @@ mod tests { #[test] fn handler_error_display_covers_all_variants() { - assert_eq!(format!("{}", HandlerError::ConnectionClosed), "connection closed"); + assert_eq!( + format!("{}", HandlerError::ConnectionClosed), + "connection closed" + ); let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "boom"); let s = format!("{}", HandlerError::StreamError(io_err)); assert!(s.starts_with("stream error: ")); - assert_eq!(format!("{}", HandlerError::AuthRequired), "authentication required"); + assert_eq!( + format!("{}", HandlerError::AuthRequired), + "authentication required" + ); let inner: Box = "oops".into(); assert_eq!( format!("{}", HandlerError::Internal(inner)), @@ -708,11 +714,18 @@ mod tests { use std::error::Error; assert!(HandlerError::ConnectionClosed.source().is_none()); assert!(HandlerError::AuthRequired.source().is_none()); - let stream_err = HandlerError::StreamError(io::Error::new(io::ErrorKind::BrokenPipe, "boom")); - assert!(stream_err.source().is_some(), "StreamError must expose its io::Error as source"); + let stream_err = + HandlerError::StreamError(io::Error::new(io::ErrorKind::BrokenPipe, "boom")); + assert!( + stream_err.source().is_some(), + "StreamError must expose its io::Error as source" + ); let internal_inner: Box = "boom".into(); let internal_err = HandlerError::Internal(internal_inner); - assert!(internal_err.source().is_some(), "Internal must expose its inner error as source"); + assert!( + internal_err.source().is_some(), + "Internal must expose its inner error as source" + ); } #[test] @@ -725,14 +738,20 @@ mod tests { format!("{:?}", StreamError::StreamClosed), "StreamError::StreamClosed" ); - assert_eq!(format!("{:?}", StreamError::Timeout), "StreamError::Timeout"); + assert_eq!( + format!("{:?}", StreamError::Timeout), + "StreamError::Timeout" + ); let dbg = format!("{:?}", StreamError::Internal(io::Error::other("x"))); assert!(dbg.contains("StreamError::Internal")); } #[test] fn stream_error_display_covers_all_variants() { - assert_eq!(format!("{}", StreamError::ConnectionClosed), "connection closed"); + assert_eq!( + format!("{}", StreamError::ConnectionClosed), + "connection closed" + ); assert_eq!(format!("{}", StreamError::StreamClosed), "stream closed"); assert_eq!(format!("{}", StreamError::Timeout), "stream timed out"); assert_eq!( @@ -748,7 +767,10 @@ mod tests { assert!(StreamError::StreamClosed.source().is_none()); assert!(StreamError::Timeout.source().is_none()); let internal = StreamError::Internal(io::Error::other("x")); - assert!(internal.source().is_some(), "Internal must expose its io::Error as source"); + assert!( + internal.source().is_some(), + "Internal must expose its io::Error as source" + ); } // --- map_*_connection_error ------------------------------------------- @@ -817,12 +839,11 @@ mod tests { #[test] fn map_iroh_connection_error_application_closed_maps_to_connection_closed() { use bytes::Bytes; - let close = iroh::endpoint::ConnectionError::ApplicationClosed( - iroh::endpoint::ApplicationClose { + let close = + iroh::endpoint::ConnectionError::ApplicationClosed(iroh::endpoint::ApplicationClose { error_code: iroh::endpoint::VarInt::from_u32(1), reason: Bytes::new(), - }, - ); + }); assert!(matches!( map_iroh_connection_error(close), StreamError::ConnectionClosed