Reorganize tests (#456)

* Reorganize tests

This breaks up the large `crate::tests` module by moving some of the
tests into a place that makes more sense. For example tests of JSON
serialization are moved to the `crate::json` module. The remaining routing
tests have been moved to `crate::routing::tests`.

I generally prefer having tests close to the code they're testing. Makes
it easier to see how/if something is tested.

* Try pinning to older version of async-graphql

* Revert "Try pinning to older version of async-graphql"

This reverts commit 2e2cae7d12f5e433a16d6607497d587863f04384.

* don't test examples on 1.54 on CI

* move ci steps around a bit
This commit is contained in:
David Pedersen 2021-11-03 10:22:31 +01:00 committed by GitHub
parent 420918a53a
commit 0f1f28062d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 427 additions and 361 deletions

View file

@ -64,12 +64,11 @@ jobs:
run: cargo hack check --each-feature --no-dev-deps --all
test-versions:
# Test against the stable, beta, and nightly Rust toolchains on ubuntu-latest.
needs: check
runs-on: ubuntu-latest
strategy:
matrix:
rust: [stable, beta, nightly, 1.54]
rust: [stable, beta, nightly]
steps:
- uses: actions/checkout@master
- uses: actions-rs/toolchain@v1
@ -84,6 +83,28 @@ jobs:
command: test
args: --all --all-features --all-targets
# some examples doesn't support 1.54 (such as async-graphql)
# so we only test axum itself on 1.54
test-msrv:
needs: check
runs-on: ubuntu-latest
strategy:
matrix:
rust: [1.54]
steps:
- uses: actions/checkout@master
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
profile: minimal
- uses: Swatinem/rust-cache@v1
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: -p axum --all-features --all-targets
test-docs:
needs: check
runs-on: ubuntu-latest

View file

@ -132,7 +132,7 @@ fn stream_body_traits() {
type EmptyStream = StreamBody<Empty<Result<Bytes, BoxError>>>;
crate::tests::assert_send::<EmptyStream>();
crate::tests::assert_sync::<EmptyStream>();
crate::tests::assert_unpin::<EmptyStream>();
crate::test_helpers::assert_send::<EmptyStream>();
crate::test_helpers::assert_sync::<EmptyStream>();
crate::test_helpers::assert_unpin::<EmptyStream>();
}

View file

@ -203,7 +203,7 @@ pub mod future {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<HandleError<(), (), NotSendSync>>();
assert_sync::<HandleError<(), (), NotSendSync>>();

View file

@ -32,7 +32,7 @@ pub struct IntoMakeServiceWithConnectInfo<S, C> {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>();
}

View file

@ -74,3 +74,60 @@ impl<T, const N: u64> Deref for ContentLengthLimit<T, N> {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{routing::post, test_helpers::*, Router};
use bytes::Bytes;
use http::StatusCode;
use serde::Deserialize;
#[tokio::test]
async fn body_with_length_limit() {
use std::iter::repeat;
#[derive(Debug, Deserialize)]
struct Input {
foo: String,
}
const LIMIT: u64 = 8;
let app = Router::new().route(
"/",
post(|_body: ContentLengthLimit<Bytes, LIMIT>| async {}),
);
let client = TestClient::new(app);
let res = client
.post("/")
.body(repeat(0_u8).take((LIMIT - 1) as usize).collect::<Vec<_>>())
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
let res = client
.post("/")
.body(repeat(0_u8).take(LIMIT as usize).collect::<Vec<_>>())
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
let res = client
.post("/")
.body(repeat(0_u8).take((LIMIT + 1) as usize).collect::<Vec<_>>())
.send()
.await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
let res = client
.post("/")
.body(reqwest::Body::wrap_stream(futures_util::stream::iter(
vec![Ok::<_, std::io::Error>(bytes::Bytes::new())],
)))
.send()
.await;
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
}
}

View file

@ -120,7 +120,7 @@ pub struct ExtractorMiddleware<S, E> {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<ExtractorMiddleware<(), NotSendSync>>();
assert_sync::<ExtractorMiddleware<(), NotSendSync>>();
}
@ -250,3 +250,57 @@ where
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{handler::Handler, routing::get, test_helpers::*, Router};
use http::StatusCode;
#[tokio::test]
async fn test_extractor_middleware() {
struct RequireAuth;
#[async_trait::async_trait]
impl<B> FromRequest<B> for RequireAuth
where
B: Send,
{
type Rejection = StatusCode;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
if let Some(auth) = req
.headers()
.expect("headers already extracted")
.get("authorization")
.and_then(|v| v.to_str().ok())
{
if auth == "secret" {
return Ok(Self);
}
}
Err(StatusCode::UNAUTHORIZED)
}
}
async fn handler() {}
let app = Router::new().route(
"/",
get(handler.layer(extractor_middleware::<RequireAuth>())),
);
let client = TestClient::new(app);
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let res = client
.get("/")
.header(http::header::AUTHORIZATION, "secret")
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
}
}

