1use 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)]
30pub 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 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 pub fn with_uri(path: &str) -> TestRequest {
90 TestRequest::default().uri(path).take()
91 }
92
93 #[must_use]
94 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 pub fn version(&mut self, ver: Version) -> &mut Self {
106 parts(&mut self.0).version = ver;
107 self
108 }
109
110 pub fn method(&mut self, meth: Method) -> &mut Self {
112 parts(&mut self.0).method = meth;
113 self
114 }
115
116 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 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 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 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 pub fn take(&mut self) -> TestRequest {
160 TestRequest(self.0.take())
161 }
162
163 #[must_use]
164 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
210pub 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
246pub 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 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)]
313pub 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 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 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 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 pub fn addr(&self) -> net::SocketAddr {
418 self.addr
419 }
420
421 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 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 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 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 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 pub async fn ws(&self) -> Result<WsConnection<impl Filter>, Error<WsClientError>> {
462 self.ws_at("/").await
463 }
464
465 #[cfg(feature = "ws")]
466 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 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 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 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}