//! Various helpers for Actix applications to use during testing. use std::rc::Rc; use std::str::FromStr; use std::sync::mpsc; use std::{net, thread}; use actix_inner::{Actor, Addr, System}; use cookie::Cookie; use futures::Future; use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; use net2::TcpBuilder; use tokio::runtime::current_thread::Runtime; #[cfg(feature = "alpn")] use openssl::ssl::SslAcceptorBuilder; #[cfg(feature = "rust-tls")] use rustls::ServerConfig; #[cfg(feature = "rust-tls")] use server::RustlsAcceptor; use application::{App, HttpApplication}; use body::Binary; use client::{ClientConnector, ClientRequest, ClientRequestBuilder}; use error::Error; use handler::{AsyncResultItem, Handler, Responder}; use header::{Header, IntoHeaderValue}; use httprequest::HttpRequest; use httpresponse::HttpResponse; use middleware::Middleware; use param::Params; use payload::Payload; use resource::Resource; use router::Router; use server::message::{Request, RequestPool}; use server::{HttpServer, IntoHttpHandler, ServerSettings}; use uri::Url as InnerUrl; use ws; /// The `TestServer` type. /// /// `TestServer` is very simple test server that simplify process of writing /// integration tests cases for actix web applications. /// /// # Examples /// /// ```rust /// # extern crate actix_web; /// # use actix_web::*; /// # /// # fn my_handler(req: &HttpRequest) -> HttpResponse { /// # HttpResponse::Ok().into() /// # } /// # /// # fn main() { /// use actix_web::test::TestServer; /// /// let mut srv = TestServer::new(|app| app.handler(my_handler)); /// /// let req = srv.get().finish().unwrap(); /// let response = srv.execute(req.send()).unwrap(); /// assert!(response.status().is_success()); /// # } /// ``` pub struct TestServer { addr: net::SocketAddr, ssl: bool, conn: Addr, rt: Runtime, } impl TestServer { /// Start new test server /// /// This method accepts configuration method. You can add /// middlewares or set handlers for test application. pub fn new(config: F) -> Self where F: Sync + Send + 'static + Fn(&mut TestApp<()>), { TestServerBuilder::new(|| ()).start(config) } /// Create test server builder pub fn build() -> TestServerBuilder<()> { TestServerBuilder::new(|| ()) } /// Create test server builder with specific state factory /// /// This method can be used for constructing application state. /// Also it can be used for external dependency initialization, /// like creating sync actors for diesel integration. pub fn build_with_state(state: F) -> TestServerBuilder where F: Fn() -> S + Sync + Send + 'static, S: 'static, { TestServerBuilder::new(state) } /// Start new test server with application factory pub fn with_factory(factory: F) -> Self where F: Fn() -> U + Sync + Send + 'static, U: IntoIterator + 'static, H: IntoHttpHandler + 'static, { let (tx, rx) = mpsc::channel(); // run server in separate thread thread::spawn(move || { let sys = System::new("actix-test-server"); let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let local_addr = tcp.local_addr().unwrap(); HttpServer::new(factory) .disable_signals() .listen(tcp) .start(); tx.send((System::current(), local_addr, TestServer::get_conn())) .unwrap(); sys.run(); }); let (system, addr, conn) = rx.recv().unwrap(); System::set_current(system); TestServer { addr, conn, ssl: false, rt: Runtime::new().unwrap(), } } fn get_conn() -> Addr { #[cfg(feature = "alpn")] { use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE); ClientConnector::with_connector(builder.build()).start() } #[cfg(all(feature = "rust-tls", not(feature = "alpn")))] { use rustls::ClientConfig; use std::fs::File; use std::io::BufReader; let mut config = ClientConfig::new(); let pem_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); config.root_store.add_pem_file(pem_file).unwrap(); ClientConnector::with_connector(config).start() } #[cfg(not(any(feature = "alpn", feature = "rust-tls")))] { ClientConnector::default().start() } } /// Get firat available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); let socket = TcpBuilder::new_v4().unwrap(); socket.bind(&addr).unwrap(); socket.reuse_address(true).unwrap(); let tcp = socket.to_tcp_listener().unwrap(); tcp.local_addr().unwrap() } /// Construct test server url pub fn addr(&self) -> net::SocketAddr { self.addr } /// Construct test server url pub fn url(&self, uri: &str) -> String { if uri.starts_with('/') { format!( "{}://localhost:{}{}", if self.ssl { "https" } else { "http" }, self.addr.port(), uri ) } else { format!( "{}://localhost:{}/{}", if self.ssl { "https" } else { "http" }, self.addr.port(), uri ) } } /// Stop http server fn stop(&mut self) { System::current().stop(); } /// Execute future on current core pub fn execute(&mut self, fut: F) -> Result where F: Future, { self.rt.block_on(fut) } /// Connect to websocket server at a given path pub fn ws_at( &mut self, path: &str, ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { let url = self.url(path); self.rt .block_on(ws::Client::with_connector(url, self.conn.clone()).connect()) } /// Connect to a websocket server pub fn ws( &mut self, ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { self.ws_at("/") } /// Create `GET` request pub fn get(&self) -> ClientRequestBuilder { ClientRequest::get(self.url("/").as_str()) } /// Create `POST` request pub fn post(&self) -> ClientRequestBuilder { ClientRequest::post(self.url("/").as_str()) } /// Create `HEAD` request pub fn head(&self) -> ClientRequestBuilder { ClientRequest::head(self.url("/").as_str()) } /// Connect to test http server pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { ClientRequest::build() .method(meth) .uri(self.url(path).as_str()) .with_connector(self.conn.clone()) .take() } } impl Drop for TestServer { fn drop(&mut self) { self.stop() } } /// An `TestServer` builder /// /// This type can be used to construct an instance of `TestServer` through a /// builder-like pattern. pub struct TestServerBuilder { state: Box S + Sync + Send + 'static>, #[cfg(feature = "alpn")] ssl: Option, #[cfg(feature = "rust-tls")] rust_ssl: Option, } impl TestServerBuilder { /// Create a new test server pub fn new(state: F) -> TestServerBuilder where F: Fn() -> S + Sync + Send + 'static, { TestServerBuilder { state: Box::new(state), #[cfg(feature = "alpn")] ssl: None, #[cfg(feature = "rust-tls")] rust_ssl: None, } } #[cfg(feature = "alpn")] /// Create ssl server pub fn ssl(mut self, ssl: SslAcceptorBuilder) -> Self { self.ssl = Some(ssl); self } #[cfg(feature = "rust-tls")] /// Create rust tls server pub fn rustls(mut self, ssl: ServerConfig) -> Self { self.rust_ssl = Some(ssl); self } #[allow(unused_mut)] /// Configure test application and run test server pub fn start(mut self, config: F) -> TestServer where F: Sync + Send + 'static + Fn(&mut TestApp), { let (tx, rx) = mpsc::channel(); let mut has_ssl = false; #[cfg(feature = "alpn")] { has_ssl = has_ssl || self.ssl.is_some(); } #[cfg(feature = "rust-tls")] { has_ssl = has_ssl || self.rust_ssl.is_some(); } // run server in separate thread thread::spawn(move || { let addr = TestServer::unused_addr(); let sys = System::new("actix-test-server"); let state = self.state; let mut srv = HttpServer::new(move || { let mut app = TestApp::new(state()); config(&mut app); vec![app] }).workers(1) .disable_signals(); tx.send((System::current(), addr, TestServer::get_conn())) .unwrap(); #[cfg(feature = "alpn")] { let ssl = self.ssl.take(); if let Some(ssl) = ssl { let tcp = net::TcpListener::bind(addr).unwrap(); srv = srv.listen_ssl(tcp, ssl).unwrap(); } } #[cfg(feature = "rust-tls")] { let ssl = self.rust_ssl.take(); if let Some(ssl) = ssl { let tcp = net::TcpListener::bind(addr).unwrap(); srv = srv.listen_with(tcp, RustlsAcceptor::new(ssl)).unwrap(); } } if !has_ssl { let tcp = net::TcpListener::bind(addr).unwrap(); srv = srv.listen(tcp); } srv.start(); sys.run(); }); let (system, addr, conn) = rx.recv().unwrap(); System::set_current(system); TestServer { addr, conn, ssl: has_ssl, rt: Runtime::new().unwrap(), } } } /// Test application helper for testing request handlers. pub struct TestApp { app: Option>, } impl TestApp { fn new(state: S) -> TestApp { let app = App::with_state(state); TestApp { app: Some(app) } } /// Register handler for "/" pub fn handler(&mut self, handler: F) where F: Fn(&HttpRequest) -> R + 'static, R: Responder + 'static, { self.app = Some(self.app.take().unwrap().resource("/", |r| r.f(handler))); } /// Register middleware pub fn middleware(&mut self, mw: T) -> &mut TestApp where T: Middleware + 'static, { self.app = Some(self.app.take().unwrap().middleware(mw)); self } /// Register resource. This method is similar /// to `App::resource()` method. pub fn resource(&mut self, path: &str, f: F) -> &mut TestApp where F: FnOnce(&mut Resource) -> R + 'static, { self.app = Some(self.app.take().unwrap().resource(path, f)); self } } impl IntoHttpHandler for TestApp { type Handler = HttpApplication; fn into_handler(mut self) -> HttpApplication { self.app.take().unwrap().into_handler() } } #[doc(hidden)] impl Iterator for TestApp { type Item = HttpApplication; fn next(&mut self) -> Option { if let Some(mut app) = self.app.take() { Some(app.finish()) } else { None } } } /// Test `HttpRequest` builder /// /// ```rust /// # extern crate http; /// # extern crate actix_web; /// # use http::{header, StatusCode}; /// # use actix_web::*; /// use actix_web::test::TestRequest; /// /// fn index(req: &HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { /// HttpResponse::Ok().into() /// } else { /// HttpResponse::BadRequest().into() /// } /// } /// /// fn main() { /// let resp = TestRequest::with_header("content-type", "text/plain") /// .run(&index) /// .unwrap(); /// assert_eq!(resp.status(), StatusCode::OK); /// /// let resp = TestRequest::default().run(&index).unwrap(); /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` pub struct TestRequest { state: S, version: Version, method: Method, uri: Uri, headers: HeaderMap, params: Params, cookies: Option>>, payload: Option, prefix: u16, } impl Default for TestRequest<()> { fn default() -> TestRequest<()> { TestRequest { state: (), method: Method::GET, uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), params: Params::new(), cookies: None, payload: None, prefix: 0, } } } impl TestRequest<()> { /// Create TestRequest and set request uri pub fn with_uri(path: &str) -> TestRequest<()> { TestRequest::default().uri(path) } /// Create TestRequest and set header pub fn with_hdr(hdr: H) -> TestRequest<()> { TestRequest::default().set(hdr) } /// Create TestRequest and set header pub fn with_header(key: K, value: V) -> TestRequest<()> where HeaderName: HttpTryFrom, V: IntoHeaderValue, { TestRequest::default().header(key, value) } } impl TestRequest { /// Start HttpRequest build process with application state pub fn with_state(state: S) -> TestRequest { TestRequest { state, method: Method::GET, uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), params: Params::new(), cookies: None, payload: None, prefix: 0, } } /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { self.version = ver; self } /// Set HTTP method of this request pub fn method(mut self, meth: Method) -> Self { self.method = meth; self } /// Set HTTP Uri of this request pub fn uri(mut self, path: &str) -> Self { self.uri = Uri::from_str(path).unwrap(); self } /// Set a header pub fn set(mut self, hdr: H) -> Self { if let Ok(value) = hdr.try_into() { self.headers.append(H::name(), value); return self; } panic!("Can not set header"); } /// Set a header pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { if let Ok(key) = HeaderName::try_from(key) { if let Ok(value) = value.try_into() { self.headers.append(key, value); return self; } } panic!("Can not create header"); } /// Set request path pattern parameter pub fn param(mut self, name: &'static str, value: &'static str) -> Self { self.params.add_static(name, value); self } /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { let mut data = data.into(); let mut payload = Payload::empty(); payload.unread_data(data.take()); self.payload = Some(payload); self } /// Set request's prefix pub fn prefix(mut self, prefix: u16) -> Self { self.prefix = prefix; self } /// Complete request creation and generate `HttpRequest` instance pub fn finish(self) -> HttpRequest { let TestRequest { state, method, uri, version, headers, mut params, cookies, payload, prefix, } = self; let router = Router::<()>::default(); let pool = RequestPool::pool(ServerSettings::default()); let mut req = RequestPool::get(pool); { let inner = req.inner_mut(); inner.method = method; inner.url = InnerUrl::new(uri); inner.version = version; inner.headers = headers; *inner.payload.borrow_mut() = payload; } params.set_url(req.url().clone()); let mut info = router.route_info_params(0, params); info.set_prefix(prefix); let mut req = HttpRequest::new(req, Rc::new(state), info); req.set_cookies(cookies); req } #[cfg(test)] /// Complete request creation and generate `HttpRequest` instance pub(crate) fn finish_with_router(self, router: Router) -> HttpRequest { let TestRequest { state, method, uri, version, headers, mut params, cookies, payload, prefix, } = self; let pool = RequestPool::pool(ServerSettings::default()); let mut req = RequestPool::get(pool); { let inner = req.inner_mut(); inner.method = method; inner.url = InnerUrl::new(uri); inner.version = version; inner.headers = headers; *inner.payload.borrow_mut() = payload; } params.set_url(req.url().clone()); let mut info = router.route_info_params(0, params); info.set_prefix(prefix); let mut req = HttpRequest::new(req, Rc::new(state), info); req.set_cookies(cookies); req } /// Complete request creation and generate server `Request` instance pub fn request(self) -> Request { let TestRequest { method, uri, version, headers, payload, .. } = self; let pool = RequestPool::pool(ServerSettings::default()); let mut req = RequestPool::get(pool); { let inner = req.inner_mut(); inner.method = method; inner.url = InnerUrl::new(uri); inner.version = version; inner.headers = headers; *inner.payload.borrow_mut() = payload; } req } /// This method generates `HttpRequest` instance and runs handler /// with generated request. /// /// This method panics is handler returns actor or async result. pub fn run>(self, h: &H) -> Result { let req = self.finish(); let resp = h.handle(&req); match resp.respond_to(&req) { Ok(resp) => match resp.into().into() { AsyncResultItem::Ok(resp) => Ok(resp), AsyncResultItem::Err(err) => Err(err), AsyncResultItem::Future(_) => panic!("Async handler is not supported."), }, Err(err) => Err(err.into()), } } /// This method generates `HttpRequest` instance and runs handler /// with generated request. /// /// This method panics is handler returns actor. pub fn run_async(self, h: H) -> Result where H: Fn(HttpRequest) -> F + 'static, F: Future + 'static, R: Responder + 'static, E: Into + 'static, { let req = self.finish(); let fut = h(req.clone()); let mut core = Runtime::new().unwrap(); match core.block_on(fut) { Ok(r) => match r.respond_to(&req) { Ok(reply) => match reply.into().into() { AsyncResultItem::Ok(resp) => Ok(resp), _ => panic!("Nested async replies are not supported"), }, Err(e) => Err(e), }, Err(err) => Err(err), } } }