use std::net::{IpAddr, TcpListener as StdTcpListener};
use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use smpp34::client::{SmppClient, SmppClientListener, BIND_TYPE};
use smpp34::{SmppServer, SmppServerListener};
use tokio::sync::mpsc;
fn closed_port() -> u16 {
let l = StdTcpListener::bind("127.0.0.1:0").expect("bind ephemeral");
let port = l.local_addr().expect("local_addr").port();
drop(l);
port
}
struct FailureRecorder {
failures: mpsc::Sender<String>,
}
#[async_trait]
impl SmppClientListener for FailureRecorder {
async fn on_connection_failed(&self, error: &str) {
let _ = self.failures.send(error.to_string()).await;
}
}
#[async_trait]
impl SmppServerListener for FailureRecorder {
async fn on_listen_failed(&self, error: &str) {
let _ = self.failures.send(error.to_string()).await;
}
}
fn client_to(port: u16, listener: Arc<FailureRecorder>) -> SmppClient {
SmppClient::new(
"127.0.0.1".to_string(),
port,
false,
BIND_TYPE::TRX,
"u".to_string(),
"p".to_string(),
String::new(),
1,
1,
String::new(),
listener,
16,
)
}
#[tokio::test]
async fn a_refused_connect_reports_its_real_cause() {
let port = closed_port();
let (tx, mut rx) = mpsc::channel(4);
let mut client = client_to(port, Arc::new(FailureRecorder { failures: tx }));
client.start().await;
let reason = tokio::time::timeout(Duration::from_secs(5), rx.recv())
.await
.expect("on_connection_failed was never called")
.expect("failure channel closed");
let lowered = reason.to_lowercase();
assert!(
lowered.contains("connect"),
"the reason should say the connect failed, got: {reason}"
);
assert!(
lowered.contains("refused"),
"the reason should carry the OS refusal, got: {reason}"
);
assert!(
reason.contains(&port.to_string()),
"the reason should name the port that refused, got: {reason}"
);
}
#[tokio::test]
async fn a_refused_connect_fails_fast_and_leaves_the_client_not_alive() {
let port = closed_port();
let (tx, _rx) = mpsc::channel(4);
let mut client = client_to(port, Arc::new(FailureRecorder { failures: tx }));
let started = Instant::now();
client.start().await;
let elapsed = started.elapsed();
assert!(
!client.is_alive(),
"a client that never connected must not report itself alive"
);
assert!(
elapsed < Duration::from_secs(2),
"a refused connect should surface immediately, took {elapsed:?}"
);
}
#[tokio::test]
async fn server_start_is_accepting_by_the_time_it_returns() {
let port = closed_port();
let (tx, _rx) = mpsc::channel(4);
let mut server = SmppServer::new(
IpAddr::from([127, 0, 0, 1]),
port,
Arc::new(FailureRecorder { failures: tx }),
);
server.start().await;
tokio::net::TcpStream::connect(("127.0.0.1", port))
.await
.expect("start() returned but the listener was not accepting yet");
assert!(server.is_alive(), "a listening server should report alive");
}
#[tokio::test]
async fn a_bind_conflict_reports_its_real_cause() {
let held = StdTcpListener::bind("127.0.0.1:0").expect("hold a port");
let port = held.local_addr().expect("local_addr").port();
let (tx, mut rx) = mpsc::channel(4);
let mut server = SmppServer::new(
IpAddr::from([127, 0, 0, 1]),
port,
Arc::new(FailureRecorder { failures: tx }),
);
server.start().await;
let reason = tokio::time::timeout(Duration::from_secs(5), rx.recv())
.await
.expect("on_listen_failed was never called")
.expect("failure channel closed");
assert!(
reason.to_lowercase().contains("bind"),
"the reason should say the bind failed, got: {reason}"
);
assert!(
reason.contains(&port.to_string()),
"the reason should name the port, got: {reason}"
);
assert!(
!server.is_alive(),
"a server that never bound must not report itself alive"
);
}
#[tokio::test]
async fn stopping_a_client_that_never_connected_is_a_no_op() {
let port = closed_port();
let (tx, _rx) = mpsc::channel(4);
let mut client = client_to(port, Arc::new(FailureRecorder { failures: tx }));
client.start().await;
assert!(!client.is_alive());
client.stop().await;
client.stop().await;
}
#[tokio::test]
async fn stopping_a_server_that_never_bound_is_a_no_op() {
let held = StdTcpListener::bind("127.0.0.1:0").expect("hold a port");
let port = held.local_addr().expect("local_addr").port();
let (tx, _rx) = mpsc::channel(4);
let mut server = SmppServer::new(
IpAddr::from([127, 0, 0, 1]),
port,
Arc::new(FailureRecorder { failures: tx }),
);
server.start().await;
assert!(!server.is_alive());
server.stop().await;
server.stop().await;
}
#[tokio::test]
async fn dropping_a_client_that_never_connected_does_not_panic() {
let port = closed_port();
let (tx, _rx) = mpsc::channel(4);
{
let mut client = client_to(port, Arc::new(FailureRecorder { failures: tx }));
client.start().await;
} }