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