Skip to main content

ntex_server/net/
test.rs

1//! Test server
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/// Test server builder
15pub struct TestServerBuilder<Cfg, F, Sf, I> {
16    id: Uuid,
17    cfg: Cfg,
18    factory: F,
19    config: SharedCfg,
20    client_config: SharedCfg,
21    _t: PhantomData<(Sf, I)>,
22}
23
24impl<Cfg, F, Sf, I> fmt::Debug for TestServerBuilder<Cfg, F, Sf, I> {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.debug_struct("TestServerBuilder")
27            .field("id", &self.id)
28            .field("config", &self.config)
29            .field("client_config", &self.client_config)
30            .finish()
31    }
32}
33
34impl<F, S, I> TestServerBuilder<NoConfig, F, S, I>
35where
36    F: AsyncFn() -> I + Send + Clone + 'static,
37    I: IntoService<S, (), Io> + 'static,
38    S: Service<(), Io> + 'static,
39{
40    #[must_use]
41    /// Create test server builder
42    pub fn new(factory: F) -> Self {
43        Self {
44            factory,
45            id: Uuid::now_v7(),
46            cfg: NoConfig,
47            config: SharedCfg::new("TEST-SERVER").into(),
48            client_config: SharedCfg::new("TEST-CLIENT").into(),
49            _t: PhantomData,
50        }
51    }
52}
53
54impl<Cfg, F, S, I> TestServerBuilder<Cfg, F, S, I>
55where
56    F: AsyncFn() -> I + Send + Clone + 'static,
57    I: IntoService<S, Cfg::State, Io> + 'static,
58    S: Service<Cfg::State, Io> + 'static,
59    Cfg: ServerAppConfig + 'static,
60{
61    #[must_use]
62    /// Create test server builder with server configuration
63    pub fn with(cfg: Cfg, factory: F) -> Self {
64        Self {
65            cfg,
66            factory,
67            id: Uuid::now_v7(),
68            config: SharedCfg::new("TEST-SERVER").into(),
69            client_config: SharedCfg::new("TEST-CLIENT").into(),
70            _t: PhantomData,
71        }
72    }
73
74    #[must_use]
75    /// Set server io configuration
76    pub fn config<T: Into<SharedCfg>>(mut self, cfg: T) -> Self {
77        self.config = cfg.into();
78        self
79    }
80
81    #[must_use]
82    /// Set client io configuration
83    pub fn client_config<T: Into<SharedCfg>>(mut self, cfg: T) -> Self {
84        self.client_config = cfg.into();
85        self
86    }
87
88    /// Start test server
89    pub fn start(self) -> TestServer {
90        log::debug!("Starting test server {:?}", self.id);
91        let cfg = self.cfg;
92        let config = self.config;
93        let factory = self.factory;
94        let sys_cfg = System::current().config();
95        let name = System::current().name().to_string();
96
97        let (tx, rx) = oneshot::channel();
98        // run server in separate thread
99        thread::spawn(move || {
100            let sys = System::with_config(&name, sys_cfg);
101            let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
102            let local_addr = tcp.local_addr().unwrap();
103
104            sys.run(move || {
105                let server = ServerBuilder::new(cfg)
106                    .listen("test", tcp, config, async move |_| factory().await)?
107                    .workers(1)
108                    .disable_signals()
109                    .enable_affinity()
110                    .run();
111
112                ntex_rt::spawn(async move {
113                    tx.send((System::current(), local_addr, server))
114                        .expect("Failed to send Server to TestServer");
115                });
116
117                Ok(())
118            })
119        });
120        let (system, addr, server) = rx.recv().unwrap();
121        thread::sleep(time::Duration::from_millis(25));
122
123        TestServer {
124            addr,
125            inner: Rc::new(TestServerInner {
126                server,
127                system,
128                id: self.id,
129                cfg: self.client_config,
130            }),
131        }
132    }
133}
134
135/// Start test server
136///
137/// `TestServer` is very simple test server that simplify process of writing
138/// integration tests cases for ntex web applications.
139///
140/// # Examples
141///
142/// ```rust
143/// use ntex::{server, http, client::Client};
144/// use ntex::web::{self, App, HttpResponse};
145///
146/// async fn my_handler() -> Result<HttpResponse, std::io::Error> {
147///     Ok(HttpResponse::Ok().into())
148/// }
149///
150/// #[ntex::test]
151/// async fn test_example() {
152///     let mut srv = server::test_server(
153///         async || http::HttpService::new(
154///             App::new().service(
155///                 web::resource("/").to(my_handler))
156///         )
157///     );
158///
159///     let req = Client::new().get("http://127.0.0.1:{}", srv.addr().port());
160///     let response = req.send().await.unwrap();
161///     assert!(response.status().is_success());
162/// }
163/// ```
164pub fn test_server<F, S>(factory: F) -> TestServer
165where
166    F: AsyncFn() -> S + Send + Clone + 'static,
167    S: Service<(), Io> + 'static,
168{
169    TestServerBuilder::new(factory).start()
170}
171
172/// Start new server with server builder
173pub fn build_test_server<Cfg, F>(cfg: Cfg, factory: F) -> TestServer
174where
175    Cfg: ServerAppConfig,
176    F: AsyncFnOnce(ServerBuilder<Cfg>) -> ServerBuilder<Cfg> + Send + 'static,
177{
178    let sys = System::current().config();
179    let name = System::current().name().to_string();
180
181    let id = Uuid::now_v7();
182    log::debug!("Starting {name:?} server {id:?}");
183
184    let (tx, rx) = oneshot::channel();
185
186    // run server in separate thread
187    thread::spawn(move || {
188        let sys = System::with_config(&name, sys);
189
190        sys.block_on(async move {
191            let server = factory(ServerBuilder::new(cfg))
192                .await
193                .workers(1)
194                .disable_signals()
195                .run();
196            tx.send((System::current(), server.clone()))
197                .expect("Failed to send Server to TestServer");
198            let _ = server.await;
199        });
200    });
201    let (system, server) = rx.recv().unwrap();
202    thread::sleep(time::Duration::from_millis(25));
203
204    TestServer {
205        addr: "127.0.0.1:0".parse().unwrap(),
206        inner: Rc::new(TestServerInner {
207            id,
208            system,
209            server,
210            cfg: SharedCfg::new("TEST-CLIENT").add(IoConfig::new()).into(),
211        }),
212    }
213}
214
215#[derive(Clone, Debug)]
216/// Test server controller
217pub struct TestServer {
218    addr: net::SocketAddr,
219    inner: Rc<TestServerInner>,
220}
221
222#[derive(Debug)]
223struct TestServerInner {
224    id: Uuid,
225    system: System,
226    server: Server,
227    cfg: SharedCfg,
228}
229
230impl TestServer {
231    /// Test server socket addr
232    pub fn addr(&self) -> net::SocketAddr {
233        self.addr
234    }
235
236    #[must_use]
237    pub fn set_addr(mut self, addr: net::SocketAddr) -> Self {
238        self.addr = addr;
239        self
240    }
241
242    /// Test client shared config
243    pub fn config(&self) -> SharedCfg {
244        self.inner.cfg.clone()
245    }
246
247    /// Connect to server, return Io
248    pub async fn connect(&self) -> io::Result<Io> {
249        tcp_connect(self.addr, self.inner.cfg.clone()).await
250    }
251
252    /// Stop http server by stopping the runtime.
253    pub fn stop(&self) {
254        drop(self.inner.server.stop(true));
255    }
256
257    /// Get first available unused address
258    pub fn unused_addr() -> net::SocketAddr {
259        let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
260        let socket = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
261        socket.set_reuse_address(true).unwrap();
262        socket.bind(&SockAddr::from(addr)).unwrap();
263        let tcp = net::TcpListener::from(socket);
264        tcp.local_addr().unwrap()
265    }
266
267    /// Get access to the running Server
268    pub fn server(&self) -> Server {
269        self.inner.server.clone()
270    }
271}
272
273impl Drop for TestServerInner {
274    fn drop(&mut self) {
275        log::debug!("Stopping test server (dropped) {:?}", self.id);
276        drop(self.server.stop(false));
277        thread::sleep(time::Duration::from_millis(75));
278        self.system.stop();
279        thread::sleep(time::Duration::from_millis(25));
280    }
281}