View file

@ -84,3 +84,100 @@ where
Ok(matched_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{extract::Extension, handler::Handler, routing::get, test_helpers::*, Router};
use http::Request;
use std::task::{Context, Poll};
use tower_service::Service;
#[derive(Clone)]
struct SetMatchedPathExtension<S>(S);
impl<B, S> Service<Request<B>> for SetMatchedPathExtension<S>
where
S: Service<Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready(cx)
}
fn call(&mut self, mut req: Request<B>) -> Self::Future {
let path = req
.extensions()
.get::<MatchedPath>()
.unwrap()
.as_str()
.to_string();
req.extensions_mut().insert(MatchedPathFromMiddleware(path));
self.0.call(req)
}
}
#[derive(Clone)]
struct MatchedPathFromMiddleware(String);
#[tokio::test]
async fn access_matched_path() {
let api = Router::new().route(
"/users/:id",
get(|path: MatchedPath| async move { path.as_str().to_string() }),
);
async fn handler(
path: MatchedPath,
Extension(MatchedPathFromMiddleware(path_from_middleware)): Extension<
MatchedPathFromMiddleware,
>,
) -> String {
format!(
"extractor = {}, middleware = {}",
path.as_str(),
path_from_middleware
)
}
let app = Router::new()
.route(
"/:key",
get(|path: MatchedPath| async move { path.as_str().to_string() }),
)
.nest("/api", api)
.nest(
"/public",
Router::new().route("/assets/*path", get(handler)),
)
.nest("/foo", handler.into_service())
.layer(tower::layer::layer_fn(SetMatchedPathExtension));
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
assert_eq!(res.text().await, "/:key");
let res = client.get("/api/users/123").send().await;
assert_eq!(res.text().await, "/api/users/:id");
let res = client.get("/public/assets/css/style.css").send().await;
assert_eq!(
res.text().await,
"extractor = /public/assets/*path, middleware = /public/assets/*path"
);
let res = client.get("/foo/bar/baz").send().await;
assert_eq!(
res.text().await,
format!(
"extractor = /foo/*{}, middleware = /foo/*{}",
crate::routing::NEST_TAIL_PARAM,
crate::routing::NEST_TAIL_PARAM,
),
);
}
}

View file

