From f9078d41cd0c05089101b9c7df167133d17c0991 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 14 Apr 2019 19:52:12 -0700 Subject: [PATCH] add test::read_response; fix TestRequest::app_data() --- CHANGES.md | 12 +++ awc/src/ws.rs | 4 +- src/data.rs | 1 + src/test.rs | 255 ++++++++++++++++++++++++++++++++++++++------------ src/web.rs | 12 +-- 5 files changed, 213 insertions(+), 71 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d1405086..0d40cf31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Changes +## [1.0.0-alpha.7] - 2019-04-xx + +### Added + +* Added helper functions for reading test response body, + `test::read_response()` and test::read_response_json()` + +### Fixed + +* Fixed `TestRequest::app_data()` + + ## [1.0.0-alpha.6] - 2019-04-14 ### Changed diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 5ed37945..4f0983dc 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -306,9 +306,7 @@ impl WebsocketsRequest { } } else { log::trace!("Invalid connection header: {:?}", conn); - return Err(WsClientError::InvalidConnectionHeader( - conn.clone(), - )); + return Err(WsClientError::InvalidConnectionHeader(conn.clone())); } } else { log::trace!("Missing connection header"); diff --git a/src/data.rs b/src/data.rs index e0eb8fa9..0c896fcc 100644 --- a/src/data.rs +++ b/src/data.rs @@ -61,6 +61,7 @@ pub(crate) trait DataFactoryResult { /// web::get().to(index))); /// } /// ``` +#[derive(Debug)] pub struct Data(Arc); impl Data { diff --git a/src/test.rs b/src/test.rs index 34218704..638bcdce 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::cookie::Cookie; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; -use actix_http::http::{HttpTryFrom, Method, StatusCode, Version}; +use actix_http::http::{HttpTryFrom, Method, StatusCode, Uri, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Url}; @@ -12,14 +12,17 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{FnService, IntoNewService, NewService, Service}; use bytes::{Bytes, BytesMut}; -use futures::{future::{lazy, ok, Future}, stream::Stream}; +use futures::{ + future::{lazy, ok, Future}, + stream::Stream, +}; use serde::de::DeserializeOwned; use serde_json; pub use actix_http::test::TestBuffer; use crate::config::{AppConfig, AppConfigInner}; -use crate::data::RouteData; +use crate::data::{Data, RouteData}; use crate::dev::{Body, MessageBody, Payload}; use crate::request::HttpRequestPool; use crate::rmap::ResourceMap; @@ -81,11 +84,12 @@ pub fn default_service( /// This method accepts application builder instance, and constructs /// service. /// -/// ```rust,ignore +/// ```rust /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// fn main() { +/// #[test] +/// fn test_init_service() { /// let mut app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| HttpResponse::Ok())) @@ -118,11 +122,12 @@ where /// Calls service and waits for response future completion. /// -/// ```rust,ignore +/// ```rust /// use actix_web::{test, App, HttpResponse, http::StatusCode}; /// use actix_service::Service; /// -/// fn main() { +/// #[test] +/// fn test_response() { /// let mut app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| HttpResponse::Ok())) @@ -144,6 +149,101 @@ where block_on(app.call(req)).unwrap() } +/// Helper function that returns a response body of a TestRequest +/// This function blocks the current thread until futures complete. +/// +/// ```rust +/// use actix_web::{test, web, App, HttpResponse, http::header}; +/// use bytes::Bytes; +/// +/// #[test] +/// fn test_index() { +/// let mut app = test::init_service( +/// App::new().service( +/// web::resource("/index.html") +/// .route(web::post().to( +/// || HttpResponse::Ok().body("welcome!"))))); +/// +/// let req = test::TestRequest::post() +/// .uri("/index.html") +/// .header(header::CONTENT_TYPE, "application/json") +/// .to_request(); +/// +/// let result = test::read_response(&mut app, req); +/// assert_eq!(result, Bytes::from_static(b"welcome!")); +/// } +/// ``` +pub fn read_response(app: &mut S, req: Request) -> Bytes +where + S: Service, Error = Error>, + B: MessageBody, +{ + block_on(app.call(req).and_then(|mut resp: ServiceResponse| { + resp.take_body() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }) + .map(|body: BytesMut| body.freeze()) + })) + .unwrap_or_else(|_| panic!("read_response failed at block_on unwrap")) +} + +/// Helper function that returns a deserialized response body of a TestRequest +/// This function blocks the current thread until futures complete. +/// +/// ```rust +/// use actix_web::{App, test, web, HttpResponse, http::header}; +/// use serde::{Serialize, Deserialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct Person { +/// id: String, +/// name: String +/// } +/// +/// #[test] +/// fn test_add_person() { +/// let mut app = test::init_service( +/// App::new().service( +/// web::resource("/people") +/// .route(web::post().to(|person: web::Json| { +/// HttpResponse::Ok() +/// .json(person.into_inner())}) +/// ))); +/// +/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); +/// +/// let req = test::TestRequest::post() +/// .uri("/people") +/// .header(header::CONTENT_TYPE, "application/json") +/// .set_payload(payload) +/// .to_request(); +/// +/// let result: Person = test::read_response_json(&mut app, req); +/// } +/// ``` +pub fn read_response_json(app: &mut S, req: Request) -> T +where + S: Service, Error = Error>, + B: MessageBody, + T: DeserializeOwned, +{ + block_on(app.call(req).and_then(|mut resp: ServiceResponse| { + resp.take_body() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }) + .and_then(|body: BytesMut| { + ok(serde_json::from_slice(&body).unwrap_or_else(|_| { + panic!("read_response_json failed during deserialization") + })) + }) + })) + .unwrap_or_else(|_| panic!("read_response_json failed at block_on unwrap")) +} + /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. @@ -153,7 +253,7 @@ where /// * `TestRequest::to_from` creates `ServiceFromRequest` instance, which is used for testing extractors. /// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// -/// ```rust,ignore +/// ```rust /// # use futures::IntoFuture; /// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; /// use actix_web::http::{header, StatusCode}; @@ -166,7 +266,8 @@ where /// } /// } /// -/// fn main() { +/// #[test] +/// fn test_index() { /// let req = test::TestRequest::with_header("content-type", "text/plain") /// .to_http_request(); /// @@ -183,6 +284,7 @@ pub struct TestRequest { rmap: ResourceMap, config: AppConfigInner, route_data: Extensions, + path: Path, } impl Default for TestRequest { @@ -192,6 +294,7 @@ impl Default for TestRequest { rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfigInner::default(), route_data: Extensions::new(), + path: Path::new(Url::new(Uri::default())), } } } @@ -267,6 +370,12 @@ impl TestRequest { self } + /// Set request path pattern parameter + pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + self.path.add_static(name, value); + self + } + /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); @@ -276,7 +385,7 @@ impl TestRequest { /// Set application data. This is equivalent of `App::data()` method /// for testing purpose. pub fn app_data(self, data: T) -> Self { - self.config.extensions.borrow_mut().insert(data); + self.config.extensions.borrow_mut().insert(Data::new(data)); self } @@ -302,9 +411,10 @@ impl TestRequest { /// Complete request creation and generate `ServiceRequest` instance pub fn to_srv_request(mut self) -> ServiceRequest { let (head, payload) = self.req.finish().into_parts(); + self.path.get_mut().update(&head.uri); let req = HttpRequest::new( - Path::new(Url::new(head.uri.clone())), + self.path, head, Rc::new(self.rmap), AppConfig::new(self.config), @@ -322,9 +432,10 @@ impl TestRequest { /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { let (head, _) = self.req.finish().into_parts(); + self.path.get_mut().update(&head.uri); let mut req = HttpRequest::new( - Path::new(Url::new(head.uri.clone())), + self.path, head, Rc::new(self.rmap), AppConfig::new(self.config), @@ -337,9 +448,10 @@ impl TestRequest { /// Complete request creation and generate `HttpRequest` and `Payload` instances pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { let (head, payload) = self.req.finish().into_parts(); + self.path.get_mut().update(&head.uri); let mut req = HttpRequest::new( - Path::new(Url::new(head.uri.clone())), + self.path, head, Rc::new(self.rmap), AppConfig::new(self.config), @@ -365,55 +477,74 @@ impl TestRequest { { block_on(f) } +} - /// Helper function that returns a deserialized response body of a TestRequest - /// This function blocks the current thread until futures complete. - /// - /// ```rust - /// use actix_web::{App, test, web, HttpResponse, http::header}; - /// use serde::{Serialize, Deserialize}; - /// - /// #[derive(Serialize, Deserialize)] - /// pub struct Person { id: String, name: String } - /// - /// #[test] - /// fn test_add_person() { - /// let mut app = test::init_service(App::new().service( - /// web::resource("/people") - /// .route(web::post().to(|person: web::Json| { - /// HttpResponse::Ok() - /// .json(person.into_inner())}) - /// ))); - /// - /// let payload = r#"{"id":"12345","name":"Nikolay Kim"}"#.as_bytes(); - /// - /// let req = test::TestRequest::post() - /// .uri("/people") - /// .header(header::CONTENT_TYPE, "application/json") - /// .set_payload(payload) - /// .to_request(); - /// - /// let result: Person = test::read_response_json(&mut app, req); - /// } - /// ``` - pub fn read_response_json(app: &mut S, req: Request) -> T - where - S: Service, Error = Error>, - B: MessageBody, - T: DeserializeOwned, - { - block_on(app.call(req).and_then(|mut resp: ServiceResponse| { - resp.take_body() - .fold(BytesMut::new(), move |mut body, chunk| { - body.extend_from_slice(&chunk); - Ok::<_, Error>(body) - }) - .and_then(|body: BytesMut| { - ok(serde_json::from_slice(&body).unwrap_or_else(|_| { - panic!("read_response_json failed during deserialization") - })) - }) - })) - .unwrap_or_else(|_| panic!("read_response_json failed at block_on unwrap")) +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use std::time::SystemTime; + + use super::*; + use crate::{http::header, web, App, HttpResponse}; + + #[test] + fn test_basics() { + let req = TestRequest::with_hdr(header::ContentType::json()) + .version(Version::HTTP_2) + .set(header::Date(SystemTime::now().into())) + .param("test", "123") + .app_data(10u32) + .to_http_request(); + assert!(req.headers().contains_key(header::CONTENT_TYPE)); + assert!(req.headers().contains_key(header::DATE)); + assert_eq!(&req.match_info()["test"], "123"); + assert_eq!(req.version(), Version::HTTP_2); + let data = req.app_data::().unwrap(); + assert_eq!(*data, 10); + assert_eq!(*data.get_ref(), 10); + } + + #[test] + fn test_response() { + let mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ); + + let req = TestRequest::post() + .uri("/index.html") + .header(header::CONTENT_TYPE, "application/json") + .to_request(); + + let result = read_response(&mut app, req); + assert_eq!(result, Bytes::from_static(b"welcome!")); + } + + #[derive(Serialize, Deserialize)] + pub struct Person { + id: String, + name: String, + } + + #[test] + fn test_response_json() { + let mut app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| { + HttpResponse::Ok().json(person.into_inner()) + }), + ))); + + let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); + + let req = TestRequest::post() + .uri("/people") + .header(header::CONTENT_TYPE, "application/json") + .set_payload(payload) + .to_request(); + + let result: Person = read_response_json(&mut app, req); + assert_eq!(&result.id, "12345"); } } diff --git a/src/web.rs b/src/web.rs index a354222c..ece869b2 100644 --- a/src/web.rs +++ b/src/web.rs @@ -103,7 +103,7 @@ pub fn route() -> Route { /// * /{project_id} /// pub fn get() -> Route { - Route::new().method(Method::GET) + method(Method::GET) } /// Create *route* with `POST` method guard. @@ -123,7 +123,7 @@ pub fn get() -> Route { /// * /{project_id} /// pub fn post() -> Route { - Route::new().method(Method::POST) + method(Method::POST) } /// Create *route* with `PUT` method guard. @@ -143,7 +143,7 @@ pub fn post() -> Route { /// * /{project_id} /// pub fn put() -> Route { - Route::new().method(Method::PUT) + method(Method::PUT) } /// Create *route* with `PATCH` method guard. @@ -163,7 +163,7 @@ pub fn put() -> Route { /// * /{project_id} /// pub fn patch() -> Route { - Route::new().method(Method::PATCH) + method(Method::PATCH) } /// Create *route* with `DELETE` method guard. @@ -183,7 +183,7 @@ pub fn patch() -> Route { /// * /{project_id} /// pub fn delete() -> Route { - Route::new().method(Method::DELETE) + method(Method::DELETE) } /// Create *route* with `HEAD` method guard. @@ -203,7 +203,7 @@ pub fn delete() -> Route { /// * /{project_id} /// pub fn head() -> Route { - Route::new().method(Method::HEAD) + method(Method::HEAD) } /// Create *route* and add method guard.