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
This commit is contained in:
deepseek-v4-pro committed 2026-07-17 10:53:25 +00:00
1 parent 2755b995c6
commit 5467c30892
12 files changed
+451 -7

No files matched your search

Generated
+1
View File
@@ -103,6 +103,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"alknet-core", "alknet-core",
"arc-swap", "arc-swap",
"async-trait",
"iroh", "iroh",
"quinn", "quinn",
"rustls", "rustls",
+3
View File
@@ -25,3 +25,6 @@ rustls = "0.23"
quinn = { version = "0.11", optional = true } quinn = { version = "0.11", optional = true }
iroh = { version = "1.0", optional = true, default-features = false, features = ["tls-aws-lc-rs"] } iroh = { version = "1.0", optional = true, default-features = false, features = ["tls-aws-lc-rs"] }
tokio-rustls = { version = "0.26", optional = true } tokio-rustls = { version = "0.26", optional = true }
[dev-dependencies]
async-trait = "0.1"
+162
View File
@@ -74,3 +74,165 @@ pub(crate) fn build_auth_context(
tls_client_fingerprint, 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<dyn alknet_core::types::ProtocolHandler> {
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<Identity> {
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<Identity> {
None
}
}
let provider: Arc<dyn IdentityProvider> = 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<Identity> {
None
}
fn resolve_from_token(&self, _token: &AuthToken) -> Option<Identity> {
None
}
}
let provider: Arc<dyn IdentityProvider> = 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<Identity> {
None
}
fn resolve_from_token(&self, _token: &AuthToken) -> Option<Identity> {
None
}
}
let provider: Arc<dyn IdentityProvider> = 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<Identity> {
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<Identity> {
None
}
}
let provider: Arc<dyn IdentityProvider> = 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");
}
}
+186
View File
@@ -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<dyn alknet_core::types::ProtocolHandler> {
Arc::new(DummyHandler { alpn })
}
struct NoProvider;
impl IdentityProvider for NoProvider {
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
None
}
fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> {
None
}
}
#[test]
fn debug_for_alknet_endpoint_is_implemented_without_panicking() {
let provider: Arc<dyn IdentityProvider> = 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<dyn IdentityProvider> = 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<dyn IdentityProvider> = 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<Mutex<bool>>,
}
#[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<dyn IdentityProvider> = 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<dyn IdentityProvider> = 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<dyn IdentityProvider> = 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());
}
}
+92
View File
@@ -59,3 +59,95 @@ impl std::fmt::Debug for HandlerRegistry {
.finish() .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<dyn ProtocolHandler> {
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::<Vec<_>>();
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"));
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/accept-iroh id: endpoint/accept-iroh
name: Implement iroh accept loop and extractors in alknet-endpoint name: Implement iroh accept loop and extractors in alknet-endpoint
status: pending status: completed
depends_on: [endpoint/dispatch] depends_on: [endpoint/dispatch]
scope: narrow scope: narrow
risk: low risk: low
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/accept-quinn id: endpoint/accept-quinn
name: Implement quinn accept loop and extractors in alknet-endpoint name: Implement quinn accept loop and extractors in alknet-endpoint
status: pending status: completed
depends_on: [endpoint/dispatch] depends_on: [endpoint/dispatch]
scope: narrow scope: narrow
risk: low risk: low
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/accept-tcp-tls id: endpoint/accept-tcp-tls
name: Implement TCP+TLS accept loop and extractors in alknet-endpoint (new code) name: Implement TCP+TLS accept loop and extractors in alknet-endpoint (new code)
status: pending status: completed
depends_on: [endpoint/dispatch] depends_on: [endpoint/dispatch]
scope: narrow scope: narrow
risk: medium risk: medium
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/crate-init id: endpoint/crate-init
name: Initialize alknet-endpoint crate with Cargo.toml, dependencies, and module skeleton name: Initialize alknet-endpoint crate with Cargo.toml, dependencies, and module skeleton
status: pending status: completed
depends_on: [tls/review-tls] depends_on: [tls/review-tls]
scope: moderate scope: moderate
risk: low risk: low
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/dispatch id: endpoint/dispatch
name: Implement public dispatch, build_auth_context, and ACME guard name: Implement public dispatch, build_auth_context, and ACME guard
status: pending status: completed
depends_on: [endpoint/endpoint-core] depends_on: [endpoint/endpoint-core]
scope: narrow scope: narrow
risk: low risk: low
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/endpoint-core id: endpoint/endpoint-core
name: Implement AlknetEndpoint struct, new, builder methods, run, and shutdown name: Implement AlknetEndpoint struct, new, builder methods, run, and shutdown
status: pending status: completed
depends_on: [endpoint/registry] depends_on: [endpoint/registry]
scope: moderate scope: moderate
risk: medium risk: medium
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: endpoint/registry id: endpoint/registry
name: Extract HandlerRegistry from alknet-core/endpoint.rs into alknet-endpoint name: Extract HandlerRegistry from alknet-core/endpoint.rs into alknet-endpoint
status: pending status: completed
depends_on: [endpoint/crate-init] depends_on: [endpoint/crate-init]
scope: narrow scope: narrow
risk: low risk: low