From 5467c308929680b457add3fb641c0a8308ac62aa Mon Sep 17 00:00:00 2001 From: deepseek-v4-pro Date: Fri, 17 Jul 2026 10:53:25 +0000 Subject: [PATCH] feat(endpoint): add tests and mark tasks 1-7 as completed - Add 7 registry tests (handler_registry_*) to registry.rs - Add 4 dispatch tests (build_auth_context_*, dispatch_decision_logic_*) to dispatch.rs - Add 6 endpoint tests to endpoint.rs: - debug_for_alknet_endpoint_is_implemented_without_panicking - endpoint_constructs_with_iroh_raw_key_identity (adapted for new API) - iroh_endpoint_runs_accept_loop_and_shutdown (adapted for new API) - with_iroh_sets_field (replaces has_iroh_identity_true_for_raw_key) - without_iroh_field_is_none (replaces has_iroh_identity_false_for_x509) - endpoint_works_without_iroh (replaces has_iroh_identity_false_when_no_identity) - Add async-trait as dev-dependency - All 17 tests pass with iroh feature - All 8 tests pass without features - Workspace tests all pass (no breakage) - Mark tasks 1-7 as completed --- Cargo.lock | 1 + crates/alknet-endpoint/Cargo.toml | 3 + crates/alknet-endpoint/src/dispatch.rs | 162 +++++++++++++++++++++ crates/alknet-endpoint/src/endpoint.rs | 186 +++++++++++++++++++++++++ crates/alknet-endpoint/src/registry.rs | 92 ++++++++++++ tasks/endpoint/accept-iroh.md | 2 +- tasks/endpoint/accept-quinn.md | 2 +- tasks/endpoint/accept-tcp-tls.md | 2 +- tasks/endpoint/crate-init.md | 2 +- tasks/endpoint/dispatch.md | 2 +- tasks/endpoint/endpoint-core.md | 2 +- tasks/endpoint/registry.md | 2 +- 12 files changed, 451 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65bc56b..f0d7f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ version = "0.1.0" dependencies = [ "alknet-core", "arc-swap", + "async-trait", "iroh", "quinn", "rustls", diff --git a/crates/alknet-endpoint/Cargo.toml b/crates/alknet-endpoint/Cargo.toml index 9994895..40410dc 100644 --- a/crates/alknet-endpoint/Cargo.toml +++ b/crates/alknet-endpoint/Cargo.toml @@ -25,3 +25,6 @@ rustls = "0.23" quinn = { version = "0.11", optional = true } iroh = { version = "1.0", optional = true, default-features = false, features = ["tls-aws-lc-rs"] } tokio-rustls = { version = "0.26", optional = true } + +[dev-dependencies] +async-trait = "0.1" diff --git a/crates/alknet-endpoint/src/dispatch.rs b/crates/alknet-endpoint/src/dispatch.rs index 617351d..d8ef072 100644 --- a/crates/alknet-endpoint/src/dispatch.rs +++ b/crates/alknet-endpoint/src/dispatch.rs @@ -74,3 +74,165 @@ pub(crate) fn build_auth_context( tls_client_fingerprint, } } + +#[cfg(test)] +mod tests { + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use super::build_auth_context; + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use std::collections::HashMap; + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use std::sync::Arc; + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use alknet_core::auth::{AuthToken, Identity, IdentityProvider}; + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use alknet_core::types::{Connection, HandlerError}; + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use async_trait::async_trait; + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + use crate::registry::HandlerRegistry; + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + struct DummyHandler { + alpn: &'static [u8], + } + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + #[async_trait] + impl alknet_core::types::ProtocolHandler for DummyHandler { + fn alpn(&self) -> &'static [u8] { + self.alpn + } + async fn handle( + &self, + _connection: Connection, + _auth: &alknet_core::auth::AuthContext, + ) -> Result<(), HandlerError> { + Ok(()) + } + } + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + fn make_handler(alpn: &'static [u8]) -> Arc { + Arc::new(DummyHandler { alpn }) + } + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + #[test] + fn build_auth_context_resolves_identity_from_fingerprint() { + struct StaticProvider; + impl IdentityProvider for StaticProvider { + fn resolve_from_fingerprint(&self, fp: &str) -> Option { + if fp == "SHA256:known" { + Some(Identity { + id: "SHA256:known".to_string(), + scopes: vec![], + resources: HashMap::new(), + }) + } else { + None + } + } + fn resolve_from_token(&self, _token: &AuthToken) -> Option { + None + } + } + let provider: Arc = Arc::new(StaticProvider); + let auth = build_auth_context( + b"alknet/test", + None, + Some("SHA256:known".to_string()), + &provider, + ); + assert_eq!(auth.identity.as_ref().unwrap().id, "SHA256:known"); + assert_eq!(auth.alpn, b"alknet/test"); + assert_eq!(auth.tls_client_fingerprint.as_deref(), Some("SHA256:known")); + } + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + #[test] + fn build_auth_context_no_fingerprint_no_identity() { + struct NoProvider; + impl IdentityProvider for NoProvider { + fn resolve_from_fingerprint(&self, _fp: &str) -> Option { + None + } + fn resolve_from_token(&self, _token: &AuthToken) -> Option { + None + } + } + let provider: Arc = Arc::new(NoProvider); + let auth = build_auth_context(b"alknet/test", None, None, &provider); + assert!(auth.identity.is_none()); + assert!(auth.tls_client_fingerprint.is_none()); + } + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + #[test] + fn build_auth_context_fingerprint_unknown_identity_none() { + struct StaticProvider; + impl IdentityProvider for StaticProvider { + fn resolve_from_fingerprint(&self, _fp: &str) -> Option { + None + } + fn resolve_from_token(&self, _token: &AuthToken) -> Option { + None + } + } + let provider: Arc = Arc::new(StaticProvider); + let auth = build_auth_context( + b"alknet/test", + None, + Some("SHA256:unknown".to_string()), + &provider, + ); + assert!(auth.identity.is_none()); + assert!(auth.tls_client_fingerprint.is_some()); + } + + #[cfg(any(feature = "quinn", feature = "iroh", feature = "tcp"))] + #[test] + fn dispatch_decision_logic_lookup_and_auth() { + let mut registry = HandlerRegistry::new(); + registry.register(make_handler(b"alknet/ssh")); + registry.register(make_handler(b"alknet/call")); + + struct StaticProvider; + impl IdentityProvider for StaticProvider { + fn resolve_from_fingerprint(&self, fp: &str) -> Option { + if fp == "SHA256:caller" { + Some(Identity { + id: "SHA256:caller".to_string(), + scopes: vec!["relay:connect".to_string()], + resources: HashMap::new(), + }) + } else { + None + } + } + fn resolve_from_token(&self, _: &AuthToken) -> Option { + None + } + } + let provider: Arc = Arc::new(StaticProvider); + + let ssh_handler = registry.get(b"alknet/ssh").expect("ssh handler registered"); + assert_eq!(ssh_handler.alpn(), b"alknet/ssh"); + let auth = build_auth_context( + b"alknet/ssh", + Some(std::net::SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), + 1234, + )), + Some("SHA256:caller".to_string()), + &provider, + ); + assert_eq!(auth.identity.as_ref().unwrap().id, "SHA256:caller"); + assert_eq!(auth.alpn, b"alknet/ssh"); + + let unknown = registry.get(b"alknet/unknown"); + assert!(unknown.is_none(), "unknown ALPN has no handler"); + } +} diff --git a/crates/alknet-endpoint/src/endpoint.rs b/crates/alknet-endpoint/src/endpoint.rs index 245d530..faab3ff 100644 --- a/crates/alknet-endpoint/src/endpoint.rs +++ b/crates/alknet-endpoint/src/endpoint.rs @@ -176,3 +176,189 @@ impl AlknetEndpoint { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::time::Duration; + + use alknet_core::auth::{AuthToken, Identity, IdentityProvider}; + use alknet_core::config::DynamicConfig; + #[cfg(feature = "iroh")] + use alknet_core::auth::AuthContext; + #[cfg(feature = "iroh")] + use alknet_core::types::{Connection, HandlerError}; + #[cfg(feature = "iroh")] + use async_trait::async_trait; + + #[cfg(feature = "iroh")] + struct DummyHandler { + alpn: &'static [u8], + } + + #[cfg(feature = "iroh")] + #[async_trait] + impl alknet_core::types::ProtocolHandler for DummyHandler { + fn alpn(&self) -> &'static [u8] { + self.alpn + } + async fn handle( + &self, + _connection: Connection, + _auth: &AuthContext, + ) -> Result<(), HandlerError> { + Ok(()) + } + } + + #[cfg(feature = "iroh")] + fn make_handler(alpn: &'static [u8]) -> Arc { + Arc::new(DummyHandler { alpn }) + } + + struct NoProvider; + impl IdentityProvider for NoProvider { + fn resolve_from_fingerprint(&self, _: &str) -> Option { + None + } + fn resolve_from_token(&self, _: &AuthToken) -> Option { + None + } + } + + #[test] + fn debug_for_alknet_endpoint_is_implemented_without_panicking() { + let provider: Arc = Arc::new(NoProvider); + let dynamic = Arc::new(ArcSwap::from_pointee(DynamicConfig::default())); + let registry = HandlerRegistry::new(); + let endpoint = AlknetEndpoint::new(registry, dynamic, provider, Duration::from_millis(10)); + let s = format!("{endpoint:?}"); + assert!(s.contains("AlknetEndpoint")); + assert!(s.contains("drain_timeout")); + } + + #[cfg(feature = "iroh")] + #[tokio::test] + async fn endpoint_constructs_with_iroh_raw_key_identity() { + let provider: Arc = Arc::new(NoProvider); + let dynamic = Arc::new(ArcSwap::from_pointee(DynamicConfig::default())); + let mut registry = HandlerRegistry::new(); + registry.register(make_handler(b"alknet/test")); + + let iroh_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal) + .secret_key(iroh::SecretKey::generate()) + .alpns(vec![b"alknet/test".to_vec()]) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await + .expect("iroh endpoint binds"); + + let endpoint = AlknetEndpoint::new(registry, dynamic, provider, Duration::from_millis(10)) + .with_iroh(iroh_endpoint); + assert!(endpoint.shutdown_sender().send(true).is_ok()); + endpoint.shutdown().await; + } + + #[cfg(feature = "iroh")] + #[tokio::test] + async fn iroh_endpoint_runs_accept_loop_and_shutdown() { + use std::sync::Mutex; + let provider: Arc = Arc::new(NoProvider); + let dynamic = Arc::new(ArcSwap::from_pointee(DynamicConfig::default())); + + let connected = Arc::new(Mutex::new(false)); + let connected_clone = connected.clone(); + struct CountingHandler { + alpn: &'static [u8], + connected: Arc>, + } + #[async_trait] + impl alknet_core::types::ProtocolHandler for CountingHandler { + fn alpn(&self) -> &'static [u8] { + self.alpn + } + async fn handle( + &self, + _conn: Connection, + _auth: &AuthContext, + ) -> Result<(), HandlerError> { + *self.connected.lock().unwrap() = true; + Ok(()) + } + } + let mut registry = HandlerRegistry::new(); + registry.register(Arc::new(CountingHandler { + alpn: b"alknet/test", + connected: connected_clone, + })); + + let iroh_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal) + .secret_key(iroh::SecretKey::generate()) + .alpns(vec![b"alknet/test".to_vec()]) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await + .expect("iroh endpoint binds"); + + let endpoint = Arc::new( + AlknetEndpoint::new(registry, dynamic, provider, Duration::from_millis(20)) + .with_iroh(iroh_endpoint), + ); + + let run_endpoint = endpoint.clone(); + let run_task = tokio::spawn(async move { + run_endpoint.run().await; + }); + + let _ = endpoint.shutdown_sender().send(true); + endpoint.shutdown().await; + let _ = run_task.await; + assert!(!*connected.lock().unwrap()); + } + + #[cfg(feature = "iroh")] + #[test] + fn with_iroh_sets_field() { + let provider: Arc = Arc::new(NoProvider); + let dynamic = Arc::new(ArcSwap::from_pointee(DynamicConfig::default())); + let registry = HandlerRegistry::new(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let iroh_endpoint = rt.block_on(async { + iroh::Endpoint::builder(iroh::endpoint::presets::Minimal) + .secret_key(iroh::SecretKey::generate()) + .alpns(vec![b"alknet/test".to_vec()]) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await + .expect("iroh endpoint binds") + }); + + let endpoint = AlknetEndpoint::new(registry, dynamic, provider, Duration::from_millis(10)) + .with_iroh(iroh_endpoint); + assert!(endpoint.iroh.is_some()); + } + + #[cfg(feature = "iroh")] + #[test] + fn without_iroh_field_is_none() { + let provider: Arc = Arc::new(NoProvider); + let dynamic = Arc::new(ArcSwap::from_pointee(DynamicConfig::default())); + let registry = HandlerRegistry::new(); + let endpoint = AlknetEndpoint::new(registry, dynamic, provider, Duration::from_millis(10)); + assert!(endpoint.iroh.is_none()); + } + + #[cfg(feature = "iroh")] + #[test] + fn endpoint_works_without_iroh() { + let provider: Arc = Arc::new(NoProvider); + let dynamic = Arc::new(ArcSwap::from_pointee(DynamicConfig::default())); + let mut registry = HandlerRegistry::new(); + registry.register(make_handler(b"alknet/test")); + let endpoint = AlknetEndpoint::new(registry, dynamic, provider, Duration::from_millis(10)); + assert!(endpoint.iroh.is_none()); + assert!(endpoint.shutdown_sender().send(true).is_ok()); + } +} diff --git a/crates/alknet-endpoint/src/registry.rs b/crates/alknet-endpoint/src/registry.rs index 6e09a54..5d1a370 100644 --- a/crates/alknet-endpoint/src/registry.rs +++ b/crates/alknet-endpoint/src/registry.rs @@ -59,3 +59,95 @@ impl std::fmt::Debug for HandlerRegistry { .finish() } } + +#[cfg(test)] +mod tests { + use super::*; + use alknet_core::auth::AuthContext; + use alknet_core::types::{Connection, HandlerError}; + use async_trait::async_trait; + + struct DummyHandler { + alpn: &'static [u8], + } + + #[async_trait] + impl ProtocolHandler for DummyHandler { + fn alpn(&self) -> &'static [u8] { + self.alpn + } + async fn handle( + &self, + _connection: Connection, + _auth: &AuthContext, + ) -> Result<(), HandlerError> { + Ok(()) + } + } + + fn make_handler(alpn: &'static [u8]) -> Arc { + Arc::new(DummyHandler { alpn }) + } + + #[test] + fn handler_registry_new_is_empty() { + let reg = HandlerRegistry::new(); + assert!(reg.alpn_strings().is_empty()); + assert!(reg.get(b"alknet/test").is_none()); + } + + #[test] + fn handler_registry_register_then_get() { + let mut reg = HandlerRegistry::new(); + reg.register(make_handler(b"alknet/test")); + assert_eq!(reg.alpn_strings(), vec![b"alknet/test".to_vec()]); + assert!(reg.get(b"alknet/test").is_some()); + assert!(reg.get(b"alknet/other").is_none()); + } + + #[test] + fn handler_registry_multiple_alpns() { + let mut reg = HandlerRegistry::new(); + reg.register(make_handler(b"alknet/ssh")); + reg.register(make_handler(b"alknet/call")); + let mut alpns = reg + .alpn_strings() + .into_iter() + .map(|a| String::from_utf8(a).unwrap()) + .collect::>(); + alpns.sort(); + assert_eq!(alpns, vec!["alknet/call", "alknet/ssh"]); + assert!(reg.get(b"alknet/ssh").is_some()); + assert!(reg.get(b"alknet/call").is_some()); + } + + #[test] + #[should_panic(expected = "ALPN already registered")] + fn handler_registry_register_panics_on_duplicate() { + let mut reg = HandlerRegistry::new(); + reg.register(make_handler(b"alknet/test")); + reg.register(make_handler(b"alknet/test")); + } + + #[test] + fn handler_registry_debug_lists_alpns() { + let mut reg = HandlerRegistry::new(); + reg.register(make_handler(b"alknet/test")); + let s = format!("{:?}", reg); + assert!(s.contains("alknet/test")); + } + + #[test] + fn handler_registry_default_is_empty() { + let reg = HandlerRegistry::default(); + assert!(reg.alpn_strings().is_empty()); + assert!(reg.get(b"alknet/test").is_none()); + } + + #[test] + fn handler_registry_debug_lists_alpns_via_default() { + let reg = HandlerRegistry::default(); + let s = format!("{reg:?}"); + assert!(s.contains("HandlerRegistry")); + } +} diff --git a/tasks/endpoint/accept-iroh.md b/tasks/endpoint/accept-iroh.md index 5b4f89b..1ef13c3 100644 --- a/tasks/endpoint/accept-iroh.md +++ b/tasks/endpoint/accept-iroh.md @@ -1,7 +1,7 @@ --- id: endpoint/accept-iroh name: Implement iroh accept loop and extractors in alknet-endpoint -status: pending +status: completed depends_on: [endpoint/dispatch] scope: narrow risk: low diff --git a/tasks/endpoint/accept-quinn.md b/tasks/endpoint/accept-quinn.md index ff989f3..8d71b80 100644 --- a/tasks/endpoint/accept-quinn.md +++ b/tasks/endpoint/accept-quinn.md @@ -1,7 +1,7 @@ --- id: endpoint/accept-quinn name: Implement quinn accept loop and extractors in alknet-endpoint -status: pending +status: completed depends_on: [endpoint/dispatch] scope: narrow risk: low diff --git a/tasks/endpoint/accept-tcp-tls.md b/tasks/endpoint/accept-tcp-tls.md index c16d6f4..de96abb 100644 --- a/tasks/endpoint/accept-tcp-tls.md +++ b/tasks/endpoint/accept-tcp-tls.md @@ -1,7 +1,7 @@ --- id: endpoint/accept-tcp-tls name: Implement TCP+TLS accept loop and extractors in alknet-endpoint (new code) -status: pending +status: completed depends_on: [endpoint/dispatch] scope: narrow risk: medium diff --git a/tasks/endpoint/crate-init.md b/tasks/endpoint/crate-init.md index 20e8280..23ceb8c 100644 --- a/tasks/endpoint/crate-init.md +++ b/tasks/endpoint/crate-init.md @@ -1,7 +1,7 @@ --- id: endpoint/crate-init name: Initialize alknet-endpoint crate with Cargo.toml, dependencies, and module skeleton -status: pending +status: completed depends_on: [tls/review-tls] scope: moderate risk: low diff --git a/tasks/endpoint/dispatch.md b/tasks/endpoint/dispatch.md index 128af22..9902518 100644 --- a/tasks/endpoint/dispatch.md +++ b/tasks/endpoint/dispatch.md @@ -1,7 +1,7 @@ --- id: endpoint/dispatch name: Implement public dispatch, build_auth_context, and ACME guard -status: pending +status: completed depends_on: [endpoint/endpoint-core] scope: narrow risk: low diff --git a/tasks/endpoint/endpoint-core.md b/tasks/endpoint/endpoint-core.md index 71a9a21..43ae6d5 100644 --- a/tasks/endpoint/endpoint-core.md +++ b/tasks/endpoint/endpoint-core.md @@ -1,7 +1,7 @@ --- id: endpoint/endpoint-core name: Implement AlknetEndpoint struct, new, builder methods, run, and shutdown -status: pending +status: completed depends_on: [endpoint/registry] scope: moderate risk: medium diff --git a/tasks/endpoint/registry.md b/tasks/endpoint/registry.md index 0489d8d..5984b51 100644 --- a/tasks/endpoint/registry.md +++ b/tasks/endpoint/registry.md @@ -1,7 +1,7 @@ --- id: endpoint/registry name: Extract HandlerRegistry from alknet-core/endpoint.rs into alknet-endpoint -status: pending +status: completed depends_on: [endpoint/crate-init] scope: narrow risk: low