use crate::internals::DebugResponseBody;
use crate::internals::ErrorMessage;
use crate::internals::RequestPathFormatter;
use crate::internals::StatusCodeFormatter;
use crate::internals::StatusCodeRangeFormatter;
use crate::internals::TryIntoRangeBounds;
use bytes::Bytes;
use cookie::Cookie;
use cookie::CookieJar;
use expect_json::expect;
use expect_json::expect_json_eq;
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
use http::StatusCode;
use http::Version;
use http::header::HeaderName;
use http::header::SET_COOKIE;
use http::response::Parts;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::convert::AsRef;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::fs::File;
use std::fs::read_to_string;
use std::io::BufReader;
use std::ops::RangeBounds;
use std::path::Path;
use url::Url;
#[cfg(feature = "pretty-assertions")]
use pretty_assertions::{assert_eq, assert_ne};
#[cfg(feature = "ws")]
use crate::TestWebSocket;
#[cfg(feature = "ws")]
use crate::internals::TestResponseWebSocket;
#[derive(Debug, Clone)]
pub struct TestResponse {
version: Version,
method: Method,
full_request_url: Url,
headers: HeaderMap<HeaderValue>,
status_code: StatusCode,
response_body: Bytes,
#[cfg(feature = "ws")]
websockets: TestResponseWebSocket,
}
impl TestResponse {
pub(crate) fn new(
version: Version,
method: Method,
full_request_url: Url,
parts: Parts,
response_body: Bytes,
#[cfg(feature = "ws")] websockets: TestResponseWebSocket,
) -> Self {
Self {
version,
method,
full_request_url,
headers: parts.headers,
status_code: parts.status,
response_body,
#[cfg(feature = "ws")]
websockets,
}
}
#[must_use]
pub fn text(&self) -> String {
String::from_utf8_lossy(self.as_bytes()).to_string()
}
#[must_use]
#[track_caller]
pub fn json<T>(&self) -> T
where
T: DeserializeOwned,
{
serde_json::from_slice::<T>(self.as_bytes())
.error_response_with_body("Failed to deserialize Json response", self)
}
#[cfg(feature = "yaml")]
#[must_use]
#[track_caller]
pub fn yaml<T>(&self) -> T
where
T: DeserializeOwned,
{
serde_yaml::from_slice::<T>(self.as_bytes())
.error_response_with_body("Failed to deserialize Yaml response", self)
}
#[cfg(feature = "msgpack")]
#[must_use]
#[track_caller]
pub fn msgpack<T>(&self) -> T
where
T: DeserializeOwned,
{
rmp_serde::from_slice::<T>(self.as_bytes())
.error_response("Failed to deserialize Msgpack response", self)
}
#[must_use]
#[track_caller]
pub fn form<T>(&self) -> T
where
T: DeserializeOwned,
{
serde_urlencoded::from_bytes::<T>(self.as_bytes())
.error_response_with_body("Failed to deserialize Form response", self)
}
#[must_use]
pub fn as_bytes(&self) -> &Bytes {
&self.response_body
}
#[must_use]
pub fn into_bytes(self) -> Bytes {
self.response_body
}
#[must_use]
pub fn status_code(&self) -> StatusCode {
self.status_code
}
#[must_use]
pub fn request_method(&self) -> Method {
self.method.clone()
}
#[must_use]
pub fn request_url(&self) -> Url {
self.full_request_url.clone()
}
#[must_use]
pub fn maybe_header<N>(&self, name: N) -> Option<HeaderValue>
where
N: TryInto<HeaderName>,
N::Error: Debug,
{
let header_name = name
.try_into()
.expect("Failed to build HeaderName from name given");
self.headers.get(header_name).map(|h| h.to_owned())
}
#[must_use]
pub fn headers(&self) -> &HeaderMap<HeaderValue> {
&self.headers
}
#[must_use]
#[track_caller]
pub fn maybe_content_type(&self) -> Option<String> {
self.headers.get(http::header::CONTENT_TYPE).map(|header| {
header
.to_str()
.error_message_fn(|| {
format!("Failed to decode header CONTENT_TYPE, received '{header:?}'")
})
.to_string()
})
}
#[must_use]
pub fn content_type(&self) -> String {
self.maybe_content_type()
.expect("CONTENT_TYPE not found in response header")
}
#[must_use]
#[track_caller]
pub fn header<N>(&self, name: N) -> HeaderValue
where
N: TryInto<HeaderName> + Display + Clone,
N::Error: Debug,
{
let debug_header = name.clone();
let header_name = name
.try_into()
.expect("Failed to build HeaderName from name given, '{debug_header}'");
self.headers
.get(header_name)
.map(|h| h.to_owned())
.error_response_fn(|| format!("Cannot find header {debug_header}"), self)
}
pub fn iter_headers(&self) -> impl Iterator<Item = (&'_ HeaderName, &'_ HeaderValue)> {
self.headers.iter()
}
pub fn iter_headers_by_name<N>(&self, name: N) -> impl Iterator<Item = &'_ HeaderValue>
where
N: TryInto<HeaderName>,
N::Error: Debug,
{
let header_name = name
.try_into()
.expect("Failed to build HeaderName from name given");
self.headers.get_all(header_name).iter()
}
#[must_use]
pub fn contains_header<N>(&self, name: N) -> bool
where
N: TryInto<HeaderName>,
N::Error: Debug,
{
let header_name = name
.try_into()
.expect("Failed to build HeaderName from name given");
self.headers.contains_key(header_name)
}
#[track_caller]
pub fn assert_contains_header<N>(&self, name: N) -> &Self
where
N: TryInto<HeaderName> + Display + Clone,
N::Error: Debug,
{
let debug_header_name = name.clone();
let debug_request_format = self.debug_request_format();
let has_header = self.contains_header(name);
assert!(
has_header,
"Expected header '{debug_header_name}' to be present in response, header was not found, for request {debug_request_format}"
);
self
}
#[track_caller]
pub fn assert_header<N, V>(&self, name: N, value: V) -> &Self
where
N: TryInto<HeaderName> + Display + Clone,
N::Error: Debug,
V: TryInto<HeaderValue>,
V::Error: Debug,
{
let debug_header_name = name.clone();
let header_name = name
.try_into()
.expect("Failed to build HeaderName from name given");
let expected_header_value = value
.try_into()
.expect("Could not turn given value into HeaderValue");
let debug_request_format = self.debug_request_format();
let maybe_found_header_value = self.maybe_header(header_name);
match maybe_found_header_value {
None => {
panic!(
"Expected header '{debug_header_name}' to be present in response, header was not found, for request {debug_request_format}"
)
}
Some(found_header_value) => {
assert_eq!(expected_header_value, found_header_value,)
}
}
self
}
#[must_use]
pub fn maybe_cookie(&self, cookie_name: &str) -> Option<Cookie<'static>> {
for cookie in self.iter_cookies() {
if cookie.name() == cookie_name {
return Some(cookie.into_owned());
}
}
None
}
#[must_use]
#[track_caller]
pub fn cookie(&self, cookie_name: &str) -> Cookie<'static> {
self.maybe_cookie(cookie_name)
.error_response_fn(|| format!("Cannot find cookie {cookie_name}"), self)
}
#[track_caller]
pub fn assert_contains_cookie(&self, cookie_name: &str) -> &Self {
let debug_request_format = self.debug_request_format();
let has_cookie = self.maybe_cookie(cookie_name).is_some();
assert!(
has_cookie,
"Assertion failed: cookie '{cookie_name}' not found in response, for request {debug_request_format}"
);
self
}
#[must_use]
pub fn cookies(&self) -> CookieJar {
let mut cookies = CookieJar::new();
for cookie in self.iter_cookies() {
cookies.add(cookie.into_owned());
}
cookies
}
#[track_caller]
pub fn iter_cookies(&self) -> impl Iterator<Item = Cookie<'_>> {
self.iter_headers_by_name(SET_COOKIE).map(|header| {
let header_str =
header.to_str().error_message_fn(|| {
let debug_request_format = self.debug_request_format();
format!(
"Reading header 'Set-Cookie' as string, for request {debug_request_format}",
)
});
Cookie::parse(header_str).error_message_fn(|| {
let debug_request_format = self.debug_request_format();
format!("Parsing 'Set-Cookie' header, for request {debug_request_format}",)
})
})
}
#[cfg(feature = "ws")]
#[must_use]
pub async fn into_websocket(self) -> TestWebSocket {
use crate::transport_layer::TransportLayerType;
if self.websockets.transport_type != TransportLayerType::Http {
panic!(
"WebSocket requires a HTTP based transport layer, see `TestServerConfig::transport`"
);
}
let debug_request_format = self.debug_request_format().to_string();
let on_upgrade = self.websockets.maybe_on_upgrade
.error_message_fn(|| {
format!("Expected WebSocket upgrade to be found, it is None, for request {debug_request_format}")
});
let upgraded = on_upgrade.await.error_message_fn(|| {
format!("Failed to upgrade connection for, for request {debug_request_format}")
});
TestWebSocket::new(upgraded).await
}
#[track_caller]
pub fn assert_text<C>(&self, expected: C) -> &Self
where
C: AsRef<str>,
{
let expected_contents = expected.as_ref();
assert_eq!(expected_contents, &self.text());
self
}
#[track_caller]
pub fn assert_text_contains<C>(&self, expected: C) -> &Self
where
C: AsRef<str>,
{
let expected_contents = expected.as_ref();
let received = self.text();
let is_contained = received.contains(expected_contents);
assert!(
is_contained,
"Failed to find '{expected_contents}', received '{received}'"
);
self
}
#[track_caller]
pub fn assert_text_from_file<P>(&self, path: P) -> &Self
where
P: AsRef<Path>,
{
let path_ref = path.as_ref();
let expected = read_to_string(path_ref)
.error_message_fn(|| format!("Failed to read from file '{}'", path_ref.display()));
self.assert_text(expected);
self
}
#[track_caller]
pub fn assert_json<T>(&self, expected: &T) -> &Self
where
T: Serialize + DeserializeOwned + PartialEq<T> + Debug,
{
let received = self.json::<T>();
if *expected != received {
if let Err(error) = expect_json_eq(&received, &expected) {
panic!(
"
{error}
",
);
}
}
self
}
#[track_caller]
pub fn assert_json_contains<T>(&self, expected: &T) -> &Self
where
T: Serialize,
{
let received = self.json::<Value>();
let expected_value = serde_json::to_value(expected).unwrap();
let result = expect_json_eq(
&received,
&expect::object().propagated_contains(expected_value),
);
if let Err(error) = result {
panic!(
"
{error}
",
);
}
self
}
#[track_caller]
pub fn assert_json_from_file<P>(&self, path: P) -> &Self
where
P: AsRef<Path>,
{
let path_ref = path.as_ref();
let file = File::open(path_ref)
.error_message_fn(|| format!("Failed to read from file '{}'", path_ref.display()));
let reader = BufReader::new(file);
let expected =
serde_json::from_reader::<_, serde_json::Value>(reader).error_message_fn(|| {
format!(
"Failed to deserialize file '{}' as json",
path_ref.display()
)
});
self.assert_json(&expected);
self
}
#[cfg(feature = "yaml")]
#[track_caller]
pub fn assert_yaml<T>(&self, other: &T) -> &Self
where
T: DeserializeOwned + PartialEq<T> + Debug,
{
assert_eq!(*other, self.yaml::<T>());
self
}
#[cfg(feature = "yaml")]
#[track_caller]
pub fn assert_yaml_from_file<P>(&self, path: P) -> &Self
where
P: AsRef<Path>,
{
let path_ref = path.as_ref();
let file = File::open(path_ref)
.error_message_fn(|| format!("Failed to read from file '{}'", path_ref.display()));
let reader = BufReader::new(file);
let expected =
serde_yaml::from_reader::<_, serde_yaml::Value>(reader).error_message_fn(|| {
format!(
"Failed to deserialize file '{}' as yaml",
path_ref.display()
)
});
self.assert_yaml(&expected);
self
}
#[cfg(feature = "msgpack")]
#[track_caller]
pub fn assert_msgpack<T>(&self, other: &T) -> &Self
where
T: DeserializeOwned + PartialEq<T> + Debug,
{
assert_eq!(*other, self.msgpack::<T>());
self
}
#[track_caller]
pub fn assert_form<T>(&self, other: &T) -> &Self
where
T: DeserializeOwned + PartialEq<T> + Debug,
{
assert_eq!(*other, self.form::<T>());
self
}
#[track_caller]
pub fn assert_status(&self, expected_status_code: StatusCode) -> &Self {
let received_debug = StatusCodeFormatter(self.status_code);
let expected_debug = StatusCodeFormatter(expected_status_code);
let debug_request_format = self.debug_request_format();
let debug_body = DebugResponseBody(self);
assert_eq!(
expected_status_code, self.status_code,
"Expected status code to be {expected_debug}, received {received_debug}, for request {debug_request_format}, with body {debug_body}"
);
self
}
#[track_caller]
pub fn assert_not_status(&self, expected_status_code: StatusCode) -> &Self {
let received_debug = StatusCodeFormatter(self.status_code);
let expected_debug = StatusCodeFormatter(expected_status_code);
let debug_request_format = self.debug_request_format();
let debug_body = DebugResponseBody(self);
assert_ne!(
expected_status_code, self.status_code,
"Expected status code to not be {expected_debug}, received {received_debug}, for request {debug_request_format}, with body {debug_body}"
);
self
}
#[track_caller]
pub fn assert_status_success(&self) -> &Self {
let status_code = self.status_code.as_u16();
let received_debug = StatusCodeFormatter(self.status_code);
let debug_request_format = self.debug_request_format();
let debug_body = DebugResponseBody(self);
assert!(
200 <= status_code && status_code <= 299,
"Expect status code within 2xx range, received {received_debug}, for request {debug_request_format}, with body {debug_body}"
);
self
}
#[track_caller]
pub fn assert_status_failure(&self) -> &Self {
let status_code = self.status_code.as_u16();
let received_debug = StatusCodeFormatter(self.status_code);
let debug_request_format = self.debug_request_format();
let debug_body = DebugResponseBody(self);
assert!(
status_code < 200 || 299 < status_code,
"Expect status code outside 2xx range, received {received_debug}, for request {debug_request_format}, with body {debug_body}"
);
self
}
#[track_caller]
pub fn assert_status_in_range<R, S>(&self, expected_status_range: R) -> &Self
where
R: RangeBounds<S> + TryIntoRangeBounds<StatusCode> + Debug,
S: TryInto<StatusCode>,
{
let range = TryIntoRangeBounds::<StatusCode>::try_into_range_bounds(expected_status_range)
.expect("Failed to convert status code");
let status_code = self.status_code();
let is_in_range = range.contains(&status_code);
let debug_request_format = self.debug_request_format();
let debug_body = DebugResponseBody(self);
assert!(
is_in_range,
"Expected status to be in range {}, received {status_code}, for request {debug_request_format}, with body {debug_body}",
StatusCodeRangeFormatter(range)
);
self
}
#[track_caller]
pub fn assert_status_not_in_range<R, S>(&self, expected_status_range: R) -> &Self
where
R: RangeBounds<S> + TryIntoRangeBounds<StatusCode> + Debug,
S: TryInto<StatusCode>,
{
let range = TryIntoRangeBounds::<StatusCode>::try_into_range_bounds(expected_status_range)
.expect("Failed to convert status code");
let status_code = self.status_code();
let is_not_in_range = !range.contains(&status_code);
let debug_request_format = self.debug_request_format();
let debug_body = DebugResponseBody(self);
assert!(
is_not_in_range,
"Expected status is not in range {}, received {status_code}, for request {debug_request_format}, with body {debug_body}",
StatusCodeRangeFormatter(range)
);
self
}
#[track_caller]
pub fn assert_status_ok(&self) -> &Self {
self.assert_status(StatusCode::OK)
}
#[track_caller]
pub fn assert_status_not_ok(&self) -> &Self {
self.assert_not_status(StatusCode::OK)
}
#[track_caller]
pub fn assert_status_no_content(&self) -> &Self {
self.assert_status(StatusCode::NO_CONTENT)
}
#[track_caller]
pub fn assert_status_see_other(&self) -> &Self {
self.assert_status(StatusCode::SEE_OTHER)
}
#[track_caller]
pub fn assert_status_bad_request(&self) -> &Self {
self.assert_status(StatusCode::BAD_REQUEST)
}
#[track_caller]
pub fn assert_status_not_found(&self) -> &Self {
self.assert_status(StatusCode::NOT_FOUND)
}
#[track_caller]
pub fn assert_status_unauthorized(&self) -> &Self {
self.assert_status(StatusCode::UNAUTHORIZED)
}
#[track_caller]
pub fn assert_status_forbidden(&self) -> &Self {
self.assert_status(StatusCode::FORBIDDEN)
}
pub fn assert_status_conflict(&self) -> &Self {
self.assert_status(StatusCode::CONFLICT)
}
#[track_caller]
pub fn assert_status_payload_too_large(&self) -> &Self {
self.assert_status(StatusCode::PAYLOAD_TOO_LARGE)
}
#[track_caller]
pub fn assert_status_unprocessable_entity(&self) -> &Self {
self.assert_status(StatusCode::UNPROCESSABLE_ENTITY)
}
#[track_caller]
pub fn assert_status_too_many_requests(&self) -> &Self {
self.assert_status(StatusCode::TOO_MANY_REQUESTS)
}
#[track_caller]
pub fn assert_status_switching_protocols(&self) -> &Self {
self.assert_status(StatusCode::SWITCHING_PROTOCOLS)
}
#[track_caller]
pub fn assert_status_internal_server_error(&self) -> &Self {
self.assert_status(StatusCode::INTERNAL_SERVER_ERROR)
}
#[track_caller]
pub fn assert_status_service_unavailable(&self) -> &Self {
self.assert_status(StatusCode::SERVICE_UNAVAILABLE)
}
pub(crate) fn debug_request_format(&self) -> RequestPathFormatter<'_, Url> {
RequestPathFormatter::new(&self.method, &self.full_request_url, None)
}
}
impl From<TestResponse> for Bytes {
fn from(response: TestResponse) -> Self {
response.into_bytes()
}
}
impl Display for TestResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let version_str = version_str(self.version);
let status = self.status_code();
let status_int = status.as_u16();
let status_reason = status.canonical_reason().unwrap_or("");
writeln!(f, "{version_str} {status_int} {status_reason}",)?;
for (name, value) in self.headers() {
writeln!(f, "{}: {}", name, value.to_str().unwrap_or("<binary>"))?;
}
writeln!(f)?;
let body_raw = String::from_utf8_lossy(&self.response_body);
writeln!(f, "{body_raw}")?;
Ok(())
}
}
fn version_str(version: Version) -> &'static str {
match version {
Version::HTTP_09 => "HTTP/0.9",
Version::HTTP_10 => "HTTP/1.0",
Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2",
Version::HTTP_3 => "HTTP/3",
_ => "HTTP/?",
}
}
#[cfg(test)]
mod test_assert_header {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::http::HeaderMap;
use axum::routing::get;
async fn route_get_header() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("x-my-custom-header", "content".parse().unwrap());
headers
}
#[tokio::test]
async fn it_should_not_panic_if_contains_header_and_content_matches() {
let router = Router::new().route(&"/header", get(route_get_header));
let server = TestServer::new(router);
server
.get(&"/header")
.await
.assert_header("x-my-custom-header", "content");
}
#[tokio::test]
async fn it_should_panic_if_contains_header_and_content_does_not_match() {
let router = Router::new().route(&"/header", get(route_get_header));
let server = TestServer::new(router);
let response = server.get(&"/header").await;
let message = catch_panic_error_message(|| {
response.assert_header("x-my-custom-header", "different-content");
});
assert_error_message(
r#"assertion failed: `(left == right)`
Diff < left / right > :
<"different-content"
>"content"
"#,
message,
);
}
#[tokio::test]
async fn it_should_panic_if_not_contains_header() {
let router = Router::new().route(&"/header", get(route_get_header));
let server = TestServer::new(router);
let response = server.get(&"/header").await;
let message = catch_panic_error_message(|| {
response.assert_header("x-custom-header-not-found", "content");
});
assert_error_message(
"Expected header 'x-custom-header-not-found' to be present in response, header was not found, for request GET http://localhost/header",
message,
);
}
}
#[cfg(test)]
mod test_assert_contains_header {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::http::HeaderMap;
use axum::routing::get;
async fn route_get_header() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("x-my-custom-header", "content".parse().unwrap());
headers
}
#[tokio::test]
async fn it_should_not_panic_if_contains_header() {
let router = Router::new().route(&"/header", get(route_get_header));
let server = TestServer::new(router);
server
.get(&"/header")
.await
.assert_contains_header("x-my-custom-header");
}
#[tokio::test]
async fn it_should_panic_if_not_contains_header() {
let router = Router::new().route(&"/header", get(route_get_header));
let server = TestServer::new(router);
let response = server.get(&"/header").await;
let message = catch_panic_error_message(|| {
response.assert_contains_header("x-custom-header-not-found");
});
assert_error_message(
"Expected header 'x-custom-header-not-found' to be present in response, header was not found, for request GET http://localhost/header",
message,
);
}
}
#[cfg(test)]
mod test_assert_success {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
use http::StatusCode;
pub async fn route_get_pass() -> StatusCode {
StatusCode::OK
}
pub async fn route_get_fail() -> StatusCode {
StatusCode::SERVICE_UNAVAILABLE
}
#[tokio::test]
async fn it_should_pass_when_200() {
let router = Router::new()
.route(&"/pass", get(route_get_pass))
.route(&"/fail", get(route_get_fail));
let server = TestServer::new(router);
let response = server.get(&"/pass").await;
response.assert_status_success();
}
#[tokio::test]
async fn it_should_panic_when_not_200() {
let router = Router::new()
.route(&"/pass", get(route_get_pass))
.route(&"/fail", get(route_get_fail));
let server = TestServer::new(router);
let response = server.get(&"/fail").expect_failure().await;
let message = catch_panic_error_message(|| {
response.assert_status_success();
});
assert_error_message(
"Expect status code within 2xx range, received 503 (Service Unavailable), for request GET http://localhost/fail, with body ''",
message,
);
}
}
#[cfg(test)]
mod test_assert_failure {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
use http::StatusCode;
pub async fn route_get_pass() -> StatusCode {
StatusCode::OK
}
pub async fn route_get_fail() -> StatusCode {
StatusCode::SERVICE_UNAVAILABLE
}
#[tokio::test]
async fn it_should_pass_when_not_200() {
let router = Router::new()
.route(&"/pass", get(route_get_pass))
.route(&"/fail", get(route_get_fail));
let server = TestServer::new(router);
let response = server.get(&"/fail").expect_failure().await;
response.assert_status_failure();
}
#[tokio::test]
async fn it_should_panic_when_200() {
let router = Router::new()
.route(&"/pass", get(route_get_pass))
.route(&"/fail", get(route_get_fail));
let server = TestServer::new(router);
let response = server.get(&"/pass").await;
let message = catch_panic_error_message(|| {
response.assert_status_failure();
});
assert_error_message(
"Expect status code outside 2xx range, received 200 (OK), for request GET http://localhost/pass, with body ''",
message,
);
}
}
#[cfg(test)]
mod test_assert_status {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
use http::StatusCode;
pub async fn route_get_ok() -> StatusCode {
StatusCode::OK
}
#[tokio::test]
async fn it_should_pass_if_given_right_status_code() {
let router = Router::new().route(&"/ok", get(route_get_ok));
let server = TestServer::new(router);
server.get(&"/ok").await.assert_status(StatusCode::OK);
}
#[tokio::test]
async fn it_should_panic_when_status_code_does_not_match() {
let router = Router::new().route(&"/ok", get(route_get_ok));
let server = TestServer::new(router);
let response = server.get(&"/ok").await;
let message = catch_panic_error_message(|| {
response.assert_status(StatusCode::ACCEPTED);
});
assert_error_message("assertion failed: `(left == right)`: Expected status code to be 202 (Accepted), received 200 (OK), for request GET http://localhost/ok, with body ''
Diff < left / right > :
<202
>200
", message);
}
}
#[cfg(test)]
mod test_assert_not_status {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
use http::StatusCode;
pub async fn route_get_ok() -> StatusCode {
StatusCode::OK
}
#[tokio::test]
async fn it_should_pass_if_status_code_does_not_match() {
let router = Router::new().route(&"/ok", get(route_get_ok));
let server = TestServer::new(router);
server
.get(&"/ok")
.await
.assert_not_status(StatusCode::ACCEPTED);
}
#[tokio::test]
async fn it_should_panic_if_status_code_matches() {
let router = Router::new().route(&"/ok", get(route_get_ok));
let server = TestServer::new(router);
let response = server.get(&"/ok").await;
let message = catch_panic_error_message(|| {
response.assert_not_status(StatusCode::OK);
});
assert_error_message("assertion failed: `(left != right)`: Expected status code to not be 200 (OK), received 200 (OK), for request GET http://localhost/ok, with body ''
Both sides:
200
", message);
}
}
#[cfg(test)]
mod test_assert_status_in_range {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::routing::Router;
use axum::routing::get;
use http::StatusCode;
use std::ops::RangeFull;
#[tokio::test]
async fn it_should_be_true_when_within_int_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range(200..299);
}
#[tokio::test]
async fn it_should_be_true_when_within_status_code_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range(StatusCode::OK..StatusCode::IM_USED);
}
#[tokio::test]
async fn it_should_be_false_when_outside_int_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_in_range(200..299);
});
assert_error_message(
"Expected status to be in range 200..299, received 500 Internal Server Error, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_false_when_outside_status_code_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_in_range(StatusCode::OK..StatusCode::IM_USED);
});
assert_error_message(
"Expected status to be in range 200..226, received 500 Internal Server Error, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_within_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range(200..=299);
}
#[tokio::test]
async fn it_should_be_false_when_outside_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_in_range(200..=299);
});
assert_error_message(
"Expected status to be in range 200..=299, received 500 Internal Server Error, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_within_to_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range(..299);
}
#[tokio::test]
async fn it_should_be_false_when_outside_to_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_in_range(..299);
});
assert_error_message(
"Expected status to be in range ..299, received 500 Internal Server Error, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_within_to_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range(..=299);
}
#[tokio::test]
async fn it_should_be_false_when_outside_to_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_in_range(..=299);
});
assert_error_message(
"Expected status to be in range ..=299, received 500 Internal Server Error, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_within_from_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range(200..);
}
#[tokio::test]
async fn it_should_be_false_when_outside_from_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_in_range(500..);
});
assert_error_message(
"Expected status to be in range 500.., received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_for_rull_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_in_range::<RangeFull, StatusCode>(..);
}
}
#[cfg(test)]
mod test_assert_status_not_in_range {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::routing::Router;
use axum::routing::get;
use http::StatusCode;
use std::ops::RangeFull;
#[tokio::test]
async fn it_should_be_false_when_within_int_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range(200..299);
});
assert_error_message(
"Expected status is not in range 200..299, received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_false_when_within_status_code_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range(StatusCode::OK..StatusCode::IM_USED);
});
assert_error_message(
"Expected status is not in range 200..226, received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_outside_int_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_not_in_range(200..299);
}
#[tokio::test]
async fn it_should_be_true_when_outside_status_code_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_not_in_range(StatusCode::OK..StatusCode::IM_USED);
}
#[tokio::test]
async fn it_should_be_false_when_within_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range(200..=299);
});
assert_error_message(
"Expected status is not in range 200..=299, received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_outside_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_not_in_range(200..=299);
}
#[tokio::test]
async fn it_should_be_false_when_within_to_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range(..299);
});
assert_error_message(
"Expected status is not in range ..299, received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_outside_to_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_not_in_range(..299);
}
#[tokio::test]
async fn it_should_be_false_when_within_to_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range(..=299);
});
assert_error_message(
"Expected status is not in range ..=299, received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_outside_to_inclusive_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::INTERNAL_SERVER_ERROR }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_not_in_range(..=299);
}
#[tokio::test]
async fn it_should_be_false_when_within_from_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range(200..);
});
assert_error_message(
"Expected status is not in range 200.., received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
#[tokio::test]
async fn it_should_be_true_when_outside_from_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
TestServer::new(app)
.get(&"/status")
.await
.assert_status_not_in_range(500..);
}
#[tokio::test]
async fn it_should_be_false_for_rull_range() {
let app = Router::new().route(
&"/status",
get(|| async { StatusCode::NON_AUTHORITATIVE_INFORMATION }),
);
let response = TestServer::new(app).get(&"/status").await;
let message = catch_panic_error_message(|| {
response.assert_status_not_in_range::<RangeFull, StatusCode>(..);
});
assert_error_message(
"Expected status is not in range .., received 203 Non Authoritative Information, for request GET http://localhost/status, with body ''",
message,
);
}
}
#[cfg(test)]
mod test_into_bytes {
use crate::TestServer;
use axum::Json;
use axum::Router;
use axum::routing::get;
use serde_json::Value;
use serde_json::json;
async fn route_get_json() -> Json<Value> {
Json(json!({
"message": "it works?"
}))
}
#[tokio::test]
async fn it_should_deserialize_into_json() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let bytes = server.get(&"/json").await.into_bytes();
let text = String::from_utf8_lossy(&bytes);
assert_eq!(text, r#"{"message":"it works?"}"#);
}
}
#[cfg(test)]
mod test_content_type {
use crate::TestServer;
use axum::Json;
use axum::Router;
use axum::routing::get;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
#[tokio::test]
async fn it_should_retrieve_json_content_type_for_json() {
let app = Router::new().route(
&"/json",
get(|| async {
Json(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}),
);
let server = TestServer::new(app);
let content_type = server.get(&"/json").await.content_type();
assert_eq!(content_type, "application/json");
}
#[cfg(feature = "yaml")]
#[tokio::test]
async fn it_should_retrieve_yaml_content_type_for_yaml() {
use axum_yaml::Yaml;
let app = Router::new().route(
&"/yaml",
get(|| async {
Yaml(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}),
);
let server = TestServer::new(app);
let content_type = server.get(&"/yaml").await.content_type();
assert_eq!(content_type, "application/yaml");
}
}
#[cfg(test)]
mod test_json {
use crate::TestServer;
use crate::testing::catch_panic_error_message;
use axum::Json;
use axum::Router;
use axum::routing::get;
use pretty_assertions::assert_eq;
use pretty_assertions::assert_str_eq;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_json() -> Json<ExampleResponse> {
Json(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
async fn route_get_fox() -> &'static str {
"🦊"
}
#[tokio::test]
async fn it_should_deserialize_into_json() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let response = server.get(&"/json").await.json::<ExampleResponse>();
assert_eq!(
response,
ExampleResponse {
name: "Joe".to_string(),
age: 20,
}
);
}
#[tokio::test]
async fn it_should_display_the_body_when_deserializing_non_json() {
let app = Router::new().route(&"/fox", get(route_get_fox));
let server = TestServer::new(app);
let response = server.get(&"/fox").await;
let message = catch_panic_error_message(|| {
let _ = response.json::<Value>();
});
assert_str_eq!(
r#"Failed to deserialize Json response,
for request GET http://localhost/fox
expected value at line 1 column 1
received:
🦊
"#,
message
);
}
}
#[cfg(feature = "yaml")]
#[cfg(test)]
mod test_yaml {
use crate::TestServer;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
use axum_yaml::Yaml;
use pretty_assertions::assert_eq;
use pretty_assertions::assert_str_eq;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_yaml() -> Yaml<ExampleResponse> {
Yaml(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
async fn route_get_fox() -> &'static str {
"🦊"
}
#[tokio::test]
async fn it_should_deserialize_into_yaml() {
let app = Router::new().route(&"/yaml", get(route_get_yaml));
let server = TestServer::new(app);
let response = server.get(&"/yaml").await.yaml::<ExampleResponse>();
assert_eq!(
response,
ExampleResponse {
name: "Joe".to_string(),
age: 20,
}
);
}
#[tokio::test]
async fn it_should_display_the_body_when_deserializing_non_yaml() {
let app = Router::new().route(&"/fox", get(route_get_fox));
let server = TestServer::new(app);
let response = server.get(&"/fox").await;
let error_message = catch_panic_error_message(|| {
let _ = response.yaml::<ExampleResponse>();
});
assert_str_eq!(
r#"Failed to deserialize Yaml response,
for request GET http://localhost/fox
invalid type: string "🦊", expected struct ExampleResponse
received:
🦊
"#,
error_message
);
}
}
#[cfg(feature = "msgpack")]
#[cfg(test)]
mod test_msgpack {
use crate::TestServer;
use axum::Router;
use axum::routing::get;
use axum_msgpack::MsgPack;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_msgpack() -> MsgPack<ExampleResponse> {
MsgPack(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_deserialize_into_msgpack() {
let app = Router::new().route(&"/msgpack", get(route_get_msgpack));
let server = TestServer::new(app);
let response = server.get(&"/msgpack").await.msgpack::<ExampleResponse>();
assert_eq!(
response,
ExampleResponse {
name: "Joe".to_string(),
age: 20,
}
);
}
}
#[cfg(test)]
mod test_form {
use crate::TestServer;
use axum::Form;
use axum::Router;
use axum::routing::get;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_form() -> Form<ExampleResponse> {
Form(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_deserialize_into_form() {
let app = Router::new().route(&"/form", get(route_get_form));
let server = TestServer::new(app);
let response = server.get(&"/form").await.form::<ExampleResponse>();
assert_eq!(
response,
ExampleResponse {
name: "Joe".to_string(),
age: 20,
}
);
}
}
#[cfg(test)]
mod test_from {
use crate::TestServer;
use axum::Router;
use axum::routing::get;
use bytes::Bytes;
#[tokio::test]
async fn it_should_turn_into_response_bytes() {
let app = Router::new().route(&"/text", get(|| async { "This is some example text" }));
let server = TestServer::new(app);
let response = server.get(&"/text").await;
let bytes: Bytes = response.into();
let text = String::from_utf8_lossy(&bytes);
assert_eq!(text, "This is some example text");
}
}
#[cfg(test)]
mod test_assert_text {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
fn new_test_server() -> TestServer {
async fn route_get_text() -> &'static str {
"This is some example text"
}
let app = Router::new().route(&"/text", get(route_get_text));
TestServer::new(app)
}
#[tokio::test]
async fn it_should_match_whole_text() {
let server = new_test_server();
server
.get(&"/text")
.await
.assert_text("This is some example text");
}
#[tokio::test]
async fn it_should_allow_chaining_direct_off_server() {
let server = new_test_server();
server
.get(&"/text")
.await
.assert_status_ok()
.assert_text("This is some example text");
}
#[tokio::test]
async fn it_should_not_match_partial_text() {
let server = new_test_server();
let response = server.get(&"/text").await;
let message = catch_panic_error_message(|| {
response.assert_text("some example");
});
assert_error_message(
"assertion failed: `(left == right)`
Diff < left / right > :
<some example
>This is some example text
",
message,
);
}
#[tokio::test]
async fn it_should_not_match_different_text() {
let server = new_test_server();
let response = server.get(&"/text").await;
let message = catch_panic_error_message(|| {
response.assert_text("🦊");
});
assert_error_message(
"assertion failed: `(left == right)`
Diff < left / right > :
<🦊
>This is some example text
",
message,
);
}
}
#[cfg(test)]
mod test_assert_text_contains {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::routing::get;
fn new_test_server() -> TestServer {
async fn route_get_text() -> &'static str {
"This is some example text"
}
let app = Router::new().route(&"/text", get(route_get_text));
TestServer::new(app)
}
#[tokio::test]
async fn it_should_match_whole_text() {
let server = new_test_server();
server
.get(&"/text")
.await
.assert_text_contains("This is some example text");
}
#[tokio::test]
async fn it_should_match_partial_text() {
let server = new_test_server();
server
.get(&"/text")
.await
.assert_text_contains("some example");
}
#[tokio::test]
async fn it_should_not_match_different_text() {
let server = new_test_server();
let response = server.get(&"/text").await;
let message = catch_panic_error_message(|| {
response.assert_text_contains("🦊");
});
assert_error_message(
"Failed to find '🦊', received 'This is some example text'",
message,
);
}
}
#[cfg(test)]
mod test_assert_text_from_file {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::routing::Router;
use axum::routing::get;
#[tokio::test]
async fn it_should_match_from_file() {
let app = Router::new().route(&"/text", get(|| async { "hello!" }));
let server = TestServer::new(app);
server
.get(&"/text")
.await
.assert_text_from_file("files/example.txt");
}
#[tokio::test]
async fn it_should_panic_when_not_match_the_file() {
let app = Router::new().route(&"/text", get(|| async { "🦊" }));
let server = TestServer::new(app);
let response = server.get(&"/text").await;
let message = catch_panic_error_message(|| {
response.assert_text_from_file("files/example.txt");
});
assert_error_message(
"assertion failed: `(left == right)`
Diff < left / right > :
<hello!
>🦊
",
message,
);
}
}
#[cfg(test)]
mod test_assert_json {
use super::*;
use crate::TestServer;
use crate::testing::ExpectStrMinLen;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Form;
use axum::Json;
use axum::Router;
use axum::routing::get;
use serde::Deserialize;
use serde_json::json;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_form() -> Form<ExampleResponse> {
Form(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
async fn route_get_json() -> Json<ExampleResponse> {
Json(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_match_json_returned() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
server.get(&"/json").await.assert_json(&ExampleResponse {
name: "Joe".to_string(),
age: 20,
});
}
#[tokio::test]
async fn it_should_match_json_returned_using_json_value() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
server.get(&"/json").await.assert_json(&json!({
"name": "Joe",
"age": 20,
}));
}
#[tokio::test]
async fn it_should_panic_if_response_is_different() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let response = server.get(&"/json").await;
let message = catch_panic_error_message(|| {
response.assert_json(&ExampleResponse {
name: "Julia".to_string(),
age: 25,
});
});
assert_error_message(
"
Json integers at root.age are not equal:
expected 25
received 20
",
message,
);
}
#[tokio::test]
async fn it_should_panic_if_response_is_form() {
let app = Router::new().route(&"/form", get(route_get_form));
let server = TestServer::new(app);
let response = server.get(&"/form").await;
let message = catch_panic_error_message(|| {
response.assert_json(&ExampleResponse {
name: "Joe".to_string(),
age: 20,
});
});
assert_error_message(
"Failed to deserialize Json response,
for request GET http://localhost/form
expected ident at line 1 column 2
received:
name=Joe&age=20
",
message,
);
}
#[tokio::test]
async fn it_should_work_with_custom_expect_op() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
server.get(&"/json").await.assert_json(&json!({
"name": ExpectStrMinLen { min: 3 },
"age": 20,
}));
}
#[tokio::test]
async fn it_should_panic_if_custom_expect_op_fails() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let response = server.get(&"/json").await;
let message = catch_panic_error_message(|| {
response.assert_json(&json!({
"name": ExpectStrMinLen { min: 10 },
"age": 20,
}));
});
assert_error_message("String is too short, received: Joe", message);
}
}
#[cfg(test)]
mod test_assert_json_contains {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Form;
use axum::Json;
use axum::Router;
use axum::routing::get;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use std::time::Instant;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
time: u64,
name: String,
age: u32,
}
async fn route_get_form() -> Form<ExampleResponse> {
Form(ExampleResponse {
time: Instant::now().elapsed().as_millis() as u64,
name: "Joe".to_string(),
age: 20,
})
}
async fn route_get_json() -> Json<ExampleResponse> {
Json(ExampleResponse {
time: Instant::now().elapsed().as_millis() as u64,
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_match_subset_of_json_returned() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
server.get(&"/json").await.assert_json_contains(&json!({
"name": "Joe",
"age": 20,
}));
}
#[tokio::test]
async fn it_should_panic_if_response_is_different() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let response = server.get(&"/json").await;
let message = catch_panic_error_message(|| {
response.assert_json_contains(&ExampleResponse {
time: 1234,
name: "Julia".to_string(),
age: 25,
});
});
assert_error_message(
"
Json integers at root.age are not equal:
expected 25
received 20
",
message,
);
}
#[tokio::test]
async fn it_should_panic_if_response_is_form() {
let app = Router::new().route(&"/form", get(route_get_form));
let server = TestServer::new(app);
let response = server.get(&"/form").await;
let message = catch_panic_error_message(|| {
response.assert_json_contains(&json!({
"name": "Joe",
"age": 20,
}));
});
assert_error_message(
"Failed to deserialize Json response,
for request GET http://localhost/form
expected ident at line 1 column 2
received:
time=0&name=Joe&age=20
",
message,
);
}
#[tokio::test]
async fn it_should_propagate_contains_to_sub_objects() {
let json_result = json!({ "a": {"prop1": "value1"} }).to_string();
let app = Router::new().route(&"/json", get(|| async { json_result }));
let server = TestServer::new(app);
let response = server.get("/json").await;
response.assert_json_contains(&json!({ "a": {} }));
}
}
#[cfg(test)]
mod test_assert_json_from_file {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Form;
use axum::Json;
use axum::routing::Router;
use axum::routing::get;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
#[tokio::test]
async fn it_should_match_json_from_file() {
let app = Router::new().route(
&"/json",
get(|| async {
Json(json!(
{
"name": "Joe",
"age": 20,
}
))
}),
);
let server = TestServer::new(app);
server
.get(&"/json")
.await
.assert_json_from_file("files/example.json");
}
#[tokio::test]
async fn it_should_panic_when_not_match_the_file() {
let app = Router::new().route(
&"/json",
get(|| async {
Json(json!(
{
"name": "Julia",
"age": 25,
}
))
}),
);
let server = TestServer::new(app);
let response = server.get(&"/json").await;
let message = catch_panic_error_message(|| {
response.assert_json_from_file("files/example.json");
});
assert_error_message(
"
Json integers at root.age are not equal:
expected 20
received 25
",
message,
);
}
#[tokio::test]
async fn it_should_panic_when_content_type_does_not_match() {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
let app = Router::new().route(
&"/form",
get(|| async {
Form(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}),
);
let server = TestServer::new(app);
let response = server.get(&"/form").await;
let message = catch_panic_error_message(|| {
response.assert_json_from_file("files/example.json");
});
assert_error_message(
"Failed to deserialize Json response,
for request GET http://localhost/form
expected ident at line 1 column 2
received:
name=Joe&age=20
",
message,
);
}
}
#[cfg(feature = "yaml")]
#[cfg(test)]
mod test_assert_yaml {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Form;
use axum::Router;
use axum::routing::get;
use axum_yaml::Yaml;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_form() -> Form<ExampleResponse> {
Form(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
async fn route_get_yaml() -> Yaml<ExampleResponse> {
Yaml(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_match_yaml_returned() {
let app = Router::new().route(&"/yaml", get(route_get_yaml));
let server = TestServer::new(app);
server.get(&"/yaml").await.assert_yaml(&ExampleResponse {
name: "Joe".to_string(),
age: 20,
});
}
#[tokio::test]
async fn it_should_panic_if_response_is_different() {
let app = Router::new().route(&"/yaml", get(route_get_yaml));
let server = TestServer::new(app);
let response = server.get(&"/yaml").await;
let message = catch_panic_error_message(|| {
response.assert_yaml(&ExampleResponse {
name: "Julia".to_string(),
age: 25,
});
});
assert_error_message(
r#"assertion failed: `(left == right)`
Diff < left / right > :
ExampleResponse {
< name: "Julia",
< age: 25,
> name: "Joe",
> age: 20,
}
"#,
message,
);
}
#[tokio::test]
async fn it_should_panic_if_response_is_form() {
let app = Router::new().route(&"/form", get(route_get_form));
let server = TestServer::new(app);
let response = server.get(&"/form").await;
let message = catch_panic_error_message(|| {
response.assert_yaml(&ExampleResponse {
name: "Joe".to_string(),
age: 20,
});
});
assert_error_message(
r#"Failed to deserialize Yaml response,
for request GET http://localhost/form
invalid type: string "name=Joe&age=20", expected struct ExampleResponse
received:
name=Joe&age=20
"#,
message,
);
}
}
#[cfg(feature = "yaml")]
#[cfg(test)]
mod test_assert_yaml_from_file {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Form;
use axum::routing::Router;
use axum::routing::get;
use axum_yaml::Yaml;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
#[tokio::test]
async fn it_should_match_yaml_from_file() {
let app = Router::new().route(
&"/yaml",
get(|| async {
Yaml(json!(
{
"name": "Joe",
"age": 20,
}
))
}),
);
let server = TestServer::new(app);
server
.get(&"/yaml")
.await
.assert_yaml_from_file("files/example.yaml");
}
#[tokio::test]
async fn it_should_panic_when_not_match_the_file() {
let app = Router::new().route(
&"/yaml",
get(|| async {
Yaml(json!(
{
"name": "Julia",
"age": 25,
}
))
}),
);
let server = TestServer::new(app);
let response = server.get(&"/yaml").await;
let message = catch_panic_error_message(|| {
response.assert_yaml_from_file("files/example.yaml");
});
assert_error_message(
r#"assertion failed: `(left == right)`
Diff < left / right > :
Mapping {
< "name": String("Joe"),
< "age": Number(20),
> "age": Number(25),
> "name": String("Julia"),
}
"#,
message,
);
}
#[tokio::test]
async fn it_should_panic_when_content_type_does_not_match() {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
let app = Router::new().route(
&"/form",
get(|| async {
Form(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}),
);
let server = TestServer::new(app);
let response = server.get(&"/form").await;
let message = catch_panic_error_message(|| {
response.assert_yaml_from_file("files/example.yaml");
});
assert_error_message(
r#"assertion failed: `(left == right)`
Diff < left / right > :
<Mapping {
< "name": String("Joe"),
< "age": Number(20),
<}
>String("name=Joe&age=20")
"#,
message,
);
}
}
#[cfg(test)]
mod test_assert_form {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Form;
use axum::Json;
use axum::Router;
use axum::routing::get;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_form() -> Form<ExampleResponse> {
Form(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
async fn route_get_json() -> Json<ExampleResponse> {
Json(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_match_form_returned() {
let app = Router::new().route(&"/form", get(route_get_form));
let server = TestServer::new(app);
server.get(&"/form").await.assert_form(&ExampleResponse {
name: "Joe".to_string(),
age: 20,
});
}
#[tokio::test]
async fn it_should_panic_if_response_is_different() {
let app = Router::new().route(&"/form", get(route_get_form));
let server = TestServer::new(app);
let response = server.get(&"/form").await;
let message = catch_panic_error_message(|| {
response.assert_form(&ExampleResponse {
name: "Julia".to_string(),
age: 25,
});
});
assert_error_message(
r#"assertion failed: `(left == right)`
Diff < left / right > :
ExampleResponse {
< name: "Julia",
< age: 25,
> name: "Joe",
> age: 20,
}
"#,
message,
);
}
#[tokio::test]
async fn it_should_panic_if_response_is_json() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let response = server.get(&"/json").await;
let message = catch_panic_error_message(|| {
response.assert_form(&ExampleResponse {
name: "Joe".to_string(),
age: 20,
});
});
assert_error_message(
r#"Failed to deserialize Form response,
for request GET http://localhost/json
missing field `name`
received:
{"name":"Joe","age":20}
"#,
message,
);
}
}
#[cfg(test)]
mod test_text {
use crate::TestServer;
use axum::Router;
use axum::routing::get;
#[tokio::test]
async fn it_should_deserialize_into_text() {
async fn route_get_text() -> String {
"hello!".to_string()
}
let app = Router::new().route(&"/text", get(route_get_text));
let server = TestServer::new(app);
let response = server.get(&"/text").await.text();
assert_eq!(response, "hello!");
}
}
#[cfg(feature = "ws")]
#[cfg(test)]
mod test_into_websocket {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message_async;
use axum::Router;
use axum::extract::WebSocketUpgrade;
use axum::extract::ws::WebSocket;
use axum::response::Response;
use axum::routing::get;
fn new_test_router() -> Router {
pub async fn route_get_websocket(ws: WebSocketUpgrade) -> Response {
async fn handle_ping_pong(mut socket: WebSocket) {
while let Some(_) = socket.recv().await {
}
}
ws.on_upgrade(move |socket| handle_ping_pong(socket))
}
let app = Router::new().route(&"/ws", get(route_get_websocket));
app
}
#[tokio::test]
async fn it_should_upgrade_on_http_transport() {
let router = new_test_router();
let server = TestServer::builder().http_transport().build(router);
let _ = server.get_websocket(&"/ws").await.into_websocket().await;
assert!(true);
}
#[tokio::test]
async fn it_should_fail_to_upgrade_on_mock_transport() {
let router = new_test_router();
let server = TestServer::builder().mock_transport().build(router);
let response = server.get_websocket(&"/ws").await;
let message = catch_panic_error_message_async(async {
let _ = response.into_websocket().await;
})
.await;
assert_error_message(
"WebSocket requires a HTTP based transport layer, see `TestServerConfig::transport`",
message,
);
}
}
#[cfg(test)]
mod test_fmt {
use crate::TestServer;
use axum::Json;
use axum::Router;
use axum::routing::get;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ExampleResponse {
name: String,
age: u32,
}
async fn route_get_json() -> Json<ExampleResponse> {
Json(ExampleResponse {
name: "Joe".to_string(),
age: 20,
})
}
#[tokio::test]
async fn it_should_output_json_in_json_format() {
let app = Router::new().route(&"/json", get(route_get_json));
let server = TestServer::new(app);
let response = server.get(&"/json").await;
let output = format!("{response}");
assert_eq!(
output,
r#"HTTP/1.1 200 OK
content-type: application/json
content-length: 23
{"name":"Joe","age":20}
"#
);
}
}
#[cfg(test)]
mod test_request_method {
use super::*;
use crate::TestServer;
use axum::Router;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn it_should_return_same_method_as_the_request() {
let server = TestServer::new(Router::new());
let method = server.get("/").await.request_method();
assert_eq!(Method::GET, method);
let method = server.post("/").await.request_method();
assert_eq!(Method::POST, method);
let method = server.put("/").await.request_method();
assert_eq!(Method::PUT, method);
let method = server.patch("/").await.request_method();
assert_eq!(Method::PATCH, method);
let method = server.delete("/").await.request_method();
assert_eq!(Method::DELETE, method);
let method = server.method(Method::OPTIONS, "/").await.request_method();
assert_eq!(Method::OPTIONS, method);
}
}
#[cfg(test)]
mod test_assert_contains_cookie {
use crate::TestServer;
use crate::testing::assert_error_message;
use crate::testing::catch_panic_error_message;
use axum::Router;
use axum::http::HeaderMap;
use axum::routing::get;
use http::header::SET_COOKIE;
async fn route_get_cookie() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, "my-cookie=my-value".parse().unwrap());
headers
}
#[tokio::test]
async fn it_should_not_panic_if_cookie_exists() {
let router = Router::new().route(&"/cookie", get(route_get_cookie));
let server = TestServer::new(router);
server
.get(&"/cookie")
.await
.assert_contains_cookie("my-cookie");
}
#[tokio::test]
async fn it_should_panic_if_cookie_does_not_exist() {
let router = Router::new().route(&"/cookie", get(route_get_cookie));
let server = TestServer::new(router);
let response = server.get(&"/cookie").await;
let message = catch_panic_error_message(|| {
response.assert_contains_cookie("cookie-not-found");
});
assert_error_message(
"Assertion failed: cookie 'cookie-not-found' not found in response, for request GET http://localhost/cookie",
message,
);
}
}