#![cfg(feature = "tokio")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use std::{net::Ipv4Addr, time::Duration};
use hick_reactor::{
Endpoint, Name, QueryEvent, QuerySpec, ServerOptions, ServiceRecords, ServiceSpec, ServiceUpdate,
wire::ResourceType,
};
fn loopback_index() -> Option<u32> {
let ifs = getifs::interfaces().ok()?;
ifs
.iter()
.find(|i| i.flags().contains(getifs::Flags::LOOPBACK))
.map(|i| i.index())
}
fn http_service(instance: &str) -> ServiceSpec {
let mut recs = ServiceRecords::new(
Name::try_from_str("_http._tcp.local.").unwrap(),
Name::try_from_str(&format!("{instance}._http._tcp.local.")).unwrap(),
Name::try_from_str(&format!("{instance}.local.")).unwrap(),
80,
120,
);
recs.add_a(Ipv4Addr::new(127, 0, 0, 1));
ServiceSpec::new(recs)
}
async fn loopback_v4_endpoint() -> Option<Endpoint> {
let idx = loopback_index()?;
let opts = ServerOptions::default()
.with_interface_index(Some(idx))
.with_ipv6(false);
match hick_reactor::tokio::server(opts).await {
Ok(ep) => Some(ep),
Err(e) => {
eprintln!("loopback multicast bind unavailable ({e}); skipping");
None
}
}
}
#[tokio::test]
async fn registered_service_reaches_advertised_state() {
let Some(ep) = loopback_v4_endpoint().await else {
return;
};
let svc = ep.register_service(http_service("alpha")).await.unwrap();
match tokio::time::timeout(Duration::from_secs(10), svc.next()).await {
Ok(Some(ServiceUpdate::Established)) => {}
Ok(Some(ServiceUpdate::Renamed(_))) => {}
Ok(Some(other)) => panic!("service failed to advertise: {other:?}"),
Ok(None) => panic!("service update channel closed before the service advertised"),
Err(_) => panic!("timed out waiting for the service to finish probing"),
}
}
#[tokio::test]
async fn browse_drives_both_run_loops() {
let Some(server) = loopback_v4_endpoint().await else {
return;
};
let Some(client) = loopback_v4_endpoint().await else {
return;
};
let _svc = server.register_service(http_service("beta")).await.unwrap();
let mut query = client
.start_query(QuerySpec::new(
Name::try_from_str("_http._tcp.local.").unwrap(),
ResourceType::Ptr,
))
.await
.unwrap();
match tokio::time::timeout(Duration::from_secs(8), query.next()).await {
Ok(Some(QueryEvent::Answer(a))) => {
assert_eq!(a.rtype(), ResourceType::Ptr);
}
Ok(Some(QueryEvent::Terminal(_))) | Ok(None) | Err(_) => {
eprintln!("no cross-socket loopback delivery here; run loops still exercised");
}
}
assert_eq!(query.dropped_answers(), 0);
}
#[tokio::test]
async fn cancel_query_and_unregister_service() {
let Some(ep) = loopback_v4_endpoint().await else {
return;
};
let query = ep
.start_query(QuerySpec::new(
Name::try_from_str("_absent._tcp.local.").unwrap(),
ResourceType::Ptr,
))
.await
.unwrap();
query.cancel().await.expect("driver still running");
let svc = ep.register_service(http_service("gamma")).await.unwrap();
svc.unregister().await.expect("driver still running");
}
#[tokio::test]
async fn server_rejects_no_family_enabled() {
let opts = ServerOptions::default().with_ipv4(false).with_ipv6(false);
match hick_reactor::tokio::server(opts).await {
Err(hick_reactor::ServerError::NoFamilyEnabled) => {}
Err(e) => panic!("expected NoFamilyEnabled, got {e:?}"),
Ok(_) => panic!("expected NoFamilyEnabled, but server() succeeded"),
}
}
#[tokio::test]
async fn dual_stack_loopback_exercises_v6_setup() {
let Some(idx) = loopback_index() else {
return;
};
let opts = ServerOptions::default().with_interface_index(Some(idx));
match hick_reactor::tokio::server(opts).await {
Ok(_ep) => {}
Err(hick_reactor::ServerError::BindV6(_)) => {}
Err(e) => panic!("unexpected dual-stack setup error: {e:?}"),
}
}