Skip to content

Commit 9409a14

Browse files
authored
Add service request validation and tests (#23)
Signed-off-by: declark1 <[email protected]>
1 parent 2be1225 commit 9409a14

File tree

6 files changed

+89
-26
lines changed

6 files changed

+89
-26
lines changed

mocktail-tests/tests/misc/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod validation;
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use anyhow::Error;
2+
use mocktail::prelude::*;
3+
use test_log::test;
4+
5+
#[test(tokio::test)]
6+
async fn test_grpc_service() -> Result<(), Error> {
7+
let server = MockServer::new("test").grpc();
8+
server.start().await?;
9+
10+
let client = reqwest::Client::builder().http2_prior_knowledge().build()?;
11+
12+
// Invalid method
13+
let response = client.get(server.url("/hello")).send().await?;
14+
assert_eq!(response.status(), http::StatusCode::METHOD_NOT_ALLOWED);
15+
assert!(response
16+
.headers()
17+
.get("allow")
18+
.is_some_and(|value| value == "POST"));
19+
20+
// Invalid content-type
21+
let response = client.post(server.url("/hello")).send().await?;
22+
assert_eq!(response.status(), http::StatusCode::UNSUPPORTED_MEDIA_TYPE);
23+
assert!(response
24+
.headers()
25+
.get("accept-post")
26+
.is_some_and(|value| value == "application/grpc"));
27+
28+
Ok(())
29+
}
30+
31+
#[test(tokio::test)]
32+
async fn test_http_service() -> Result<(), Error> {
33+
let server = MockServer::new("test");
34+
server.start().await?;
35+
36+
let client = reqwest::Client::builder().http2_prior_knowledge().build()?;
37+
38+
// Invalid method
39+
let response = client.patch(server.url("/hello")).send().await?;
40+
assert_eq!(response.status(), http::StatusCode::METHOD_NOT_ALLOWED);
41+
assert!(response.headers().get("allow").is_some());
42+
43+
Ok(())
44+
}

mocktail/src/headers.rs

-8
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,6 @@ impl Headers {
7070
other.is_subset(self)
7171
}
7272

73-
/// Returns true if the headers contains a `content-type` header equal to value.
74-
pub fn has_content_type(&self, value: &str) -> bool {
75-
if let Some(content_type) = self.get("content-type") {
76-
return content_type.starts_with(value);
77-
}
78-
false
79-
}
80-
8173
/// Returns an iterator over the headers.
8274
pub fn iter(&self) -> std::slice::Iter<'_, (HeaderName, HeaderValue)> {
8375
self.0.iter()

mocktail/src/server.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ where
195195
ServerKind::Grpc => conn::auto::Builder::new(TokioExecutor::new()).http2_only(),
196196
};
197197
if let Err(err) = builder.serve_connection(io, service).await {
198-
error!("connection error: {err}");
198+
debug!("connection error: {err}");
199199
}
200200
debug!("connection dropped: {addr}");
201201
});

mocktail/src/service/grpc.rs

+21-16
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use tokio_stream::wrappers::ReceiverStream;
1515
use tonic::body::BoxBody;
1616
use tracing::debug;
1717

18-
use crate::{headers::Headers, mock_set::MockSet, request::Request};
18+
use crate::{mock_set::MockSet, request::Request};
1919

2020
/// Mock gRPC service.
2121
#[derive(Debug, Clone)]
@@ -38,10 +38,27 @@ impl Service<http::Request<Incoming>> for GrpcMockService {
3838
let mocks = self.mocks.clone();
3939
let fut = async move {
4040
debug!(?req, "handling request");
41-
let headers: Headers = req.headers().into();
42-
if !headers.has_content_type("application/grpc") {
43-
return Ok(invalid_content_type_response());
41+
42+
if req.method() != http::Method::POST {
43+
return Ok(http::Response::builder()
44+
.status(http::StatusCode::METHOD_NOT_ALLOWED)
45+
.header("Allow", "POST")
46+
.body(tonic::body::empty_body())
47+
.unwrap());
48+
}
49+
let content_type = req.headers().get("content-type");
50+
if !content_type.is_some_and(|v| {
51+
v.to_str()
52+
.unwrap_or_default()
53+
.starts_with("application/grpc")
54+
}) {
55+
return Ok(http::Response::builder()
56+
.status(http::StatusCode::UNSUPPORTED_MEDIA_TYPE)
57+
.header("Accept-Post", "application/grpc")
58+
.body(tonic::body::empty_body())
59+
.unwrap());
4460
}
61+
4562
let (parts, body) = req.into_parts();
4663
let mut stream = body.into_data_stream();
4764

@@ -106,18 +123,6 @@ impl Service<http::Request<Incoming>> for GrpcMockService {
106123
}
107124
}
108125

109-
fn invalid_content_type_response() -> http::Response<BoxBody> {
110-
http::Response::builder()
111-
.header("content-type", "application/grpc")
112-
.header("grpc-status", tonic::Code::InvalidArgument as i32)
113-
.header(
114-
"grpc-message",
115-
"invalid content-type: expected `application/grpc`",
116-
)
117-
.body(tonic::body::empty_body())
118-
.unwrap()
119-
}
120-
121126
fn mock_not_found_trailer() -> HeaderMap {
122127
let mut headers = HeaderMap::new();
123128
headers.insert("grpc-status", (tonic::Code::NotFound as i32).into());

mocktail/src/service/http.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use bytes::{Bytes, BytesMut};
88
use futures::{future::BoxFuture, StreamExt};
99
use http::HeaderMap;
1010
use http_body::{Body as _, Frame};
11-
use http_body_util::{BodyExt, Full, StreamBody};
11+
use http_body_util::{BodyExt, Empty, Full, StreamBody};
1212
use hyper::{body::Incoming, service::Service};
1313
use tokio::sync::mpsc;
1414
use tokio_stream::wrappers::ReceiverStream;
@@ -18,6 +18,14 @@ use crate::{mock_set::MockSet, request::Request};
1818

1919
type BoxBody = http_body_util::combinators::BoxBody<Bytes, hyper::Error>;
2020

21+
const ALLOWED_METHODS: [http::Method; 5] = [
22+
http::Method::GET,
23+
http::Method::POST,
24+
http::Method::PUT,
25+
http::Method::HEAD,
26+
http::Method::DELETE,
27+
];
28+
2129
/// Mock HTTP service.
2230
#[derive(Debug, Clone)]
2331
pub struct HttpMockService {
@@ -39,6 +47,15 @@ impl Service<http::Request<Incoming>> for HttpMockService {
3947
let mocks = self.mocks.clone();
4048
let fut = async move {
4149
debug!(?req, "handling request");
50+
51+
if !ALLOWED_METHODS.contains(req.method()) {
52+
return Ok(http::Response::builder()
53+
.status(http::StatusCode::METHOD_NOT_ALLOWED)
54+
.header("Allow", "GET, POST, PUT, HEAD, DELETE")
55+
.body(empty())
56+
.unwrap());
57+
}
58+
4259
let (parts, mut body) = req.into_parts();
4360

4461
// Get initial data frame
@@ -144,3 +161,7 @@ impl Service<http::Request<Incoming>> for HttpMockService {
144161
fn full(data: Bytes) -> BoxBody {
145162
Full::new(data).map_err(|err| match err {}).boxed()
146163
}
164+
165+
fn empty() -> BoxBody {
166+
Empty::new().map_err(|err| match err {}).boxed()
167+
}

0 commit comments

Comments
 (0)