@ -356,3 +356,20 @@ pub(crate) fn has_content_type<B>(
pub(crate) fn take_body<B>(req: &mut RequestParts<B>) -> Result<B, BodyAlreadyExtracted> {
req.take_body().ok_or(BodyAlreadyExtracted)
}
#[cfg(test)]
mod tests {
use crate::test_helpers::*;
use crate::{routing::get, Router};
#[tokio::test]
async fn consume_body() {
let app = Router::new().route("/", get(|body: String| async { body }));
let client = TestClient::new(app);
let res = client.get("/").body("foo").send().await;
let body = res.text().await;
assert_eq!(body, "foo");
}
}

View file

@ -173,11 +173,44 @@ where
#[cfg(test)]
mod tests {
use http::StatusCode;
use super::*;
use crate::tests::*;
use crate::test_helpers::*;
use crate::{routing::get, Router};
use std::collections::HashMap;
#[tokio::test]
async fn extracting_url_params() {
let app = Router::new().route(
"/users/:id",
get(|Path(id): Path<i32>| async move {
assert_eq!(id, 42);
})
.post(|Path(params_map): Path<HashMap<String, i32>>| async move {
assert_eq!(params_map.get("id").unwrap(), &1337);
}),
);
let client = TestClient::new(app);
let res = client.get("/users/42").send().await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.post("/users/1337").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn extracting_url_params_multiple_times() {
let app = Router::new().route("/users/:id", get(|_: Path<i32>, _: Path<String>| async {}));
let client = TestClient::new(app);
let res = client.get("/users/42").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn percent_decoding() {
let app = Router::new().route(
@ -235,4 +268,17 @@ mod tests {
let res = client.get("/bar/baz/qux").send().await;
assert_eq!(res.text().await, "/baz/qux");
}
#[tokio::test]
async fn captures_dont_match_empty_segments() {
let app = Router::new().route("/:key", get(|| async {}));
let client = TestClient::new(app);
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/foo").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
}

View file

@ -238,8 +238,8 @@ impl fmt::Debug for BodyStream {
#[test]
fn body_stream_traits() {
crate::tests::assert_send::<BodyStream>();
crate::tests::assert_sync::<BodyStream>();
crate::test_helpers::assert_send::<BodyStream>();
crate::test_helpers::assert_sync::<BodyStream>();
}
/// Extractor that extracts the raw request body.
@ -326,7 +326,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::{body::Body, routing::post, tests::*, Router};
use crate::{body::Body, routing::post, test_helpers::*, Router};
use http::StatusCode;
#[tokio::test]

View file

@ -140,7 +140,7 @@ impl std::error::Error for TypedHeaderRejection {
#[cfg(test)]
mod tests {
use super::*;
use crate::{response::IntoResponse, routing::get, tests::*, Router};
use crate::{response::IntoResponse, routing::get, test_helpers::*, Router};
#[tokio::test]
async fn typed_header() {

View file

@ -19,7 +19,7 @@ pub struct IntoService<H, B, T> {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<IntoService<(), NotSendSync, NotSendSync>>();
assert_sync::<IntoService<(), NotSendSync, NotSendSync>>();
}

View file

@ -384,10 +384,30 @@ impl<S, T> Layered<S, T> {
}
}
#[test]
fn traits() {
use crate::routing::MethodRouter;
use crate::tests::*;
assert_send::<MethodRouter<(), NotSendSync, NotSendSync, ()>>();
assert_sync::<MethodRouter<(), NotSendSync, NotSendSync, ()>>();
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use http::StatusCode;
#[tokio::test]
async fn handler_into_service() {
async fn handle(body: String) -> impl IntoResponse {
format!("you said: {}", body)
}
let client = TestClient::new(handle.into_service());
let res = client.post("/").body("hi there!").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "you said: hi there!");
}
#[test]
fn traits() {
use crate::routing::MethodRouter;
use crate::test_helpers::*;
assert_send::<MethodRouter<(), NotSendSync, NotSendSync, ()>>();
assert_sync::<MethodRouter<(), NotSendSync, NotSendSync, ()>>();
}
}

View file

@ -195,3 +195,69 @@ where
res
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{routing::post, test_helpers::*, Router};
use serde::Deserialize;
use serde_json::{json, Value};
#[tokio::test]
async fn deserialize_body() {
#[derive(Debug, Deserialize)]
struct Input {
foo: String,
}
let app = Router::new().route("/", post(|input: Json<Input>| async { input.0.foo }));
let client = TestClient::new(app);
let res = client.post("/").json(&json!({ "foo": "bar" })).send().await;
let body = res.text().await;
assert_eq!(body, "bar");
}
#[tokio::test]
async fn consume_body_to_json_requires_json_content_type() {
#[derive(Debug, Deserialize)]
struct Input {
foo: String,
}
let app = Router::new().route("/", post(|input: Json<Input>| async { input.0.foo }));
let client = TestClient::new(app);
let res = client.post("/").body(r#"{ "foo": "bar" }"#).send().await;
let status = res.status();
dbg!(res.text().await);
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn json_content_types() {
async fn valid_json_content_type(content_type: &str) -> bool {
println!("testing {:?}", content_type);
let app = Router::new().route("/", post(|Json(_): Json<Value>| async {}));
let res = TestClient::new(app)
.post("/")
.header("content-type", content_type)
.body("{}")
.send()
.await;
res.status() == StatusCode::OK
}
assert!(valid_json_content_type("application/json").await);
assert!(valid_json_content_type("application/json; charset=utf-8").await);
assert!(valid_json_content_type("application/json;charset=utf-8").await);
assert!(valid_json_content_type("application/cloudevents+json").await);
assert!(!valid_json_content_type("text/json").await);
}
}

View file

@ -325,7 +325,7 @@ pub mod response;
pub mod routing;
#[cfg(test)]
mod tests;
mod test_helpers;
pub use add_extension::{AddExtension, AddExtensionLayer};
#[doc(no_inline)]

View file

@ -50,7 +50,7 @@ mod tests {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<IntoMakeService<Body>>();
}

View file

@ -74,7 +74,7 @@ mod tests {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<MethodNotAllowed<NotSendSync>>();
assert_sync::<MethodNotAllowed<NotSendSync>>();

View file

@ -37,6 +37,9 @@ mod not_found;
mod route;
mod strip_prefix;
#[cfg(tests)]
mod tests;
pub use self::{
into_make_service::IntoMakeService, method_filter::MethodFilter,
method_not_allowed::MethodNotAllowed, route::Route,
@ -574,6 +577,6 @@ where
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<Router<()>>();
}

View file

@ -95,7 +95,7 @@ mod tests {
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<Route<()>>();
}
}

View file

@ -553,7 +553,7 @@ where
#[test]
fn traits() {
use crate::tests::*;
use crate::test_helpers::*;
assert_send::<MethodRouter<(), (), NotSendSync>>();
assert_sync::<MethodRouter<(), (), NotSendSync>>();

View file

@ -1,7 +1,6 @@
#![allow(clippy::blacklisted_name)]
use crate::error_handling::HandleErrorLayer;
use crate::extract::{Extension, MatchedPath};
use crate::test_helpers::*;
use crate::BoxError;
use crate::{
extract::{self, Path},
@ -31,12 +30,9 @@ use tower::{service_fn, timeout::TimeoutLayer, ServiceBuilder, ServiceExt};
use tower_http::auth::RequireAuthorizationLayer;
use tower_service::Service;
pub(crate) use helpers::*;
mod fallback;
mod get_to_head;
mod handle_error;
mod helpers;
mod merge;
mod nest;
@ -73,105 +69,6 @@ async fn hello_world() {
assert_eq!(body, "users#create");
}
#[tokio::test]
async fn consume_body() {
let app = Router::new().route("/", get(|body: String| async { body }));
let client = TestClient::new(app);
let res = client.get("/").body("foo").send().await;
let body = res.text().await;
assert_eq!(body, "foo");
}
#[tokio::test]
async fn deserialize_body() {
#[derive(Debug, Deserialize)]
struct Input {
foo: String,
}
let app = Router::new().route(
"/",
post(|input: extract::Json<Input>| async { input.0.foo }),
);
let client = TestClient::new(app);
let res = client.post("/").json(&json!({ "foo": "bar" })).send().await;
let body = res.text().await;
assert_eq!(body, "bar");
}
#[tokio::test]
async fn consume_body_to_json_requires_json_content_type() {
#[derive(Debug, Deserialize)]
struct Input {
foo: String,
}
let app = Router::new().route(
"/",
post(|input: extract::Json<Input>| async { input.0.foo }),
);
let client = TestClient::new(app);
let res = client.post("/").body(r#"{ "foo": "bar" }"#).send().await;
let status = res.status();
dbg!(res.text().await);
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn body_with_length_limit() {
use std::iter::repeat;
#[derive(Debug, Deserialize)]
struct Input {
foo: String,
}
const LIMIT: u64 = 8;
let app = Router::new().route(
"/",
post(|_body: extract::ContentLengthLimit<Bytes, LIMIT>| async {}),
);
let client = TestClient::new(app);
let res = client
.post("/")
.body(repeat(0_u8).take((LIMIT - 1) as usize).collect::<Vec<_>>())
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
let res = client
.post("/")
.body(repeat(0_u8).take(LIMIT as usize).collect::<Vec<_>>())
.send()
.await;
assert_eq!(res.status(), StatusCode::OK);
let res = client
.post("/")
.body(repeat(0_u8).take((LIMIT + 1) as usize).collect::<Vec<_>>())
.send()
.await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
let res = client
.post("/")
.body(reqwest::Body::wrap_stream(futures_util::stream::iter(
vec![Ok::<_, std::io::Error>(bytes::Bytes::new())],
)))
.send()
.await;
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
}
#[tokio::test]
async fn routing() {
let app = Router::new()
@ -209,42 +106,8 @@ async fn routing() {
}
#[tokio::test]
async fn extracting_url_params() {
let app = Router::new().route(
"/users/:id",
get(|Path(id): Path<i32>| async move {
assert_eq!(id, 42);
})
.post(|Path(params_map): Path<HashMap<String, i32>>| async move {
assert_eq!(params_map.get("id").unwrap(), &1337);
}),
);
let client = TestClient::new(app);
let res = client.get("/users/42").send().await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.post("/users/1337").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn extracting_url_params_multiple_times() {
let app = Router::new().route(
"/users/:id",
get(|_: extract::Path<i32>, _: extract::Path<String>| async {}),
);
let client = TestClient::new(app);
let res = client.get("/users/42").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn boxing() {
let app = Router::new()
async fn router_type_doesnt_change() {
let app: Router = Router::new()
.route(
"/",
on(MethodFilter::GET, |_: Request<Body>| async {
@ -351,49 +214,6 @@ async fn service_in_bottom() {
TestClient::new(app);
}
#[tokio::test]
async fn test_extractor_middleware() {
struct RequireAuth;
#[async_trait::async_trait]
impl<B> extract::FromRequest<B> for RequireAuth
where
B: Send,
{
type Rejection = StatusCode;
async fn from_request(req: &mut extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
if let Some(auth) = req
.headers()
.expect("headers already extracted")
.get("authorization")
.and_then(|v| v.to_str().ok())
{
if auth == "secret" {
return Ok(Self);
}
}
Err(StatusCode::UNAUTHORIZED)
}
}
async fn handler() {}
let app = Router::new().route(
"/",
get(handler.layer(extract::extractor_middleware::<RequireAuth>())),
);
let client = TestClient::new(app);
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let res = client.get("/").header(AUTHORIZATION, "secret").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn wrong_method_handler() {
let app = Router::new()
@ -470,56 +290,6 @@ async fn multiple_methods_for_one_handler() {
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn handler_into_service() {
async fn handle(body: String) -> impl IntoResponse {
format!("you said: {}", body)
}
let client = TestClient::new(handle.into_service());
let res = client.post("/").body("hi there!").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "you said: hi there!");
}
#[tokio::test]
async fn captures_dont_match_empty_segments() {
let app = Router::new().route("/:key", get(|| async {}));
let client = TestClient::new(app);
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/foo").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn json_content_types() {
async fn valid_json_content_type(content_type: &str) -> bool {
println!("testing {:?}", content_type);
let app = Router::new().route("/", post(|Json(_): Json<Value>| async {}));
let res = TestClient::new(app)
.post("/")
.header("content-type", content_type)
.body("{}")
.send()
.await;
res.status() == StatusCode::OK
}
assert!(valid_json_content_type("application/json").await);
assert!(valid_json_content_type("application/json; charset=utf-8").await);
assert!(valid_json_content_type("application/json;charset=utf-8").await);
assert!(valid_json_content_type("application/cloudevents+json").await);
assert!(!valid_json_content_type("text/json").await);
}
#[tokio::test]
async fn wildcard_sees_whole_url() {
let app = Router::new().route("/api/*rest", get(|uri: Uri| async move { uri.to_string() }));
@ -634,94 +404,6 @@ async fn wildcard_with_trailing_slash() {
);
}
#[derive(Clone)]
struct SetMatchedPathExtension<S>(S);
impl<B, S> Service<Request<B>> for SetMatchedPathExtension<S>
where
S: Service<Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready(cx)
}
fn call(&mut self, mut req: Request<B>) -> Self::Future {
let path = req
.extensions()
.get::<MatchedPath>()
.unwrap()
.as_str()
.to_string();
req.extensions_mut().insert(MatchedPathFromMiddleware(path));
self.0.call(req)
}
}
#[derive(Clone)]
struct MatchedPathFromMiddleware(String);
#[tokio::test]
async fn access_matched_path() {
let api = Router::new().route(
"/users/:id",
get(|path: MatchedPath| async move { path.as_str().to_string() }),
);
async fn handler(
path: MatchedPath,
Extension(MatchedPathFromMiddleware(path_from_middleware)): Extension<
MatchedPathFromMiddleware,
>,
) -> String {
format!(
"extractor = {}, middleware = {}",
path.as_str(),
path_from_middleware
)
}
let app = Router::new()
.route(
"/:key",
get(|path: MatchedPath| async move { path.as_str().to_string() }),
)
.nest("/api", api)
.nest(
"/public",
Router::new().route("/assets/*path", get(handler)),
)
.nest("/foo", handler.into_service())
.layer(tower::layer::layer_fn(SetMatchedPathExtension));
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
assert_eq!(res.text().await, "/:key");
let res = client.get("/api/users/123").send().await;
assert_eq!(res.text().await, "/api/users/:id");
let res = client.get("/public/assets/css/style.css").send().await;
assert_eq!(
res.text().await,
"extractor = /public/assets/*path, middleware = /public/assets/*path"
);
let res = client.get("/foo/bar/baz").send().await;
assert_eq!(
res.text().await,
format!(
"extractor = /foo/*{}, middleware = /foo/*{}",
crate::routing::NEST_TAIL_PARAM,
crate::routing::NEST_TAIL_PARAM,
),
);
}
#[tokio::test]
async fn static_and_dynamic_paths() {
let app = Router::new()
@ -801,9 +483,3 @@ async fn middleware_still_run_for_unmatched_requests() {
async fn routing_to_router_panics() {
TestClient::new(Router::new().route("/", Router::new()));
}
pub(crate) fn assert_send<T: Send>() {}
pub(crate) fn assert_sync<T: Sync>() {}
pub(crate) fn assert_unpin<T: Unpin>() {}
pub(crate) struct NotSendSync(*const ());

View file

@ -1,16 +1,22 @@
#![allow(clippy::blacklisted_name)]
use crate::BoxError;
use http::{
header::{HeaderName, HeaderValue},
Request, StatusCode,
};
use hyper::{Body, Server};
use std::{
convert::TryFrom,
net::{SocketAddr, TcpListener},
};
use std::net::SocketAddr;
use std::{convert::TryFrom, net::TcpListener};
use tower::make::Shared;
use tower_service::Service;
pub(crate) fn assert_send<T: Send>() {}
pub(crate) fn assert_sync<T: Sync>() {}
pub(crate) fn assert_unpin<T: Unpin>() {}
pub(crate) struct NotSendSync(*const ());
pub(crate) struct TestClient {
client: reqwest::Client,
addr: SocketAddr,
@ -53,12 +59,14 @@ impl TestClient {
}
}
#[allow(dead_code)]
pub(crate) fn put(&self, url: &str) -> RequestBuilder {
RequestBuilder {
builder: self.client.put(format!("http://{}{}", self.addr, url)),
}
}
#[allow(dead_code)]
pub(crate) fn patch(&self, url: &str) -> RequestBuilder {
RequestBuilder {
builder: self.client.patch(format!("http://{}{}", self.addr, url)),
@ -71,8 +79,8 @@ pub(crate) struct RequestBuilder {
}
impl RequestBuilder {
pub(crate) async fn send(self) -> Response {
Response {
pub(crate) async fn send(self) -> TestResponse {
TestResponse {
response: self.builder.send().await.unwrap(),
}
}
@ -101,15 +109,16 @@ impl RequestBuilder {
}
}
pub(crate) struct Response {
pub(crate) struct TestResponse {
response: reqwest::Response,
}
impl Response {
impl TestResponse {
pub(crate) async fn text(self) -> String {
self.response.text().await.unwrap()
}
#[allow(dead_code)]
pub(crate) async fn json<T>(self) -> T
where
T: serde::de::DeserializeOwned,