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