ntex_server/net/
test.rs

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