1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// Test server implementation
//
// Provides a wrapper around SMCP server for testing purposes
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::{Method, StatusCode};
use hyper_util::rt::TokioIo;
use std::net::SocketAddr;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tower::{Layer, Service};
use tracing::info;
use smcp_server_core::SmcpServerBuilder;
/// Test server wrapper with proper lifecycle management
pub struct TestServer {
pub addr: SocketAddr,
pub server_url: String,
_shutdown_tx: broadcast::Sender<()>,
}
impl TestServer {
/// Create and start a new test server
pub async fn start() -> Result<Self, Box<dyn std::error::Error>> {
// Create TCP listener and bind to available port
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let server_url = format!("http://{}", addr);
info!("Starting test server on {}", addr);
// Build SMCP layer with default auth for testing
// This layer already has all Socket.IO handlers registered
let smcp_layer = SmcpServerBuilder::new()
.with_default_auth(Some("test_secret".to_string()), None)
.build_layer()?;
// Create shutdown channel
let (shutdown_tx, mut shutdown_rx) = broadcast::channel::<()>(1);
// Spawn server in background
tokio::spawn(async move {
info!("Test server task started on {}", addr);
// Accept connections with shutdown support
loop {
tokio::select! {
result = listener.accept() => {
match result {
Ok((stream, _remote_addr)) => {
let layer = smcp_layer.clone();
let io = TokioIo::new(stream);
tokio::spawn(async move {
// Create service that chains socket.io layer with fallback
let svc = tower::service_fn(|req| {
let layer = layer.clone();
async move {
// Use tower layer chain: socket.io layer -> fallback service
let mut svc = layer.layer.layer(tower::service_fn(|req: hyper::Request<hyper::body::Incoming>| async move {
// Fallback service for non-socket.io routes
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => {
Ok::<_, std::convert::Infallible>(
hyper::Response::builder()
.status(StatusCode::OK)
.body(Full::new(Bytes::from("SMCP Server is running")))
.unwrap()
)
}
(&Method::GET, "/health") => {
Ok::<_, std::convert::Infallible>(
hyper::Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Full::new(Bytes::from("{\"status\":\"ok\"}")))
.unwrap()
)
}
_ => {
Ok::<_, std::convert::Infallible>(
hyper::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("Not found")))
.unwrap()
)
}
}
}));
svc.call(req).await
}
});
// Convert tower service to hyper service
let svc = hyper_util::service::TowerToHyperService::new(svc);
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(io, svc)
.with_upgrades()
.await;
});
}
Err(e) => {
eprintln!("Server accept error: {}", e);
break;
}
}
}
_ = shutdown_rx.recv() => {
info!("Server shutdown signal received");
break;
}
}
}
info!("Test server task finished");
});
// Wait a bit for server to actually start listening
tokio::time::sleep(Duration::from_millis(100)).await;
info!("Test server is ready on {}", addr);
Ok(Self {
addr,
server_url,
_shutdown_tx: shutdown_tx,
})
}
/// Get the server URL for client connections
pub fn url(&self) -> &str {
&self.server_url
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_test_server_startup() {
let server = TestServer::start()
.await
.expect("Failed to start test server");
assert!(server.addr.port() > 0);
assert!(server.url().contains("http://127.0.0.1"));
}
}