Skip to main content

actix_http_test/
lib.rs

1//! Various helpers for Actix applications to use during testing.
2
3#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
4#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
5#![cfg_attr(docsrs, feature(doc_cfg))]
6
7#[cfg(feature = "openssl")]
8extern crate tls_openssl as openssl;
9
10use std::{net, thread, time::Duration};
11
12use actix_codec::{AsyncRead, AsyncWrite, Framed};
13use actix_rt::{net::TcpStream, System};
14use actix_server::{Server, ServerServiceFactory};
15use awc::{
16    error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
17    Connector,
18};
19use bytes::Bytes;
20use futures_core::stream::Stream;
21use http::Method;
22use socket2::{Domain, Protocol, Socket, Type};
23use tokio::sync::mpsc;
24
25/// Start test server.
26///
27/// `TestServer` is very simple test server that simplify process of writing integration tests cases
28/// for HTTP applications.
29///
30/// # Examples
31///
32/// ```
33/// use actix_http::{HttpService, Response, Error, StatusCode};
34/// use actix_http_test::test_server;
35/// use actix_service::{fn_service, map_config, ServiceFactoryExt as _};
36///
37/// #[actix_rt::test]
38/// # async fn hidden_test() {}
39/// async fn test_example() {
40///     let srv = test_server(|| {
41///         HttpService::build()
42///             .h1(fn_service(|req| async move {
43///                 Ok::<_, Error>(Response::ok())
44///             }))
45///             .tcp()
46///             .map_err(|_| ())
47///     })
48///     .await;
49///
50///     let req = srv.get("/");
51///     let response = req.send().await.unwrap();
52///
53///     assert_eq!(response.status(), StatusCode::OK);
54/// }
55/// # actix_rt::System::new().block_on(test_example());
56/// ```
57pub async fn test_server<F: ServerServiceFactory<TcpStream>>(factory: F) -> TestServer {
58    let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
59    test_server_with_addr(tcp, factory).await
60}
61
62/// Start [`test server`](test_server()) on an existing address binding.
63pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
64    tcp: net::TcpListener,
65    factory: F,
66) -> TestServer {
67    let (started_tx, started_rx) = std::sync::mpsc::channel();
68    let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
69
70    // run server in separate thread
71    thread::spawn(move || {
72        System::new().block_on(async move {
73            let local_addr = tcp.local_addr().unwrap();
74
75            let srv = Server::build()
76                .workers(1)
77                .disable_signals()
78                .system_exit()
79                .listen("test", tcp, factory)
80                .expect("test server could not be created");
81
82            let srv = srv.run();
83            started_tx
84                .send((System::current(), srv.handle(), local_addr))
85                .unwrap();
86
87            // drive server loop
88            srv.await.unwrap();
89        });
90
91        // notify TestServer that server and system have shut down
92        // all thread managed resources should be dropped at this point
93        #[allow(clippy::let_underscore_future)]
94        let _ = thread_stop_tx.send(());
95    });
96
97    let (system, server, addr) = started_rx.recv().unwrap();
98
99    let client = {
100        #[cfg(feature = "openssl")]
101        let connector = {
102            use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
103
104            let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
105
106            builder.set_verify(SslVerifyMode::NONE);
107            let _ = builder
108                .set_alpn_protos(b"\x02h2\x08http/1.1")
109                .map_err(|err| log::error!("Can not set ALPN protocol: {err}"));
110
111            Connector::new()
112                .conn_lifetime(Duration::from_secs(0))
113                .timeout(Duration::from_millis(30000))
114                .openssl(builder.build())
115        };
116
117        #[cfg(not(feature = "openssl"))]
118        let connector = {
119            Connector::new()
120                .conn_lifetime(Duration::from_secs(0))
121                .timeout(Duration::from_millis(30000))
122        };
123
124        Client::builder().connector(connector).finish()
125    };
126
127    TestServer {
128        server,
129        client,
130        system,
131        addr,
132        thread_stop_rx,
133    }
134}
135
136/// Test server controller
137pub struct TestServer {
138    server: actix_server::ServerHandle,
139    client: awc::Client,
140    system: actix_rt::System,
141    addr: net::SocketAddr,
142    thread_stop_rx: mpsc::Receiver<()>,
143}
144
145impl TestServer {
146    /// Construct test server url
147    pub fn addr(&self) -> net::SocketAddr {
148        self.addr
149    }
150
151    /// Construct test server url
152    pub fn url(&self, uri: &str) -> String {
153        if uri.starts_with('/') {
154            format!("http://localhost:{}{}", self.addr.port(), uri)
155        } else {
156            format!("http://localhost:{}/{}", self.addr.port(), uri)
157        }
158    }
159
160    /// Construct test HTTPS server URL.
161    pub fn surl(&self, uri: &str) -> String {
162        if uri.starts_with('/') {
163            format!("https://localhost:{}{}", self.addr.port(), uri)
164        } else {
165            format!("https://localhost:{}/{}", self.addr.port(), uri)
166        }
167    }
168
169    /// Create `GET` request
170    pub fn get<S: AsRef<str>>(&self, path: S) -> ClientRequest {
171        self.client.get(self.url(path.as_ref()).as_str())
172    }
173
174    /// Create HTTPS `GET` request
175    pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
176        self.client.get(self.surl(path.as_ref()).as_str())
177    }
178
179    /// Create `POST` request
180    pub fn post<S: AsRef<str>>(&self, path: S) -> ClientRequest {
181        self.client.post(self.url(path.as_ref()).as_str())
182    }
183
184    /// Create HTTPS `POST` request
185    pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
186        self.client.post(self.surl(path.as_ref()).as_str())
187    }
188
189    /// Create `HEAD` request
190    pub fn head<S: AsRef<str>>(&self, path: S) -> ClientRequest {
191        self.client.head(self.url(path.as_ref()).as_str())
192    }
193
194    /// Create HTTPS `HEAD` request
195    pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
196        self.client.head(self.surl(path.as_ref()).as_str())
197    }
198
199    /// Create `PUT` request
200    pub fn put<S: AsRef<str>>(&self, path: S) -> ClientRequest {
201        self.client.put(self.url(path.as_ref()).as_str())
202    }
203
204    /// Create HTTPS `PUT` request
205    pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
206        self.client.put(self.surl(path.as_ref()).as_str())
207    }
208
209    /// Create `PATCH` request
210    pub fn patch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
211        self.client.patch(self.url(path.as_ref()).as_str())
212    }
213
214    /// Create HTTPS `PATCH` request
215    pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
216        self.client.patch(self.surl(path.as_ref()).as_str())
217    }
218
219    /// Create `DELETE` request
220    pub fn delete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
221        self.client.delete(self.url(path.as_ref()).as_str())
222    }
223
224    /// Create HTTPS `DELETE` request
225    pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
226        self.client.delete(self.surl(path.as_ref()).as_str())
227    }
228
229    /// Create `OPTIONS` request
230    pub fn options<S: AsRef<str>>(&self, path: S) -> ClientRequest {
231        self.client.options(self.url(path.as_ref()).as_str())
232    }
233
234    /// Create HTTPS `OPTIONS` request
235    pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
236        self.client.options(self.surl(path.as_ref()).as_str())
237    }
238
239    /// Connect to test HTTP server
240    pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
241        self.client.request(method, path.as_ref())
242    }
243
244    pub async fn load_body<S>(
245        &mut self,
246        mut response: ClientResponse<S>,
247    ) -> Result<Bytes, PayloadError>
248    where
249        S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
250    {
251        response.body().limit(10_485_760).await
252    }
253
254    /// Connect to WebSocket server at a given path.
255    pub async fn ws_at(
256        &mut self,
257        path: &str,
258    ) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
259        let url = self.url(path);
260        let connect = self.client.ws(url).connect();
261        connect.await.map(|(_, framed)| framed)
262    }
263
264    /// Connect to a WebSocket server.
265    pub async fn ws(
266        &mut self,
267    ) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
268        self.ws_at("/").await
269    }
270
271    /// Get default HeaderMap of Client.
272    ///
273    /// Returns Some(&mut HeaderMap) when Client object is unique
274    /// (No other clone of client exists at the same time).
275    pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
276        self.client.headers()
277    }
278
279    /// Stop HTTP server.
280    ///
281    /// Waits for spawned `Server` and `System` to (force) shutdown.
282    pub async fn stop(&mut self) {
283        // signal server to stop
284        self.server.stop(false).await;
285
286        // also signal system to stop
287        // though this is handled by `ServerBuilder::exit_system` too
288        self.system.stop();
289
290        // wait for thread to be stopped but don't care about result
291        let _ = self.thread_stop_rx.recv().await;
292    }
293}
294
295impl Drop for TestServer {
296    fn drop(&mut self) {
297        // calls in this Drop impl should be enough to shut down the server, system, and thread
298        // without needing to await anything
299
300        // signal server to stop
301        #[allow(clippy::let_underscore_future)]
302        let _ = self.server.stop(true);
303
304        // signal system to stop
305        self.system.stop();
306    }
307}
308
309/// Get a localhost socket address with random, unused port.
310pub fn unused_addr() -> net::SocketAddr {
311    let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
312    let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
313    socket.bind(&addr.into()).unwrap();
314    socket.set_reuse_address(true).unwrap();
315    let tcp = net::TcpListener::from(socket);
316    tcp.local_addr().unwrap()
317}