Skip to main content

ntex/http/
test.rs

1//! Test helpers to use during testing.
2use std::{net, str::FromStr, sync::mpsc, thread, time};
3
4#[cfg(feature = "cookie")]
5use coo_kie::{Cookie, CookieJar};
6
7use ntex_tls::TlsConfig;
8use urly::Url;
9use uuid::Uuid;
10
11use crate::channel::bstream;
12use crate::client::{
13    Client, ClientConfig, ClientRequest, ClientResponse, error::ClientPayloadError,
14};
15use crate::error::Error;
16#[cfg(feature = "ws")]
17use crate::io::Filter;
18use crate::io::{Io, IoConfig};
19use crate::server::{NoConfig, Server};
20use crate::service::{IntoService, Service, cfg::SharedCfg};
21#[cfg(feature = "ws")]
22use crate::ws::{WsClient, WsClientConfig, WsConnection, error::WsClientError};
23use crate::{rt::System, time::Millis, time::Seconds, util::Bytes};
24
25use super::header::{self, HeaderMap, HeaderName, HeaderValue};
26
27use super::{Method, Request, Version, error::HttpError, payload::Payload};
28
29#[derive(Debug)]
30/// Test `Request` builder
31///
32/// ```rust,no_run
33/// use ntex::http::test::TestRequest;
34/// use ntex::http::{header, Request, Response, StatusCode, HttpMessage};
35///
36/// fn index(req: Request) -> Response {
37///     if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
38///         Response::Ok().into()
39///     } else {
40///         Response::BadRequest().into()
41///     }
42/// }
43///
44/// let resp = index(
45///     TestRequest::with_header("content-type", "text/plain").build()
46/// );
47/// assert_eq!(resp.status(), StatusCode::OK);
48///
49/// let resp = index(
50///     TestRequest::default().build()
51/// );
52/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
53/// ```
54pub struct TestRequest(Option<Inner>);
55
56#[derive(Debug)]
57struct Inner {
58    version: Version,
59    method: Method,
60    uri: Url,
61    headers: HeaderMap,
62    #[cfg(feature = "cookie")]
63    cookies: CookieJar,
64    payload: Option<Payload>,
65}
66
67impl Default for TestRequest {
68    fn default() -> TestRequest {
69        Self::builder()
70    }
71}
72
73impl TestRequest {
74    /// Creates a test request builder with a `GET /` request.
75    pub fn builder() -> TestRequest {
76        TestRequest(Some(Inner {
77            method: Method::GET,
78            uri: Url::new(),
79            version: Version::HTTP_11,
80            headers: HeaderMap::new(),
81            #[cfg(feature = "cookie")]
82            cookies: CookieJar::new(),
83            payload: None,
84        }))
85    }
86
87    #[must_use]
88    /// Create `TestRequest` and set request uri.
89    pub fn with_uri(path: &str) -> TestRequest {
90        TestRequest::default().uri(path).take()
91    }
92
93    #[must_use]
94    /// Create `TestRequest` and append header.
95    pub fn with_header<K, V>(key: K, value: V) -> TestRequest
96    where
97        HeaderName: TryFrom<K>,
98        HeaderValue: TryFrom<V>,
99        <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
100    {
101        TestRequest::default().header(key, value).take()
102    }
103
104    /// Set HTTP version of this request.
105    pub fn version(&mut self, ver: Version) -> &mut Self {
106        parts(&mut self.0).version = ver;
107        self
108    }
109
110    /// Set HTTP method of this request.
111    pub fn method(&mut self, meth: Method) -> &mut Self {
112        parts(&mut self.0).method = meth;
113        self
114    }
115
116    /// Set HTTP Uri of this request.
117    pub fn uri(&mut self, path: &str) -> &mut Self {
118        parts(&mut self.0).uri = Url::from_str(path).unwrap();
119        self
120    }
121
122    /// Append a header.
123    ///
124    /// Existing values for the same header name are kept.
125    pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
126    where
127        HeaderName: TryFrom<K>,
128        HeaderValue: TryFrom<V>,
129        <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
130    {
131        if let Ok(key) = HeaderName::try_from(key)
132            && let Ok(value) = HeaderValue::try_from(value)
133        {
134            parts(&mut self.0).headers.append(key, value);
135            return self;
136        }
137        panic!("Cannot create header");
138    }
139
140    #[cfg(feature = "cookie")]
141    /// Set cookie for this request.
142    pub fn cookie<C>(&mut self, cookie: C) -> &mut Self
143    where
144        C: Into<Cookie<'static>>,
145    {
146        parts(&mut self.0).cookies.add(cookie.into());
147        self
148    }
149
150    /// Set request payload.
151    pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self {
152        let payload = bstream::empty(Some(data.into()));
153        parts(&mut self.0).payload = Some(payload.into());
154        self
155    }
156
157    #[must_use]
158    /// Take test request.
159    pub fn take(&mut self) -> TestRequest {
160        TestRequest(self.0.take())
161    }
162
163    #[must_use]
164    /// Complete request creation and generate `Request` instance.
165    pub fn build(&mut self) -> Request {
166        let inner = self.0.take().expect("cannot reuse test request builder");
167
168        let mut req = if let Some(pl) = inner.payload {
169            Request::with_payload(pl)
170        } else {
171            Request::with_payload(bstream::empty(None).into())
172        };
173
174        let head = req.head_mut();
175        head.uri = inner.uri;
176        head.method = inner.method;
177        head.version = inner.version;
178        head.headers = inner.headers;
179
180        if let Some(conn) = head.headers.get(header::CONNECTION)
181            && let Ok(s) = conn.to_str()
182            && s.to_lowercase().contains("upgrade")
183        {
184            head.set_upgrade();
185        }
186
187        #[cfg(feature = "cookie")]
188        {
189            let mut cookie = Vec::new();
190            for c in inner.cookies.delta() {
191                super::helpers::push_cookie(&mut cookie, c.name(), c.value());
192            }
193            if !cookie.is_empty() {
194                head.headers.insert(
195                    super::header::COOKIE,
196                    HeaderValue::from_bytes(&cookie).unwrap(),
197                );
198            }
199        }
200
201        req
202    }
203}
204
205#[inline]
206fn parts(parts: &mut Option<Inner>) -> &mut Inner {
207    parts.as_mut().expect("cannot reuse test request builder")
208}
209
210/// Start test server
211///
212/// `TestServer` is very simple test server that simplify process of writing
213/// integration tests cases for ntex web applications.
214///
215/// # Examples
216///
217/// ```rust
218/// use ntex::http::{self, Method, Response};
219///
220/// #[ntex::test]
221/// async fn test_example() {
222///     let srv = http::test::server(async |_| {
223///         http::HttpService::h1(async |_| Ok::<_, std::io::Error>(Response::Ok().build()))
224///     });
225///
226///     let req = srv.request(Method::GET, "/");
227///     let response = req.send().await.unwrap();
228///     assert!(response.status().is_success());
229/// }
230/// ```
231pub fn server<F, S, I>(factory: F) -> TestServer
232where
233    F: AsyncFn(&()) -> I + Send + Clone + 'static,
234    S: Service<(), Io> + 'static,
235    I: IntoService<S, (), Io> + 'static,
236{
237    server_with_config::<_, _, _>(
238        factory,
239        SharedCfg::new("HTTP-TEST-SRV")
240            .add(IoConfig::new())
241            .add(TlsConfig::new())
242            .add(ntex_h2::ServiceConfig::new()),
243    )
244}
245
246/// Start test server with custom configuration
247///
248/// Same as [`server()`], but the server uses the provided shared configuration.
249///
250/// # Examples
251///
252/// ```rust
253/// use ntex::http::{self, Method, Response};
254/// use ntex::{SharedCfg, io::IoConfig, time::Seconds};
255///
256/// #[ntex::test]
257/// async fn test_example() {
258///     let srv = http::test::server_with_config(
259///         async |_| {
260///             http::HttpService::h1(async |_| {
261///                 Ok::<_, std::io::Error>(Response::Ok().build())
262///             })
263///         },
264///         SharedCfg::new("TEST-SRV")
265///             .add(IoConfig::new().set_shutdown_timeout(Seconds(1))),
266///     );
267///
268///     let req = srv.request(Method::GET, "/");
269///     let response = req.send().await.unwrap();
270///     assert!(response.status().is_success());
271/// }
272/// ```
273pub fn server_with_config<F, S, I>(f: F, cfg: impl Into<SharedCfg>) -> TestServer
274where
275    F: AsyncFn(&()) -> I + Send + Clone + 'static,
276    S: Service<(), Io> + 'static,
277    I: IntoService<S, (), Io> + 'static,
278{
279    let sys = System::current().config();
280    let name = System::current().name().to_string();
281
282    let id = Uuid::now_v7();
283    let cfg = cfg.into();
284    let (tx, rx) = mpsc::channel();
285    log::debug!("Starting {name:?} http server {id:?}");
286
287    // run server in separate thread
288    thread::spawn(move || {
289        let sys = System::with_config(&name, sys);
290        let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
291        let local_addr = tcp.local_addr().unwrap();
292
293        sys.run(move || {
294            let srv = crate::server::ServerBuilder::new(NoConfig)
295                .listen("test", tcp, cfg, async move |st| f(st).await)?
296                .workers(1)
297                .disable_signals()
298                .run();
299
300            crate::rt::spawn(async move {
301                tx.send((System::current(), srv, local_addr)).unwrap();
302            });
303            Ok(())
304        })
305    });
306    let (system, server, addr) = rx.recv().unwrap();
307    thread::sleep(Millis(25).into());
308
309    TestServer::create(id, system, server, addr, Seconds(90), Millis(90_000))
310}
311
312#[derive(Debug)]
313/// Test server controller
314pub struct TestServer {
315    id: Uuid,
316    cfg: SharedCfg,
317    addr: net::SocketAddr,
318    client: Client,
319    system: System,
320    server: Server,
321}
322
323impl TestServer {
324    /// Creates a test-server controller from a running server and its client
325    /// connection settings.
326    pub fn create(
327        id: Uuid,
328        system: System,
329        server: Server,
330        addr: net::SocketAddr,
331        timeout: Seconds,
332        connect_timeout: Millis,
333    ) -> Self {
334        let cfg = SharedCfg::new("TEST-CLIENT")
335            .add(IoConfig::new().set_connect_timeout(connect_timeout))
336            .add(TlsConfig::new().set_handshake_timeout(timeout))
337            .add(
338                ClientConfig::new()
339                    .set_response_timeout(Seconds(30))
340                    .set_response_payload_timeout(Seconds(30)),
341            )
342            .add(
343                ntex_h2::ServiceConfig::new()
344                    .set_max_header_list_size(256 * 1024)
345                    .set_max_header_continuation_frames(96),
346            )
347            .add(
348                WsClientConfig::new()
349                    .set_address(addr)
350                    .set_handshake_timeout(Seconds(30)),
351            )
352            .build();
353
354        let client = Self::create_client(cfg.clone());
355
356        TestServer {
357            id,
358            cfg,
359            addr,
360            client,
361            system,
362            server,
363        }
364    }
365
366    #[must_use]
367    /// Sets the client timeouts.
368    ///
369    /// `timeout` controls the TLS handshake, response-header and response
370    /// payload timeouts, while `connect_timeout` controls how long the client
371    /// waits to establish a connection. By default the response timeouts are
372    /// 30 seconds.
373    pub fn set_client_timeout(mut self, timeout: Seconds, connect_timeout: Millis) -> Self {
374        self.cfg = SharedCfg::new("TEST-CLIENT")
375            .add(IoConfig::new().set_connect_timeout(connect_timeout))
376            .add(TlsConfig::new().set_handshake_timeout(timeout))
377            .add(
378                ClientConfig::new()
379                    .set_response_timeout(timeout)
380                    .set_response_payload_timeout(timeout),
381            )
382            .add(
383                ntex_h2::ServiceConfig::new()
384                    .set_max_header_list_size(256 * 1024)
385                    .set_max_header_continuation_frames(96),
386            )
387            .add(
388                WsClientConfig::new()
389                    .set_address(self.addr)
390                    .set_handshake_timeout(Seconds(30)),
391            )
392            .build();
393        self.client = Self::create_client(self.cfg.clone());
394        self
395    }
396
397    /// Set client timeout
398    fn create_client(cfg: SharedCfg) -> Client {
399        #[cfg(feature = "openssl")]
400        {
401            use tls_openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
402
403            let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
404            builder.set_verify(SslVerifyMode::NONE);
405            let _ = builder
406                .set_alpn_protos(b"\x02h2\x08http/1.1")
407                .map_err(|e| log::error!("Cannot set alpn protocol: {e:?}"));
408            Client::builder().openssl(builder.build()).build(cfg)
409        }
410        #[cfg(not(feature = "openssl"))]
411        {
412            Client::builder().build(cfg)
413        }
414    }
415
416    /// Returns the test server's socket address.
417    pub fn addr(&self) -> net::SocketAddr {
418        self.addr
419    }
420
421    /// Construct test server url
422    pub fn url(&self, uri: &str) -> String {
423        if uri.starts_with('/') {
424            format!("http://localhost:{}{}", self.addr.port(), uri)
425        } else {
426            format!("http://localhost:{}/{}", self.addr.port(), uri)
427        }
428    }
429
430    /// Construct test https server url
431    pub fn surl(&self, uri: &str) -> String {
432        if uri.starts_with('/') {
433            format!("https://localhost:{}{}", self.addr.port(), uri)
434        } else {
435            format!("https://localhost:{}/{}", self.addr.port(), uri)
436        }
437    }
438
439    /// Create client request
440    pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
441        self.client
442            .request(method, self.url(path.as_ref()).as_str())
443    }
444
445    /// Create secure client request
446    pub fn srequest<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
447        self.client
448            .request(method, self.surl(path.as_ref()).as_str())
449    }
450
451    /// Load response's body
452    pub async fn load_body(
453        &self,
454        response: ClientResponse,
455    ) -> Result<Bytes, Error<ClientPayloadError>> {
456        response.body().limit(10_485_760).await
457    }
458
459    #[cfg(feature = "ws")]
460    /// Connect to a websocket server
461    pub async fn ws(&self) -> Result<WsConnection<impl Filter>, Error<WsClientError>> {
462        self.ws_at("/").await
463    }
464
465    #[cfg(feature = "ws")]
466    /// Connect to websocket server at a given path
467    pub async fn ws_at(
468        &self,
469        path: &str,
470    ) -> Result<WsConnection<impl Filter>, Error<WsClientError>> {
471        WsClient::new(self.url(path), &self.cfg).connect().await
472    }
473
474    #[cfg(all(feature = "openssl", feature = "ws"))]
475    /// Connect to a websocket server
476    pub async fn wss(
477        &self,
478    ) -> Result<
479        WsConnection<crate::io::Layer<crate::connect::openssl::SslFilter>>,
480        Error<WsClientError>,
481    > {
482        self.wss_at("/").await
483    }
484
485    #[cfg(all(feature = "openssl", feature = "ws"))]
486    /// Connect to secure websocket server at a given path
487    pub async fn wss_at(
488        &self,
489        path: &str,
490    ) -> Result<
491        WsConnection<crate::io::Layer<crate::connect::openssl::SslFilter>>,
492        Error<WsClientError>,
493    > {
494        use tls_openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
495
496        let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
497        builder.set_verify(SslVerifyMode::NONE);
498        let _ = builder
499            .set_alpn_protos(b"\x08http/1.1")
500            .map_err(|e| log::error!("Cannot set alpn protocol: {e:?}"));
501
502        WsClient::new(self.url(path), &self.cfg)
503            .openssl(builder.build())
504            .connect()
505            .await
506    }
507
508    /// Stop http server
509    pub async fn stop(self, graceful: bool) {
510        self.server.stop(graceful).await;
511    }
512}
513
514impl Drop for TestServer {
515    fn drop(&mut self) {
516        log::debug!("Stopping test http server {:?}", self.id);
517        drop(self.server.stop(false));
518        thread::sleep(time::Duration::from_millis(75));
519        self.system.stop();
520        thread::sleep(time::Duration::from_millis(25));
521    }
522}