1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-01 16:55:08 +02:00

migrate integration testing to new crate (#2112)

This commit is contained in:
Rob Ede
2021-04-02 08:26:59 +01:00
committed by GitHub
parent 50dc13f280
commit c54a0713de
41 changed files with 789 additions and 644 deletions

View File

@ -103,8 +103,8 @@ impl AppService {
}
}
/// Application connection config
#[derive(Clone)]
/// Application connection config.
#[derive(Debug, Clone)]
pub struct AppConfig {
secure: bool,
host: String,
@ -112,10 +112,15 @@ pub struct AppConfig {
}
impl AppConfig {
pub(crate) fn new(secure: bool, addr: SocketAddr, host: String) -> Self {
pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self {
AppConfig { secure, host, addr }
}
#[doc(hidden)]
pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self {
AppConfig::new(secure, host, addr)
}
/// Server host name.
///
/// Host name is used by application router as a hostname for url generation.
@ -142,8 +147,8 @@ impl Default for AppConfig {
fn default() -> Self {
AppConfig::new(
false,
"127.0.0.1:8080".parse().unwrap(),
"localhost:8080".to_owned(),
"127.0.0.1:8080".parse().unwrap(),
)
}
}

View File

@ -148,13 +148,13 @@ impl<T: ?Sized + 'static> DataFactory for Data<T> {
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
use crate::dev::Service;
use crate::http::StatusCode;
use crate::test::{self, init_service, TestRequest};
use crate::{web, App, HttpResponse};
use crate::{
dev::Service,
http::StatusCode,
test::{init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test]
async fn test_data_extractor() {
@ -270,49 +270,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_data_drop() {
struct TestData(Arc<AtomicUsize>);
impl TestData {
fn new(inner: Arc<AtomicUsize>) -> Self {
let _ = inner.fetch_add(1, Ordering::SeqCst);
Self(inner)
}
}
impl Clone for TestData {
fn clone(&self) -> Self {
let inner = self.0.clone();
let _ = inner.fetch_add(1, Ordering::SeqCst);
Self(inner)
}
}
impl Drop for TestData {
fn drop(&mut self) {
let _ = self.0.fetch_sub(1, Ordering::SeqCst);
}
}
let num = Arc::new(AtomicUsize::new(0));
let data = TestData::new(num.clone());
assert_eq!(num.load(Ordering::SeqCst), 1);
let srv = test::start(move || {
let data = data.clone();
App::new()
.data(data)
.service(web::resource("/").to(|_data: Data<TestData>| async { "ok" }))
});
assert!(srv.get("/").send().await.unwrap().status().is_success());
srv.stop().await;
assert_eq!(num.load(Ordering::SeqCst), 0);
}
#[actix_rt::test]
async fn test_data_from_arc() {
let data_new = Data::new(String::from("test-123"));

View File

@ -71,11 +71,6 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
#[cfg(feature = "rustls")]
extern crate tls_rustls as rustls;
mod app;
mod app_service;
mod config;

View File

@ -137,7 +137,7 @@ mod tests {
use crate::{web, App, HttpResponse};
#[actix_rt::test]
#[cfg(feature = "cookies")]
#[cfg(all(feature = "cookies", feature = "compress"))]
async fn test_scope_middleware() {
use crate::middleware::Compress;
@ -160,7 +160,7 @@ mod tests {
}
#[actix_rt::test]
#[cfg(feature = "cookies")]
#[cfg(all(feature = "cookies", feature = "compress"))]
async fn test_resource_scope_middleware() {
use crate::middleware::Compress;

View File

@ -52,6 +52,18 @@ impl HttpRequest {
}),
}
}
#[doc(hidden)]
pub fn __priv_test_new(
path: Path<Url>,
head: Message<RequestHead>,
rmap: Rc<ResourceMap>,
config: AppConfig,
app_data: Rc<Extensions>,
) -> HttpRequest {
let app_state = AppInitServiceState::new(rmap, config);
Self::new(path, head, app_state, app_data)
}
}
impl HttpRequest {

View File

@ -71,12 +71,15 @@ impl<F, I, S, B> HttpServer<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
// S::Future: 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
S::Service: 'static,
// S::Service: 'static,
B: MessageBody + 'static,
{
/// Create new HTTP server with application factory
@ -288,7 +291,7 @@ where
};
svc.finish(map_config(factory(), move |_| {
AppConfig::new(false, addr, host.clone())
AppConfig::new(false, host.clone(), addr)
}))
.tcp()
})?;
@ -343,10 +346,11 @@ where
};
svc.finish(map_config(factory(), move |_| {
AppConfig::new(true, addr, host.clone())
AppConfig::new(true, host.clone(), addr)
}))
.openssl(acceptor.clone())
})?;
Ok(self)
}
@ -396,10 +400,11 @@ where
};
svc.finish(map_config(factory(), move |_| {
AppConfig::new(true, addr, host.clone())
AppConfig::new(true, host.clone(), addr)
}))
.rustls(config.clone())
})?;
Ok(self)
}
@ -502,8 +507,8 @@ where
let c = cfg.lock().unwrap();
let config = AppConfig::new(
false,
socket_addr,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
socket_addr,
);
pipeline_factory(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) })
@ -552,8 +557,8 @@ where
let c = cfg.lock().unwrap();
let config = AppConfig::new(
false,
socket_addr,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
socket_addr,
);
pipeline_factory(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) })
.and_then(

View File

@ -69,6 +69,12 @@ impl ServiceRequest {
Self { req, payload }
}
/// Construct service request.
#[doc(hidden)]
pub fn __priv_test_new(req: HttpRequest, payload: Payload) -> Self {
Self::new(req, payload)
}
/// Deconstruct request into parts
#[inline]
pub fn into_parts(self) -> (HttpRequest, Payload) {

View File

@ -1,47 +1,41 @@
//! Various helpers for Actix applications to use during testing.
use std::net::SocketAddr;
use std::rc::Rc;
use std::sync::mpsc;
use std::{fmt, net, thread, time};
use std::{net::SocketAddr, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
#[cfg(feature = "cookies")]
use actix_http::cookie::Cookie;
use actix_http::http::{HeaderMap, Method, StatusCode, Uri, Version};
use actix_http::test::TestRequest as HttpTestRequest;
use actix_http::{ws, Extensions, HttpService, Request};
pub use actix_http::test::TestBuffer;
use actix_http::{
http::{header::IntoHeaderPair, Method, StatusCode, Uri, Version},
test::TestRequest as HttpTestRequest,
Extensions, Request,
};
use actix_router::{Path, ResourceDef, Url};
use actix_rt::{time::sleep, System};
use actix_service::{map_config, IntoService, IntoServiceFactory, Service, ServiceFactory};
use actix_service::{IntoService, IntoServiceFactory, Service, ServiceFactory};
use actix_utils::future::ok;
use awc::error::PayloadError;
use awc::{Client, ClientRequest, ClientResponse, Connector};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use futures_util::StreamExt as _;
use serde::de::DeserializeOwned;
use serde::Serialize;
use socket2::{Domain, Protocol, Socket, Type};
use serde::{de::DeserializeOwned, Serialize};
pub use actix_http::test::TestBuffer;
use crate::{
app_service::AppInitServiceState,
config::AppConfig,
data::Data,
dev::{Body, MessageBody, Payload},
http::header::ContentType,
rmap::ResourceMap,
service::{ServiceRequest, ServiceResponse},
web::{Bytes, BytesMut},
Error, HttpRequest, HttpResponse,
};
use crate::app_service::AppInitServiceState;
use crate::config::AppConfig;
use crate::data::Data;
use crate::dev::{Body, MessageBody, Payload, Server};
use crate::http::header::{ContentType, IntoHeaderPair};
use crate::rmap::ResourceMap;
use crate::service::{ServiceRequest, ServiceResponse};
use crate::{Error, HttpRequest, HttpResponse};
/// Create service that always responds with `HttpResponse::Ok()`
/// Create service that always responds with `HttpResponse::Ok()` and no body.
pub fn ok_service(
) -> impl Service<ServiceRequest, Response = ServiceResponse<Body>, Error = Error> {
default_service(StatusCode::OK)
}
/// Create service that responds with response with specified status code
/// Create service that always responds with given status code and no body.
pub fn default_service(
status_code: StatusCode,
) -> impl Service<ServiceRequest, Response = ServiceResponse<Body>, Error = Error> {
@ -51,8 +45,7 @@ pub fn default_service(
.into_service()
}
/// This method accepts application builder instance, and constructs
/// service.
/// Initialize service from application builder instance.
///
/// ```
/// use actix_service::Service;
@ -83,10 +76,10 @@ where
{
try_init_service(app)
.await
.expect("service initilization failed")
.expect("service initialization failed")
}
/// Fallible version of init_service that allows testing data factory errors.
/// Fallible version of [`init_service`] that allows testing initialization errors.
pub(crate) async fn try_init_service<R, S, B, E>(
app: R,
) -> Result<impl Service<Request, Response = ServiceResponse<B>, Error = E>, S::InitError>
@ -166,9 +159,11 @@ where
let mut body = resp.take_body();
let mut bytes = BytesMut::new();
while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.unwrap());
}
bytes.freeze()
}
@ -573,430 +568,12 @@ impl TestRequest {
}
}
/// Start test server with default configuration
///
/// Test server is very simple server that simplify process of writing
/// integration tests cases for actix web applications.
///
/// # Examples
///
/// ```
/// use actix_web::{web, test, App, HttpResponse, Error};
///
/// async fn my_handler() -> Result<HttpResponse, Error> {
/// Ok(HttpResponse::Ok().into())
/// }
///
/// #[actix_rt::test]
/// async fn test_example() {
/// let srv = test::start(
/// || App::new().service(
/// web::resource("/").to(my_handler))
/// );
///
/// let req = srv.get("/");
/// let response = req.send().await.unwrap();
/// assert!(response.status().is_success());
/// }
/// ```
pub fn start<F, I, S, B>(factory: F) -> TestServer
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<HttpResponse<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
start_with(TestServerConfig::default(), factory)
}
/// Start test server with custom configuration
///
/// Test server could be configured in different ways, for details check
/// `TestServerConfig` docs.
///
/// # Examples
///
/// ```
/// use actix_web::{web, test, App, HttpResponse, Error};
///
/// async fn my_handler() -> Result<HttpResponse, Error> {
/// Ok(HttpResponse::Ok().into())
/// }
///
/// #[actix_rt::test]
/// async fn test_example() {
/// let srv = test::start_with(test::config().h1(), ||
/// App::new().service(web::resource("/").to(my_handler))
/// );
///
/// let req = srv.get("/");
/// let response = req.send().await.unwrap();
/// assert!(response.status().is_success());
/// }
/// ```
pub fn start_with<F, I, S, B>(cfg: TestServerConfig, factory: F) -> TestServer
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<HttpResponse<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
let (tx, rx) = mpsc::channel();
let ssl = match cfg.stream {
StreamType::Tcp => false,
#[cfg(feature = "openssl")]
StreamType::Openssl(_) => true,
#[cfg(feature = "rustls")]
StreamType::Rustls(_) => true,
};
// run server in separate thread
thread::spawn(move || {
let sys = System::new();
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone();
let cfg = cfg.clone();
let ctimeout = cfg.client_timeout;
let builder = Server::build().workers(1).disable_signals();
let srv = match cfg.stream {
StreamType::Tcp => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(false, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.h1(map_config(factory(), move |_| cfg.clone()))
.tcp()
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(false, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.h2(map_config(factory(), move |_| cfg.clone()))
.tcp()
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(false, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.finish(map_config(factory(), move |_| cfg.clone()))
.tcp()
}),
},
#[cfg(feature = "openssl")]
StreamType::Openssl(acceptor) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.h1(map_config(factory(), move |_| cfg.clone()))
.openssl(acceptor.clone())
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.h2(map_config(factory(), move |_| cfg.clone()))
.openssl(acceptor.clone())
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.finish(map_config(factory(), move |_| cfg.clone()))
.openssl(acceptor.clone())
}),
},
#[cfg(feature = "rustls")]
StreamType::Rustls(config) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.h1(map_config(factory(), move |_| cfg.clone()))
.rustls(config.clone())
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.h2(map_config(factory(), move |_| cfg.clone()))
.rustls(config.clone())
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr));
HttpService::build()
.client_timeout(ctimeout)
.finish(map_config(factory(), move |_| cfg.clone()))
.rustls(config.clone())
}),
},
}
.unwrap();
sys.block_on(async {
let srv = srv.run();
tx.send((System::current(), srv, local_addr)).unwrap();
});
sys.run()
});
let (system, server, addr) = rx.recv().unwrap();
let client = {
let connector = {
#[cfg(feature = "openssl")]
{
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE);
let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
.ssl(builder.build())
}
#[cfg(not(feature = "openssl"))]
{
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
}
};
Client::builder().connector(connector).finish()
};
TestServer {
addr,
client,
system,
ssl,
server,
}
}
#[derive(Clone)]
pub struct TestServerConfig {
tp: HttpVer,
stream: StreamType,
client_timeout: u64,
}
#[derive(Clone)]
enum HttpVer {
Http1,
Http2,
Both,
}
#[derive(Clone)]
enum StreamType {
Tcp,
#[cfg(feature = "openssl")]
Openssl(openssl::ssl::SslAcceptor),
#[cfg(feature = "rustls")]
Rustls(rustls::ServerConfig),
}
impl Default for TestServerConfig {
fn default() -> Self {
TestServerConfig::new()
}
}
/// Create default test server config
pub fn config() -> TestServerConfig {
TestServerConfig::new()
}
impl TestServerConfig {
/// Create default server configuration
pub(crate) fn new() -> TestServerConfig {
TestServerConfig {
tp: HttpVer::Both,
stream: StreamType::Tcp,
client_timeout: 5000,
}
}
/// Start HTTP/1.1 server only
pub fn h1(mut self) -> Self {
self.tp = HttpVer::Http1;
self
}
/// Start HTTP/2 server only
pub fn h2(mut self) -> Self {
self.tp = HttpVer::Http2;
self
}
/// Start openssl server
#[cfg(feature = "openssl")]
pub fn openssl(mut self, acceptor: openssl::ssl::SslAcceptor) -> Self {
self.stream = StreamType::Openssl(acceptor);
self
}
/// Start rustls server
#[cfg(feature = "rustls")]
pub fn rustls(mut self, config: rustls::ServerConfig) -> Self {
self.stream = StreamType::Rustls(config);
self
}
/// Set server client timeout in milliseconds for first request.
pub fn client_timeout(mut self, val: u64) -> Self {
self.client_timeout = val;
self
}
}
/// Get first available unused address
pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = net::TcpListener::from(socket);
tcp.local_addr().unwrap()
}
/// Test server controller
pub struct TestServer {
addr: net::SocketAddr,
client: awc::Client,
system: actix_rt::System,
ssl: bool,
server: Server,
}
impl TestServer {
/// Construct test server url
pub fn addr(&self) -> net::SocketAddr {
self.addr
}
/// Construct test server url
pub fn url(&self, uri: &str) -> String {
let scheme = if self.ssl { "https" } else { "http" };
if uri.starts_with('/') {
format!("{}://localhost:{}{}", scheme, self.addr.port(), uri)
} else {
format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri)
}
}
/// Create `GET` request
pub fn get<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.get(self.url(path.as_ref()).as_str())
}
/// Create `POST` request
pub fn post<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.post(self.url(path.as_ref()).as_str())
}
/// Create `HEAD` request
pub fn head<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.head(self.url(path.as_ref()).as_str())
}
/// Create `PUT` request
pub fn put<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.put(self.url(path.as_ref()).as_str())
}
/// Create `PATCH` request
pub fn patch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.patch(self.url(path.as_ref()).as_str())
}
/// Create `DELETE` request
pub fn delete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.delete(self.url(path.as_ref()).as_str())
}
/// Create `OPTIONS` request
pub fn options<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.options(self.url(path.as_ref()).as_str())
}
/// Connect to test HTTP server
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
self.client.request(method, path.as_ref())
}
pub async fn load_body<S>(
&mut self,
mut response: ClientResponse<S>,
) -> Result<Bytes, PayloadError>
where
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
{
response.body().limit(10_485_760).await
}
/// Connect to WebSocket server at a given path.
pub async fn ws_at(
&mut self,
path: &str,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
let url = self.url(path);
let connect = self.client.ws(url).connect();
connect.await.map(|(_, framed)| framed)
}
/// Connect to a WebSocket server.
pub async fn ws(
&mut self,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
self.ws_at("/").await
}
/// Get default HeaderMap of Client.
///
/// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time).
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
self.client.headers()
}
/// Gracefully stop HTTP server
pub async fn stop(self) {
self.server.stop(true).await;
self.system.stop();
sleep(time::Duration::from_millis(100)).await;
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.system.stop()
}
}
#[cfg(test)]
mod tests {
use std::time::SystemTime;
use actix_http::HttpMessage;
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
use super::*;
use crate::{http::header, web, App, HttpResponse, Responder};