mirror of
https://github.com/tokio-rs/axum.git
synced 2025-03-13 19:27:53 +01:00
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:
parent
420918a53a
commit
0f1f28062d
27 changed files with 427 additions and 361 deletions
25
.github/workflows/CI.yml
vendored
25
.github/workflows/CI.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
@ -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>>();
|
||||
|
|
|
@ -32,7 +32,7 @@ pub struct IntoMakeServiceWithConnectInfo<S, C> {
|
|||
|
||||
#[test]
|
||||
fn traits() {
|
||||
use crate::tests::*;
|
||||
use crate::test_helpers::*;
|
||||
assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>>();
|
||||
}
|
||||
|
|
|
@ -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, ()>>();
|
||||
}
|
||||
}
|
||||
|
|
66
src/json.rs
66
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -50,7 +50,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn traits() {
|
||||
use crate::tests::*;
|
||||
use crate::test_helpers::*;
|
||||
|
||||
assert_send::<IntoMakeService<Body>>();
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn traits() {
|
||||
use crate::tests::*;
|
||||
use crate::test_helpers::*;
|
||||
|
||||
assert_send::<MethodNotAllowed<NotSendSync>>();
|
||||
assert_sync::<MethodNotAllowed<NotSendSync>>();
|
||||
|
|
|
@ -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<()>>();
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn traits() {
|
||||
use crate::tests::*;
|
||||
use crate::test_helpers::*;
|
||||
assert_send::<Route<()>>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -553,7 +553,7 @@ where
|
|||
|
||||
#[test]
|
||||
fn traits() {
|
||||
use crate::tests::*;
|
||||
use crate::test_helpers::*;
|
||||
|
||||
assert_send::<MethodRouter<(), (), NotSendSync>>();
|
||||
assert_sync::<MethodRouter<(), (), NotSendSync>>();
|
||||
|
|
|
@ -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 ());
|
|
@ -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,
|
Loading…
Add table
Reference in a new issue