Skip to main content

ntex_server/net/
test.rs

1//! Test server utilities.
2#![allow(clippy::missing_panics_doc)]
3use std::{fmt, io, marker::PhantomData, net, rc::Rc, thread, time};
4
5use ntex_io::{Io, IoConfig};
6use ntex_net::tcp_connect;
7use ntex_rt::System;
8use ntex_service::{IntoService, Service, cfg::SharedCfg};
9use socket2::{Domain, SockAddr, Socket, Type};
10use uuid::Uuid;
11
12use super::{NoConfig, Server, ServerAppConfig, ServerBuilder};
13
14/// Builder for a single-worker test server.
15///
16/// The server listens on an ephemeral `127.0.0.1` port and runs in a separate
17/// thread with its own runtime.
18pub struct TestServerBuilder<Cfg, F, Sf, I> {
19    id: Uuid,
20    cfg: Cfg,
21    factory: F,
22    config: SharedCfg,
23    client_config: SharedCfg,
24    _t: PhantomData<(Sf, I)>,
25}
26
27impl<Cfg, F, Sf, I> fmt::Debug for TestServerBuilder<Cfg, F, Sf, I> {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.debug_struct("TestServerBuilder")
30            .field("id", &self.id)
31            .field("config", &self.config)
32            .field("client_config", &self.client_config)
33            .finish()
34    }
35}
36
37impl<F, S, I> TestServerBuilder<NoConfig, F, S, I>
38where
39    F: AsyncFn() -> I + Send + Clone + 'static,
40    I: IntoService<S, (), Io> + 'static,
41    S: Service<(), Io> + 'static,
42{
43    #[must_use]
44    /// Creates a test server builder for the specified service factory.
45    pub fn new(factory: F) -> Self {
46        Self {
47            factory,
48            id: Uuid::now_v7(),
49            cfg: NoConfig,
50            config: SharedCfg::new("TEST-SERVER").into(),
51            client_config: SharedCfg::new("TEST-CLIENT").into(),
52            _t: PhantomData,
53        }
54    }
55}
56
57impl<Cfg, F, S, I> TestServerBuilder<Cfg, F, S, I>
58where
59    F: AsyncFn() -> I + Send + Clone + 'static,
60    I: IntoService<S, Cfg::State, Io> + 'static,
61    S: Service<Cfg::State, Io> + 'static,
62    Cfg: ServerAppConfig + 'static,
63{
64    #[must_use]
65    /// Creates a test server builder with application configuration.
66    pub fn with(cfg: Cfg, factory: F) -> Self {
67        Self {
68            cfg,
69            factory,
70            id: Uuid::now_v7(),
71            config: SharedCfg::new("TEST-SERVER").into(),
72            client_config: SharedCfg::new("TEST-CLIENT").into(),
73            _t: PhantomData,
74        }
75    }
76
77    #[must_use]
78    /// Sets the I/O configuration for accepted server connections.
79    pub fn config<T: Into<SharedCfg>>(mut self, cfg: T) -> Self {
80        self.config = cfg.into();
81        self
82    }
83
84    #[must_use]
85    /// Sets the I/O configuration used by [`TestServer::connect`].
86    pub fn client_config<T: Into<SharedCfg>>(mut self, cfg: T) -> Self {
87        self.client_config = cfg.into();
88        self
89    }
90
91    /// Starts the test server and returns its controller.
92    pub fn start(self) -> TestServer {
93        log::debug!("Starting test server {:?}", self.id);
94        let cfg = self.cfg;
95        let config = self.config;
96        let factory = self.factory;
97        let sys_cfg = System::current().config();
98        let name = System::current().name().to_string();
99
100        let (tx, rx) = oneshot::channel();
101        // run server in separate thread
102        thread::spawn(move || {
103            let sys = System::with_config(&name, sys_cfg);
104            let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
105            let local_addr = tcp.local_addr().unwrap();
106
107            sys.run(move || {
108                let server = ServerBuilder::new(cfg)
109                    .listen("test", tcp, config, async move |_| factory().await)?
110                    .workers(1)
111                    .disable_signals()
112                    .run();
113
114                ntex_rt::spawn_detached(async move {
115                    tx.send((System::current(), local_addr, server))
116                        .expect("Failed to send Server to TestServer");
117                });
118
119                Ok(())
120            })
121        });
122        let (system, addr, server) = rx.recv().unwrap();
123        thread::sleep(time::Duration::from_millis(25));
124
125        TestServer {
126            addr,
127            inner: Rc::new(TestServerInner {
128                server,
129                system,
130                id: self.id,
131                cfg: self.client_config,
132            }),
133        }
134    }
135}
136
137/// Starts a test server for the specified service factory.
138///
139/// [`TestServer`] is a simple server that makes it easier to write
140/// integration tests for ntex applications.
141///
142/// # Examples
143///
144/// ```rust
145/// use ntex::{server, http, client::Client};
146/// use ntex::web::{self, App, HttpResponse};
147///
148/// async fn my_handler() -> Result<HttpResponse, std::io::Error> {
149///     Ok(HttpResponse::Ok().into())
150/// }
151///
152/// #[ntex::test]
153/// async fn test_example() {
154///     let srv = server::test_server(
155///         async || http::HttpService::new(
156///             App::new().service(
157///                 web::resource("/").to(my_handler))
158///         )
159///     );
160///
161///     let req = Client::new().get(format!("http://127.0.0.1:{}", srv.addr().port()).as_str());
162///     let response = req.send().await.unwrap();
163///     assert!(response.status().is_success());
164/// }
165/// ```
166pub fn test_server<F, S>(factory: F) -> TestServer
167where
168    F: AsyncFn() -> S + Send + Clone + 'static,
169    S: Service<(), Io> + 'static,
170{
171    TestServerBuilder::new(factory).start()
172}
173
174/// Starts a test server configured by a server builder.
175///
176/// `factory` receives a single-worker builder with signals disabled and must
177/// register the listeners. The returned server's address is `127.0.0.1:0`;
178/// call [`TestServer::set_addr`] before using [`TestServer::connect`].
179pub fn build_test_server<Cfg, F>(cfg: Cfg, factory: F) -> TestServer
180where
181    Cfg: ServerAppConfig,
182    F: AsyncFnOnce(ServerBuilder<Cfg>) -> ServerBuilder<Cfg> + Send + 'static,
183{
184    let sys = System::current().config();
185    let name = System::current().name().to_string();
186
187    let id = Uuid::now_v7();
188    log::debug!("Starting {name:?} server {id:?}");
189
190    let (tx, rx) = oneshot::channel();
191
192    // run server in separate thread
193    thread::spawn(move || {
194        let sys = System::with_config(&name, sys);
195
196        sys.block_on(async move {
197            let server = factory(ServerBuilder::new(cfg))
198                .await
199                .workers(1)
200                .disable_signals()
201                .run();
202            tx.send((System::current(), server.clone()))
203                .expect("Failed to send Server to TestServer");
204            let _ = server.await;
205        });
206    });
207    let (system, server) = rx.recv().unwrap();
208    thread::sleep(time::Duration::from_millis(25));
209
210    TestServer {
211        addr: "127.0.0.1:0".parse().unwrap(),
212        inner: Rc::new(TestServerInner {
213            id,
214            system,
215            server,
216            cfg: SharedCfg::new("TEST-CLIENT").add(IoConfig::new()).into(),
217        }),
218    }
219}
220
221#[derive(Clone, Debug)]
222/// Test server controller.
223///
224/// Dropping the last clone stops the server and its runtime, blocking the
225/// current thread for about 100 ms.
226pub struct TestServer {
227    addr: net::SocketAddr,
228    inner: Rc<TestServerInner>,
229}
230
231#[derive(Debug)]
232struct TestServerInner {
233    id: Uuid,
234    system: System,
235    server: Server,
236    cfg: SharedCfg,
237}
238
239impl TestServer {
240    /// Returns the server address.
241    pub fn addr(&self) -> net::SocketAddr {
242        self.addr
243    }
244
245    #[must_use]
246    /// Sets the address used by [`connect`](Self::connect).
247    pub fn set_addr(mut self, addr: net::SocketAddr) -> Self {
248        self.addr = addr;
249        self
250    }
251
252    /// Returns the client I/O configuration.
253    pub fn config(&self) -> SharedCfg {
254        self.inner.cfg.clone()
255    }
256
257    /// Connects to the server address.
258    pub async fn connect(&self) -> io::Result<Io> {
259        tcp_connect(self.addr, self.inner.cfg.clone()).await
260    }
261
262    /// Requests a graceful server stop.
263    ///
264    /// Returns without waiting for the stop to complete.
265    pub fn stop(&self) {
266        drop(self.inner.server.stop(true));
267    }
268
269    /// Returns a currently unused local address.
270    ///
271    /// The port is released before this returns, so another process may take
272    /// it first.
273    pub fn unused_addr() -> net::SocketAddr {
274        let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
275        let socket = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
276        socket.set_reuse_address(true).unwrap();
277        socket.bind(&SockAddr::from(addr)).unwrap();
278        let tcp = net::TcpListener::from(socket);
279        tcp.local_addr().unwrap()
280    }
281
282    /// Returns the running server controller.
283    pub fn server(&self) -> Server {
284        self.inner.server.clone()
285    }
286}
287
288impl Drop for TestServerInner {
289    fn drop(&mut self) {
290        log::debug!("Stopping test server (dropped) {:?}", self.id);
291        drop(self.server.stop(false));
292        thread::sleep(time::Duration::from_millis(75));
293        self.system.stop();
294        thread::sleep(time::Duration::from_millis(25));
295    }
296}