From 0f1f28062d944bd22dde25d7b168c01d859b9ae2 Mon Sep 17 00:00:00 2001 From: David Pedersen <david.pdrsn@gmail.com> Date: Wed, 3 Nov 2021 10:22:31 +0100 Subject: [PATCH] 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 --- .github/workflows/CI.yml | 25 +- src/body/stream_body.rs | 6 +- src/error_handling/mod.rs | 2 +- src/extract/connect_info.rs | 2 +- src/extract/content_length_limit.rs | 57 ++++ src/extract/extractor_middleware.rs | 56 +++- src/extract/matched_path.rs | 97 +++++++ src/extract/mod.rs | 17 ++ src/extract/path/mod.rs | 48 +++- src/extract/request_parts.rs | 6 +- src/extract/typed_header.rs | 2 +- src/handler/into_service.rs | 2 +- src/handler/mod.rs | 32 ++- src/json.rs | 66 +++++ src/lib.rs | 2 +- src/routing/into_make_service.rs | 2 +- src/routing/method_not_allowed.rs | 2 +- src/routing/mod.rs | 5 +- src/routing/route.rs | 2 +- src/routing/service_method_routing.rs | 2 +- src/{ => routing}/tests/fallback.rs | 0 src/{ => routing}/tests/get_to_head.rs | 0 src/{ => routing}/tests/handle_error.rs | 0 src/{ => routing}/tests/merge.rs | 0 src/{ => routing}/tests/mod.rs | 330 +--------------------- src/{ => routing}/tests/nest.rs | 0 src/{tests/helpers.rs => test_helpers.rs} | 25 +- 27 files changed, 427 insertions(+), 361 deletions(-) rename src/{ => routing}/tests/fallback.rs (100%) rename src/{ => routing}/tests/get_to_head.rs (100%) rename src/{ => routing}/tests/handle_error.rs (100%) rename src/{ => routing}/tests/merge.rs (100%) rename src/{ => routing}/tests/mod.rs (60%) rename src/{ => routing}/tests/nest.rs (100%) rename src/{tests/helpers.rs => test_helpers.rs} (86%) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2c307695..512af468 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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 diff --git a/src/body/stream_body.rs b/src/body/stream_body.rs index b9e360d7..616cc7e9 100644 --- a/src/body/stream_body.rs +++ b/src/body/stream_body.rs @@ -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>(); } diff --git a/src/error_handling/mod.rs b/src/error_handling/mod.rs index 54456dad..f97bcdc0 100644 --- a/src/error_handling/mod.rs +++ b/src/error_handling/mod.rs @@ -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>>(); diff --git a/src/extract/connect_info.rs b/src/extract/connect_info.rs index 18b419d8..9fcd18e0 100644 --- a/src/extract/connect_info.rs +++ b/src/extract/connect_info.rs @@ -32,7 +32,7 @@ pub struct IntoMakeServiceWithConnectInfo<S, C> { #[test] fn traits() { - use crate::tests::*; + use crate::test_helpers::*; assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>(); } diff --git a/src/extract/content_length_limit.rs b/src/extract/content_length_limit.rs index fc8cb8f4..fe8961d4 100644 --- a/src/extract/content_length_limit.rs +++ b/src/extract/content_length_limit.rs @@ -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); + } +} diff --git a/src/extract/extractor_middleware.rs b/src/extract/extractor_middleware.rs index 5a68f802..e7aeee79 100644 --- a/src/extract/extractor_middleware.rs +++ b/src/extract/extractor_middleware.rs @@ -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); + } +} diff --git a/src/extract/matched_path.rs b/src/extract/matched_path.rs index 80947612..7218393b 100644 --- a/src/extract/matched_path.rs +++ b/src/extract/matched_path.rs @@ -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, + ), + ); + } +} diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 6640d74c..4c9c9ad7 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -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"); + } +} diff --git a/src/extract/path/mod.rs b/src/extract/path/mod.rs index e74b977b..d7313050 100644 --- a/src/extract/path/mod.rs +++ b/src/extract/path/mod.rs @@ -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); + } } diff --git a/src/extract/request_parts.rs b/src/extract/request_parts.rs index c76d39c8..0a84dee2 100644 --- a/src/extract/request_parts.rs +++ b/src/extract/request_parts.rs @@ -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] diff --git a/src/extract/typed_header.rs b/src/extract/typed_header.rs index 7b4abb73..17c037e5 100644 --- a/src/extract/typed_header.rs +++ b/src/extract/typed_header.rs @@ -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() { diff --git a/src/handler/into_service.rs b/src/handler/into_service.rs index ffde1359..c49de9f1 100644 --- a/src/handler/into_service.rs +++ b/src/handler/into_service.rs @@ -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>>(); } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index bbaf6b9b..bbad6d9a 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -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, ()>>(); + } } diff --git a/src/json.rs b/src/json.rs index 93f9963a..71d65fad 100644 --- a/src/json.rs +++ b/src/json.rs @@ -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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 486afc92..b582a529 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)] diff --git a/src/routing/into_make_service.rs b/src/routing/into_make_service.rs index 142cf9f8..fbc57c4a 100644 --- a/src/routing/into_make_service.rs +++ b/src/routing/into_make_service.rs @@ -50,7 +50,7 @@ mod tests { #[test] fn traits() { - use crate::tests::*; + use crate::test_helpers::*; assert_send::<IntoMakeService<Body>>(); } diff --git a/src/routing/method_not_allowed.rs b/src/routing/method_not_allowed.rs index a1d47665..6f812b11 100644 --- a/src/routing/method_not_allowed.rs +++ b/src/routing/method_not_allowed.rs @@ -74,7 +74,7 @@ mod tests { #[test] fn traits() { - use crate::tests::*; + use crate::test_helpers::*; assert_send::<MethodNotAllowed<NotSendSync>>(); assert_sync::<MethodNotAllowed<NotSendSync>>(); diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 4c5efadb..e856127d 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -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<()>>(); } diff --git a/src/routing/route.rs b/src/routing/route.rs index 9e7f661b..82fc1447 100644 --- a/src/routing/route.rs +++ b/src/routing/route.rs @@ -95,7 +95,7 @@ mod tests { #[test] fn traits() { - use crate::tests::*; + use crate::test_helpers::*; assert_send::<Route<()>>(); } } diff --git a/src/routing/service_method_routing.rs b/src/routing/service_method_routing.rs index ccaf6c43..f26dde5c 100644 --- a/src/routing/service_method_routing.rs +++ b/src/routing/service_method_routing.rs @@ -553,7 +553,7 @@ where #[test] fn traits() { - use crate::tests::*; + use crate::test_helpers::*; assert_send::<MethodRouter<(), (), NotSendSync>>(); assert_sync::<MethodRouter<(), (), NotSendSync>>(); diff --git a/src/tests/fallback.rs b/src/routing/tests/fallback.rs similarity index 100% rename from src/tests/fallback.rs rename to src/routing/tests/fallback.rs diff --git a/src/tests/get_to_head.rs b/src/routing/tests/get_to_head.rs similarity index 100% rename from src/tests/get_to_head.rs rename to src/routing/tests/get_to_head.rs diff --git a/src/tests/handle_error.rs b/src/routing/tests/handle_error.rs similarity index 100% rename from src/tests/handle_error.rs rename to src/routing/tests/handle_error.rs diff --git a/src/tests/merge.rs b/src/routing/tests/merge.rs similarity index 100% rename from src/tests/merge.rs rename to src/routing/tests/merge.rs diff --git a/src/tests/mod.rs b/src/routing/tests/mod.rs similarity index 60% rename from src/tests/mod.rs rename to src/routing/tests/mod.rs index 68bb5079..3a77ddb1 100644 --- a/src/tests/mod.rs +++ b/src/routing/tests/mod.rs @@ -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 ()); diff --git a/src/tests/nest.rs b/src/routing/tests/nest.rs similarity index 100% rename from src/tests/nest.rs rename to src/routing/tests/nest.rs diff --git a/src/tests/helpers.rs b/src/test_helpers.rs similarity index 86% rename from src/tests/helpers.rs rename to src/test_helpers.rs index bcd3c73d..113f83dd 100644 --- a/src/tests/helpers.rs +++ b/src/test_helpers.rs @@ -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,