#![cfg(all(feature = "macros", feature = "json"))]
use kynos::{
Router,
extract::{
body::text::Text,
connection::{ConnectInfo, MatchedPath},
params::path::Path,
},
http::{Method, StatusCode, header},
response::status::NoContent,
router::policy::{FallbackPolicy, TrailingSlashPolicy},
};
#[path = "support/mod.rs"]
mod support;
use support::{App, get, send, service};
#[tokio::test]
async fn a_matched_request_reaches_its_operation() {
let reply = get(&service(), "/users/42").call().await;
assert_eq!(reply.status, StatusCode::OK);
assert_eq!(reply.json()["id"], 42);
assert_eq!(reply.json()["name"], "user from pool 7");
}
#[tokio::test]
async fn a_path_no_template_matches_is_not_found() {
let reply = get(&service(), "/widgets").call().await;
assert_eq!(reply.status, StatusCode::NOT_FOUND);
assert_eq!(
reply.field(header::CONTENT_TYPE.as_str()).as_deref(),
Some("application/problem+json")
);
assert_eq!(reply.json()["status"], 404);
}
#[tokio::test]
async fn a_method_no_operation_declares_is_refused_with_what_is_allowed() {
let reply = send(&service(), Method::PATCH, "/users/42").call().await;
assert_eq!(reply.status, StatusCode::METHOD_NOT_ALLOWED);
let allow = reply.field(header::ALLOW.as_str()).expect("an Allow field");
let mut methods: Vec<&str> = allow.split(", ").collect();
methods.sort_unstable();
assert_eq!(methods, ["DELETE", "GET"]);
}
#[tokio::test]
async fn a_trailing_slash_variant_is_redirected_when_the_policy_says_so() {
let service = support::router()
.trailing_slashes(TrailingSlashPolicy::Redirect)
.build(App::new())
.expect("a describable router");
let reply = get(&service, "/users/42/").call().await;
assert_eq!(reply.status, StatusCode::PERMANENT_REDIRECT);
assert_eq!(
reply.field(header::LOCATION.as_str()).as_deref(),
Some("/users/42")
);
}
#[tokio::test]
async fn a_trailing_slash_variant_is_a_plain_miss_by_default() {
let reply = get(&service(), "/users/42/").call().await;
assert_eq!(reply.status, StatusCode::NOT_FOUND);
assert!(reply.field(header::LOCATION.as_str()).is_none());
}
#[tokio::test]
async fn a_redirect_carries_the_query_it_was_given() {
let service = support::router()
.trailing_slashes(TrailingSlashPolicy::Redirect)
.build(App::new())
.expect("a describable router");
let reply = get(&service, "/users/?limit=2").call().await;
assert_eq!(reply.status, StatusCode::PERMANENT_REDIRECT);
assert_eq!(
reply.field(header::LOCATION.as_str()).as_deref(),
Some("/users?limit=2")
);
}
#[tokio::test]
async fn an_empty_fallback_sends_the_status_and_nothing_else() {
let service = support::router()
.not_found(FallbackPolicy::Empty)
.method_not_allowed(FallbackPolicy::Empty)
.build(App::new())
.expect("a describable router");
let missing = get(&service, "/widgets").call().await;
assert_eq!(missing.status, StatusCode::NOT_FOUND);
assert!(missing.body.is_empty(), "{:?}", missing.text());
let refused = send(&service, Method::PATCH, "/users/42").call().await;
assert_eq!(refused.status, StatusCode::METHOD_NOT_ALLOWED);
assert!(refused.body.is_empty(), "{:?}", refused.text());
assert!(refused.field(header::ALLOW.as_str()).is_some());
}
#[kynos::get("/echo/{id}")]
async fn echo_matched_path(
Path(_): Path<support::UserPath>,
MatchedPath(template): MatchedPath,
) -> Text {
Text(template.to_owned())
}
#[tokio::test]
async fn the_matched_path_is_the_template_and_not_the_request_target() {
let service = Router::<()>::new()
.mount(kynos::routes![echo_matched_path])
.build(())
.expect("a describable router");
let first = get(&service, "/echo/1").call().await;
let second = get(&service, "/echo/99999").call().await;
assert_eq!(first.text(), "/echo/{id}");
assert_eq!(
first.text(),
second.text(),
"two concrete paths under one template produced two labels"
);
}
#[kynos::get("/nothing")]
async fn nothing() -> NoContent {
NoContent
}
#[tokio::test]
async fn an_operation_returning_no_content_sends_no_body() {
let service = Router::<()>::new()
.mount(kynos::routes![nothing])
.build(())
.expect("a describable router");
let reply = get(&service, "/nothing").call().await;
assert_eq!(reply.status, StatusCode::NO_CONTENT);
assert!(reply.body.is_empty());
}
#[kynos::get("/who")]
async fn who(peer: ConnectInfo) -> Text {
Text(peer.0.to_string())
}
#[tokio::test]
async fn a_directly_driven_service_reports_an_in_process_connection() {
let service = Router::<()>::new()
.mount(kynos::routes![who])
.build(())
.expect("a describable router");
let reply = get(&service, "/who").call().await;
assert_eq!(reply.status, StatusCode::OK);
assert!(
reply.text().ends_with(":0"),
"an in-process connection reported `{}`",
reply.text()
);
}