From 604496168059ae808422d2871061cf68cf21ce66 Mon Sep 17 00:00:00 2001 From: Jeb Rosen Date: Sat, 27 Jul 2019 09:15:23 -0700 Subject: [PATCH] Update parts of contrib and associated tests and examples: * json * templates * helmet * databases * serve * examples/cookies * examples/handlebars_templates * examples/json * examples/session * examples/static_files * examples/tera_templates --- contrib/lib/Cargo.toml | 1 + contrib/lib/src/databases.rs | 6 +- contrib/lib/src/helmet/helmet.rs | 6 +- contrib/lib/src/json.rs | 80 +++++++++++++--------- contrib/lib/src/lib.rs | 1 + contrib/lib/src/serve.rs | 14 ++-- contrib/lib/src/templates/metadata.rs | 6 +- contrib/lib/src/templates/mod.rs | 29 ++++---- contrib/lib/tests/helmet.rs | 2 +- contrib/lib/tests/static_files.rs | 8 +-- contrib/lib/tests/templates.rs | 2 +- core/lib/src/lib.rs | 1 + examples/cookies/src/tests.rs | 2 +- examples/handlebars_templates/src/tests.rs | 6 +- examples/json/src/tests.rs | 8 +-- examples/session/src/tests.rs | 4 +- examples/static_files/src/tests.rs | 2 +- examples/tera_templates/src/tests.rs | 6 +- 18 files changed, 105 insertions(+), 79 deletions(-) diff --git a/contrib/lib/Cargo.toml b/contrib/lib/Cargo.toml index 40a12e55..e4557dc3 100644 --- a/contrib/lib/Cargo.toml +++ b/contrib/lib/Cargo.toml @@ -42,6 +42,7 @@ memcache_pool = ["databases", "memcache", "r2d2-memcache"] [dependencies] # Global dependencies. +futures-preview = { version = "0.3.0-alpha.17" } rocket_contrib_codegen = { version = "0.5.0-dev", path = "../codegen", optional = true } rocket = { version = "0.5.0-dev", path = "../../core/lib/", default-features = false } log = "0.4" diff --git a/contrib/lib/src/databases.rs b/contrib/lib/src/databases.rs index 33e3560d..d0b9ad60 100644 --- a/contrib/lib/src/databases.rs +++ b/contrib/lib/src/databases.rs @@ -73,7 +73,7 @@ //! Whenever a connection to the database is needed: //! //! ```rust -//! # #![feature(proc_macro_hygiene)] +//! # #![feature(proc_macro_hygiene, async_await)] //! # //! # #[macro_use] extern crate rocket; //! # #[macro_use] extern crate rocket_contrib; @@ -289,7 +289,7 @@ //! connection to a given database: //! //! ```rust -//! # #![feature(proc_macro_hygiene)] +//! # #![feature(proc_macro_hygiene, async_await)] //! # //! # #[macro_use] extern crate rocket; //! # #[macro_use] extern crate rocket_contrib; @@ -311,7 +311,7 @@ //! connection type: //! //! ```rust -//! # #![feature(proc_macro_hygiene)] +//! # #![feature(proc_macro_hygiene, async_await)] //! # //! # #[macro_use] extern crate rocket; //! # #[macro_use] extern crate rocket_contrib; diff --git a/contrib/lib/src/helmet/helmet.rs b/contrib/lib/src/helmet/helmet.rs index c18251d8..dd54bf0d 100644 --- a/contrib/lib/src/helmet/helmet.rs +++ b/contrib/lib/src/helmet/helmet.rs @@ -196,8 +196,10 @@ impl Fairing for SpaceHelmet { } } - fn on_response(&self, _request: &Request<'_>, response: &mut Response<'_>) { - self.apply(response); + fn on_response<'a>(&'a self, _request: &'a Request<'_>, response: &'a mut Response<'_>) -> std::pin::Pin + Send + 'a>> { + Box::pin(async move { + self.apply(response); + }) } fn on_launch(&self, rocket: &Rocket) { diff --git a/contrib/lib/src/json.rs b/contrib/lib/src/json.rs index d0827556..e18d29df 100644 --- a/contrib/lib/src/json.rs +++ b/contrib/lib/src/json.rs @@ -15,14 +15,17 @@ //! ``` use std::ops::{Deref, DerefMut}; -use std::io::{self, Read}; +use std::io; use std::iter::FromIterator; +use futures::io::AsyncReadExt; + use rocket::request::Request; use rocket::outcome::Outcome::*; -use rocket::data::{Outcome, Transform, Transform::*, Transformed, Data, FromData}; +use rocket::data::{Transform::*, Transformed, Data, FromData, TransformFuture, FromDataFuture}; use rocket::response::{self, Responder, content}; use rocket::http::Status; +use rocket::AsyncReadExt as _; use serde::{Serialize, Serializer}; use serde::de::{Deserialize, Deserializer}; @@ -41,7 +44,7 @@ pub use serde_json::{json_internal, json_internal_vec}; /// or from [`serde`]. The data is parsed from the HTTP request body. /// /// ```rust -/// # #![feature(proc_macro_hygiene)] +/// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # extern crate rocket_contrib; /// # type User = usize; @@ -65,7 +68,7 @@ pub use serde_json::{json_internal, json_internal_vec}; /// set to `application/json` automatically. /// /// ```rust -/// # #![feature(proc_macro_hygiene)] +/// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # extern crate rocket_contrib; /// # type User = usize; @@ -133,42 +136,53 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for Json { type Owned = String; type Borrowed = str; - fn transform(r: &Request<'_>, d: Data) -> Transform> { + fn transform(r: &Request<'_>, d: Data) -> TransformFuture<'a, Self::Owned, Self::Error> { let size_limit = r.limits().get("json").unwrap_or(LIMIT); - let mut s = String::with_capacity(512); - match d.open().take(size_limit).read_to_string(&mut s) { - Ok(_) => Borrowed(Success(s)), - Err(e) => Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))) - } + Box::pin(async move { + let mut v = Vec::with_capacity(512); + let mut reader = d.open().take(size_limit); + match reader.read_to_end(&mut v).await { + Ok(_) => { + match String::from_utf8(v) { + Ok(s) => Borrowed(Success(s)), + Err(e) => Borrowed(Failure((Status::BadRequest, JsonError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))))), + } + }, + Err(e) => Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))) + } + }) } - fn from_data(_: &Request<'_>, o: Transformed<'a, Self>) -> Outcome { - let string = try_outcome!(o.borrowed()); - match serde_json::from_str(&string) { - Ok(v) => Success(Json(v)), - Err(e) => { - error_!("Couldn't parse JSON body: {:?}", e); - if e.is_data() { - Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) - } else { - Failure((Status::BadRequest, JsonError::Parse(string, e))) + fn from_data(_: &Request<'_>, o: Transformed<'a, Self>) -> FromDataFuture<'a, Self, Self::Error> { + Box::pin(async move { + let string = try_outcome!(o.borrowed()); + match serde_json::from_str(&string) { + Ok(v) => Success(Json(v)), + Err(e) => { + error_!("Couldn't parse JSON body: {:?}", e); + if e.is_data() { + Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) + } else { + Failure((Status::BadRequest, JsonError::Parse(string, e))) + } } } - } + }) } } /// Serializes the wrapped value into JSON. Returns a response with Content-Type /// JSON and a fixed-size body with the serialized value. If serialization /// fails, an `Err` of `Status::InternalServerError` is returned. -impl<'a, T: Serialize> Responder<'a> for Json { - fn respond_to(self, req: &Request<'_>) -> response::Result<'a> { - serde_json::to_string(&self.0).map(|string| { - content::Json(string).respond_to(req).unwrap() - }).map_err(|e| { - error_!("JSON failed to serialize: {:?}", e); - Status::InternalServerError - }) +impl<'r, T: Serialize> Responder<'r> for Json { + fn respond_to(self, req: &'r Request<'_>) -> response::ResultFuture<'r> { + match serde_json::to_string(&self.0) { + Ok(string) => Box::pin(async move { Ok(content::Json(string).respond_to(req).await.unwrap()) }), + Err(e) => Box::pin(async move { + error_!("JSON failed to serialize: {:?}", e); + Err(Status::InternalServerError) + }) + } } } @@ -210,7 +224,7 @@ impl DerefMut for Json { /// fashion during request handling. This looks something like: /// /// ```rust -/// # #![feature(proc_macro_hygiene)] +/// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket_contrib; /// use rocket_contrib::json::JsonValue; @@ -283,9 +297,9 @@ impl FromIterator for JsonValue where serde_json::Value: FromIterator { /// Serializes the value into JSON. Returns a response with Content-Type JSON /// and a fixed-size body with the serialized value. -impl<'a> Responder<'a> for JsonValue { +impl<'r> Responder<'r> for JsonValue { #[inline] - fn respond_to(self, req: &Request<'_>) -> response::Result<'a> { + fn respond_to(self, req: &'r Request<'_>) -> response::ResultFuture<'r> { content::Json(self.0.to_string()).respond_to(req) } } @@ -305,7 +319,7 @@ impl<'a> Responder<'a> for JsonValue { /// value created with this macro can be returned from a handler as follows: /// /// ```rust -/// # #![feature(proc_macro_hygiene)] +/// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket_contrib; /// use rocket_contrib::json::JsonValue; diff --git a/contrib/lib/src/lib.rs b/contrib/lib/src/lib.rs index dba2846d..a60e3bfe 100644 --- a/contrib/lib/src/lib.rs +++ b/contrib/lib/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(async_await)] #![doc(html_root_url = "https://api.rocket.rs/v0.5")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] diff --git a/contrib/lib/src/serve.rs b/contrib/lib/src/serve.rs index 7cbf8de7..39511b4f 100644 --- a/contrib/lib/src/serve.rs +++ b/contrib/lib/src/serve.rs @@ -18,7 +18,7 @@ use std::path::{PathBuf, Path}; use rocket::{Request, Data, Route}; use rocket::http::{Method, uri::Segments, ext::IntoOwned}; -use rocket::handler::{Handler, Outcome}; +use rocket::handler::{Handler, HandlerFuture, Outcome}; use rocket::response::{NamedFile, Redirect}; /// A bitset representing configurable options for the [`StaticFiles`] handler. @@ -290,18 +290,20 @@ impl Into> for StaticFiles { } impl Handler for StaticFiles { - fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r> { - fn handle_dir<'r>(opt: Options, r: &'r Request<'_>, d: Data, path: &Path) -> Outcome<'r> { + fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> HandlerFuture<'r> { + fn handle_dir<'r>(opt: Options, r: &'r Request<'_>, d: Data, path: &Path) -> HandlerFuture<'r> { if opt.contains(Options::NormalizeDirs) && !r.uri().path().ends_with('/') { let new_path = r.uri().map_path(|p| p.to_owned() + "/") .expect("adding a trailing slash to a known good path results in a valid path") .into_owned(); - return Outcome::from_or_forward(r, d, Redirect::permanent(new_path)); + return Box::pin(async move { + Outcome::from_or_forward(r, d, Redirect::permanent(new_path)) + }); } if !opt.contains(Options::Index) { - return Outcome::forward(d); + return Box::pin(async move { Outcome::forward(d) }); } let file = NamedFile::open(path.join("index.html")).ok(); @@ -327,7 +329,7 @@ impl Handler for StaticFiles { match &path { Some(path) if path.is_dir() => handle_dir(self.options, req, data, path), Some(path) => Outcome::from_or_forward(req, data, NamedFile::open(path).ok()), - None => Outcome::forward(data) + None => Box::pin(async move { Outcome::forward(data) }), } } } diff --git a/contrib/lib/src/templates/metadata.rs b/contrib/lib/src/templates/metadata.rs index b60bb673..4503fa3e 100644 --- a/contrib/lib/src/templates/metadata.rs +++ b/contrib/lib/src/templates/metadata.rs @@ -12,7 +12,7 @@ use crate::templates::ContextManager; /// used as a request guard in any request handler. /// /// ```rust -/// # #![feature(proc_macro_hygiene)] +/// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket_contrib; /// use rocket_contrib::templates::{Template, Metadata}; @@ -46,7 +46,7 @@ impl Metadata<'_> { /// # Example /// /// ```rust - /// # #![feature(proc_macro_hygiene)] + /// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # extern crate rocket_contrib; /// # @@ -67,7 +67,7 @@ impl Metadata<'_> { /// # Example /// /// ```rust - /// # #![feature(proc_macro_hygiene)] + /// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # extern crate rocket_contrib; /// # diff --git a/contrib/lib/src/templates/mod.rs b/contrib/lib/src/templates/mod.rs index 9d3dbb4a..ef18f1e1 100644 --- a/contrib/lib/src/templates/mod.rs +++ b/contrib/lib/src/templates/mod.rs @@ -37,7 +37,7 @@ //! of the template file minus the last two extensions, from a handler. //! //! ```rust -//! # #![feature(proc_macro_hygiene)] +//! # #![feature(proc_macro_hygiene, async_await)] //! # #[macro_use] extern crate rocket; //! # #[macro_use] extern crate rocket_contrib; //! # fn context() { } @@ -180,7 +180,7 @@ const DEFAULT_TEMPLATE_DIR: &str = "templates"; /// returned from a request handler directly: /// /// ```rust -/// # #![feature(proc_macro_hygiene)] +/// # #![feature(proc_macro_hygiene, async_await)] /// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket_contrib; /// # fn context() { } @@ -383,16 +383,21 @@ impl Template { /// Returns a response with the Content-Type derived from the template's /// extension and a fixed-size body containing the rendered template. If /// rendering fails, an `Err` of `Status::InternalServerError` is returned. -impl Responder<'static> for Template { - fn respond_to(self, req: &Request<'_>) -> response::Result<'static> { - let ctxt = req.guard::>().succeeded().ok_or_else(|| { - error_!("Uninitialized template context: missing fairing."); - info_!("To use templates, you must attach `Template::fairing()`."); - info_!("See the `Template` documentation for more information."); - Status::InternalServerError - })?.inner().context(); +impl<'r> Responder<'r> for Template { + fn respond_to(self, req: &'r Request<'_>) -> response::ResultFuture<'r> { + Box::pin(async move { + let (render, content_type) = { + let ctxt = req.guard::>().succeeded().ok_or_else(|| { + error_!("Uninitialized template context: missing fairing."); + info_!("To use templates, you must attach `Template::fairing()`."); + info_!("See the `Template` documentation for more information."); + Status::InternalServerError + })?.inner().context(); - let (render, content_type) = self.finalize(&ctxt)?; - Content(content_type, render).respond_to(req) + self.finalize(&ctxt)? + }; + + Content(content_type, render).respond_to(req).await + }) } } diff --git a/contrib/lib/tests/helmet.rs b/contrib/lib/tests/helmet.rs index 7ece1df7..2b0f053e 100644 --- a/contrib/lib/tests/helmet.rs +++ b/contrib/lib/tests/helmet.rs @@ -1,4 +1,4 @@ -#![feature(proc_macro_hygiene)] +#![feature(proc_macro_hygiene, async_await)] #[macro_use] #[cfg(feature = "helmet")] diff --git a/contrib/lib/tests/static_files.rs b/contrib/lib/tests/static_files.rs index b65acf09..0d290ac1 100644 --- a/contrib/lib/tests/static_files.rs +++ b/contrib/lib/tests/static_files.rs @@ -1,4 +1,4 @@ -#![feature(proc_macro_hygiene)] +#![feature(proc_macro_hygiene, async_await)] #[cfg(feature = "serve")] mod static_tests { @@ -59,7 +59,7 @@ mod static_tests { let mut file = File::open(path).expect("open file"); let mut expected_contents = String::new(); file.read_to_string(&mut expected_contents).expect("read file"); - assert_eq!(response.body_string(), Some(expected_contents)); + assert_eq!(response.body_string_wait(), Some(expected_contents)); } else { assert_eq!(response.status(), Status::NotFound); } @@ -135,11 +135,11 @@ mod static_tests { let mut response = client.get("/default/ireallydontexist").dispatch(); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body_string().unwrap(), "ireallydontexist"); + assert_eq!(response.body_string_wait().unwrap(), "ireallydontexist"); let mut response = client.get("/default/idont/exist").dispatch(); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body_string().unwrap(), "idont/exist"); + assert_eq!(response.body_string_wait().unwrap(), "idont/exist"); assert_all(&client, "both", REGULAR_FILES, true); assert_all(&client, "both", HIDDEN_FILES, true); diff --git a/contrib/lib/tests/templates.rs b/contrib/lib/tests/templates.rs index 25c62f90..371adfd5 100644 --- a/contrib/lib/tests/templates.rs +++ b/contrib/lib/tests/templates.rs @@ -1,4 +1,4 @@ -#![feature(proc_macro_hygiene)] +#![feature(proc_macro_hygiene, async_await)] #[cfg(feature = "templates")] #[macro_use] extern crate rocket; diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 9adb9f2e..867766ec 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -137,6 +137,7 @@ pub use crate::router::Route; pub use crate::request::{Request, State}; pub use crate::catcher::Catcher; pub use crate::rocket::Rocket; +pub use ext::AsyncReadExt; /// Alias to [`Rocket::ignite()`] Creates a new instance of `Rocket`. pub fn ignite() -> Rocket { diff --git a/examples/cookies/src/tests.rs b/examples/cookies/src/tests.rs index 4f62c28e..34071efb 100644 --- a/examples/cookies/src/tests.rs +++ b/examples/cookies/src/tests.rs @@ -30,7 +30,7 @@ fn test_body(optional_cookie: Option>, expected_body: String) { }; assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body_string(), Some(expected_body)); + assert_eq!(response.body_string_wait(), Some(expected_body)); } #[test] diff --git a/examples/handlebars_templates/src/tests.rs b/examples/handlebars_templates/src/tests.rs index 89d159f9..e653d361 100644 --- a/examples/handlebars_templates/src/tests.rs +++ b/examples/handlebars_templates/src/tests.rs @@ -33,7 +33,7 @@ fn test_root() { let expected = Template::show(client.rocket(), "error/404", &map).unwrap(); assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.body_string(), Some(expected)); + assert_eq!(response.body_string_wait(), Some(expected)); }); } } @@ -51,7 +51,7 @@ fn test_name() { let expected = Template::show(client.rocket(), "index", &context).unwrap(); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body_string(), Some(expected)); + assert_eq!(response.body_string_wait(), Some(expected)); }); } @@ -64,6 +64,6 @@ fn test_404() { let expected = Template::show(client.rocket(), "error/404", &map).unwrap(); assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.body_string(), Some(expected)); + assert_eq!(response.body_string_wait(), Some(expected)); }); } diff --git a/examples/json/src/tests.rs b/examples/json/src/tests.rs index 8b690937..21ecf3cd 100644 --- a/examples/json/src/tests.rs +++ b/examples/json/src/tests.rs @@ -10,13 +10,13 @@ fn bad_get_put() { let mut res = client.get("/message/99").header(ContentType::JSON).dispatch(); assert_eq!(res.status(), Status::NotFound); - let body = res.body_string().unwrap(); + let body = res.body_string_wait().unwrap(); assert!(body.contains("error")); assert!(body.contains("Resource was not found.")); // Try to get a message with an invalid ID. let mut res = client.get("/message/hi").header(ContentType::JSON).dispatch(); - let body = res.body_string().unwrap(); + let body = res.body_string_wait().unwrap(); assert_eq!(res.status(), Status::NotFound); assert!(body.contains("error")); @@ -52,7 +52,7 @@ fn post_get_put_get() { // Check that the message exists with the correct contents. let mut res = client.get("/message/1").header(ContentType::JSON).dispatch(); assert_eq!(res.status(), Status::Ok); - let body = res.body().unwrap().into_string().unwrap(); + let body = res.body_string_wait().unwrap(); assert!(body.contains("Hello, world!")); // Change the message contents. @@ -66,7 +66,7 @@ fn post_get_put_get() { // Check that the message exists with the updated contents. let mut res = client.get("/message/1").header(ContentType::JSON).dispatch(); assert_eq!(res.status(), Status::Ok); - let body = res.body().unwrap().into_string().unwrap(); + let body = res.body_string_wait().unwrap(); assert!(!body.contains("Hello, world!")); assert!(body.contains("Bye bye, world!")); } diff --git a/examples/session/src/tests.rs b/examples/session/src/tests.rs index 556a14f9..417e288c 100644 --- a/examples/session/src/tests.rs +++ b/examples/session/src/tests.rs @@ -35,7 +35,7 @@ fn can_login() { let client = Client::new(rocket()).unwrap(); let mut response = client.get("/login").dispatch(); - let body = response.body_string().unwrap(); + let body = response.body_string_wait().unwrap(); assert_eq!(response.status(), Status::Ok); assert!(body.contains("Please login to continue.")); } @@ -54,7 +54,7 @@ fn login_logout_succeeds() { // Ensure we're logged in. let mut response = client.get("/").cookie(login_cookie.clone()).dispatch(); - let body = response.body_string().unwrap(); + let body = response.body_string_wait().unwrap(); assert_eq!(response.status(), Status::Ok); assert!(body.contains("Logged in with user ID 1")); diff --git a/examples/static_files/src/tests.rs b/examples/static_files/src/tests.rs index c7b5d443..e61c3785 100644 --- a/examples/static_files/src/tests.rs +++ b/examples/static_files/src/tests.rs @@ -13,7 +13,7 @@ fn test_query_file (path: &str, file: T, status: Status) let mut response = client.get(path).dispatch(); assert_eq!(response.status(), status); - let body_data = response.body().and_then(|body| body.into_bytes()); + let body_data = response.body_bytes_wait(); if let Some(filename) = file.into() { let expected_data = read_file_content(filename); assert!(body_data.map_or(false, |s| s == expected_data)); diff --git a/examples/tera_templates/src/tests.rs b/examples/tera_templates/src/tests.rs index 9fc00270..d3502957 100644 --- a/examples/tera_templates/src/tests.rs +++ b/examples/tera_templates/src/tests.rs @@ -32,7 +32,7 @@ fn test_root() { let expected = Template::show(client.rocket(), "error/404", &map).unwrap(); assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.body_string(), Some(expected)); + assert_eq!(response.body_string_wait(), Some(expected)); }); } } @@ -48,7 +48,7 @@ fn test_name() { let expected = Template::show(client.rocket(), "index", &context).unwrap(); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body_string(), Some(expected)); + assert_eq!(response.body_string_wait(), Some(expected)); }); } @@ -61,6 +61,6 @@ fn test_404() { let expected = Template::show(client.rocket(), "error/404", &map).unwrap(); assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.body_string(), Some(expected)); + assert_eq!(response.body_string_wait(), Some(expected)); }); }