Skip to content

Commit 33936ba

Browse files
committed
feat: Tests for headers
1 parent 32550f4 commit 33936ba

File tree

5 files changed

+347
-142
lines changed

5 files changed

+347
-142
lines changed

deny.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ notice = "warn"
4848
# A list of advisory IDs to ignore. Note that ignored advisories will still
4949
# output a note when they are encountered.
5050
ignore = [
51-
"RUSTSEC-2021-0131", # Open PR to resolve this https://github.com/actix/actix-web/pull/2538
51+
# "RUSTSEC-2021-0131",
5252
]
5353
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
5454
# lower than the range specified will be ignored. Note that ignored advisories

openapi.json

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,94 @@
133133
}
134134
}
135135
}
136+
},
137+
"/parameters/header": {
138+
"description": "Test the valid types to send in headers.",
139+
"post": {
140+
"tags": [
141+
"parameters"
142+
],
143+
"parameters": [
144+
{
145+
"required": true,
146+
"schema": {
147+
"type": "boolean"
148+
},
149+
"name": "Boolean-Header",
150+
"in": "header"
151+
},
152+
{
153+
"required": true,
154+
"schema": {
155+
"type": "string"
156+
},
157+
"name": "String-Header",
158+
"in": "header"
159+
},
160+
{
161+
"required": true,
162+
"schema": {
163+
"type": "number"
164+
},
165+
"name": "Number-Header",
166+
"in": "header"
167+
},
168+
{
169+
"required": true,
170+
"schema": {
171+
"type": "integer"
172+
},
173+
"name": "Integer-Header",
174+
"in": "header"
175+
}
176+
],
177+
"responses": {
178+
"200": {
179+
"description": "OK",
180+
"content": {
181+
"application/json": {
182+
"schema": {
183+
"type": "object",
184+
"properties": {
185+
"boolean": {
186+
"type": "boolean",
187+
"description": "Echo of the 'Boolean-Header' input parameter from the header."
188+
},
189+
"string": {
190+
"type": "string",
191+
"description": "Echo of the 'String-Header' input parameter from the header."
192+
},
193+
"number": {
194+
"type": "number",
195+
"description": "Echo of the 'Number-Header' input parameter from the header."
196+
},
197+
"integer": {
198+
"type": "integer",
199+
"description": "Echo of the 'Integer-Header' input parameter from the header."
200+
}
201+
},
202+
"required": [
203+
"boolean",
204+
"string",
205+
"number",
206+
"integer"
207+
]
208+
}
209+
}
210+
}
211+
},
212+
"400": {
213+
"content": {
214+
"application/json": {
215+
"schema": {
216+
"$ref": "#/components/schemas/PublicError"
217+
}
218+
}
219+
},
220+
"description": "Bad Request"
221+
}
222+
}
223+
}
136224
}
137225
}
138226
}

src/headers.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use axum::http::HeaderMap;
2+
use axum::Json;
3+
use serde::Serialize;
4+
5+
use crate::PublicError;
6+
7+
const STRING_HEADER_NAME: &str = "String-Header";
8+
const INTEGER_HEADER_NAME: &str = "Integer-Header";
9+
const NUMBER_HEADER_NAME: &str = "Number-Header";
10+
const BOOLEAN_HEADER_NAME: &str = "Boolean-Header";
11+
12+
pub(crate) async fn main(mut headers: HeaderMap) -> Result<Json<Data>, PublicError> {
13+
let string = match headers.remove(STRING_HEADER_NAME) {
14+
Some(value) => value
15+
.to_str()
16+
.map_err(|_| PublicError::invalid(STRING_HEADER_NAME, "Not a valid string"))?
17+
.to_string(),
18+
None => return Err(PublicError::missing(STRING_HEADER_NAME)),
19+
};
20+
let integer = match headers.remove(INTEGER_HEADER_NAME) {
21+
Some(value) => {
22+
let str_value = value.to_str().map_err(|_| {
23+
PublicError::invalid(
24+
INTEGER_HEADER_NAME,
25+
"Header needs to be a string to parse a number from.",
26+
)
27+
})?;
28+
str_value
29+
.parse::<i32>()
30+
.map_err(|_| PublicError::invalid(INTEGER_HEADER_NAME, "Not a valid integer"))?
31+
}
32+
None => return Err(PublicError::missing(INTEGER_HEADER_NAME)),
33+
};
34+
let number = match headers.remove(NUMBER_HEADER_NAME) {
35+
Some(value) => {
36+
let str_value = value.to_str().map_err(|_| {
37+
PublicError::invalid(
38+
NUMBER_HEADER_NAME,
39+
"Header must be a string to parse a number from",
40+
)
41+
})?;
42+
str_value
43+
.parse::<f64>()
44+
.map_err(|_| PublicError::invalid(NUMBER_HEADER_NAME, "Not a valid number"))?
45+
}
46+
None => return Err(PublicError::missing(NUMBER_HEADER_NAME)),
47+
};
48+
let boolean = match headers.remove(BOOLEAN_HEADER_NAME) {
49+
Some(value) => {
50+
let str_value = value.to_str().map_err(|_| {
51+
PublicError::invalid(BOOLEAN_HEADER_NAME, "Header must be a string")
52+
})?;
53+
match str_value {
54+
"true" => true,
55+
"false" => false,
56+
_ => {
57+
return Err(PublicError::invalid(
58+
BOOLEAN_HEADER_NAME,
59+
"Value must either be 'true' or 'false'",
60+
))
61+
}
62+
}
63+
}
64+
None => return Err(PublicError::missing(BOOLEAN_HEADER_NAME)),
65+
};
66+
67+
Ok(Json(Data {
68+
string,
69+
integer,
70+
number,
71+
boolean,
72+
}))
73+
}
74+
75+
#[derive(Debug, Serialize)]
76+
pub(crate) struct Data {
77+
string: String,
78+
integer: i32,
79+
number: f64,
80+
boolean: bool,
81+
}

src/main.rs

Lines changed: 32 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
#![deny(clippy::all)]
22
#![deny(clippy::pedantic)]
33
#![forbid(unsafe_code)]
4+
#![allow(clippy::unused_async)]
45

56
use std::net::SocketAddr;
67

7-
use axum::{extract::Multipart, http::StatusCode, Json, Router};
8-
use axum::extract::multipart::MultipartError;
98
use axum::response::{IntoResponse, Response};
109
use axum::routing::{get, post};
10+
use axum::{http::StatusCode, Json, Router};
1111
use serde::Serialize;
1212

13+
mod headers;
14+
mod multipart;
15+
1316
#[tokio::main]
1417
async fn main() {
15-
// initialize tracing
1618
tracing_subscriber::fmt::init();
1719

18-
let app = Router::new().route("/body/multipart", post(upload)).route("/openapi.json", get(openapi));
20+
let app = Router::new()
21+
.route("/body/multipart", post(multipart::upload))
22+
.route("/parameters/header", post(headers::main))
23+
.route("/openapi.json", get(openapi));
1924
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
2025
tracing::debug!("listening on {}", addr);
2126
axum::Server::bind(&addr)
@@ -36,153 +41,39 @@ struct PublicError {
3641
missing_parameters: Vec<String>,
3742
}
3843

39-
#[derive(Debug, Serialize)]
40-
struct Problem {
41-
parameter_name: String,
42-
description: String,
43-
}
44-
45-
impl IntoResponse for PublicError {
46-
fn into_response(self) -> Response {
47-
(StatusCode::BAD_REQUEST, Json(self)).into_response()
48-
}
49-
}
50-
51-
impl From<MultipartError> for PublicError {
52-
fn from(e: MultipartError) -> Self {
44+
impl PublicError {
45+
pub(crate) fn missing(parameter: &str) -> Self {
5346
PublicError {
54-
errors: vec![format!("Invalid multipart request: {}", e.to_string())],
55-
..Default::default()
47+
missing_parameters: vec![String::from(parameter)],
48+
..Self::default()
5649
}
5750
}
58-
}
59-
60-
async fn upload(mut multipart: Multipart) -> Result<Json<File>, PublicError> {
61-
let mut err: Option<PublicError> = None;
62-
let mut a_string = None;
63-
let mut description = None;
64-
let mut file_content_type = None;
65-
let mut file_name = None;
66-
let mut file_data = None;
67-
68-
while let Some(field) = multipart.next_field().await? {
69-
let field_name = match field.name() {
70-
Some(name) => name,
71-
None => {
72-
err.get_or_insert_with(Default::default)
73-
.extra_parameters
74-
.push(String::from("unnamed parameter"));
75-
continue;
76-
}
77-
};
7851

79-
match field_name {
80-
"a_string" => {
81-
if let Ok(value) = field.text().await {
82-
a_string = Some(value);
83-
} else {
84-
err.get_or_insert_with(Default::default)
85-
.invalid_parameters
86-
.push(Problem {
87-
parameter_name: String::from("a_string"),
88-
description: String::from("must be a string"),
89-
});
90-
}
91-
}
92-
"description" => {
93-
if let Ok(value) = field.text().await {
94-
description = Some(value);
95-
} else {
96-
err.get_or_insert_with(Default::default)
97-
.invalid_parameters
98-
.push(Problem {
99-
parameter_name: String::from("description"),
100-
description: String::from("must be a string"),
101-
});
102-
}
103-
}
104-
"file" => {
105-
if let Some(value) = field.file_name() {
106-
file_name = Some(value.to_string());
107-
} else {
108-
err.get_or_insert_with(Default::default)
109-
.invalid_parameters
110-
.push(Problem {
111-
parameter_name: String::from("file"),
112-
description: String::from("must have a file name"),
113-
});
114-
}
115-
116-
if let Some(value) = field.content_type() {
117-
file_content_type = Some(value.to_string());
118-
} else {
119-
err.get_or_insert_with(Default::default)
120-
.invalid_parameters
121-
.push(Problem {
122-
parameter_name: String::from("file"),
123-
description: String::from("must have a content type"),
124-
});
125-
}
126-
127-
if let Ok(value) = field.bytes().await {
128-
file_data = Some(value);
129-
} else {
130-
err.get_or_insert_with(Default::default)
131-
.invalid_parameters
132-
.push(Problem {
133-
parameter_name: String::from("file"),
134-
description: String::from("must have data"),
135-
});
136-
}
137-
}
138-
field_name => {
139-
err.get_or_insert_with(Default::default)
140-
.extra_parameters
141-
.push(String::from(field_name));
142-
}
52+
pub(crate) fn invalid(parameter_name: &str, description: &str) -> Self {
53+
PublicError {
54+
invalid_parameters: vec![Problem::new(parameter_name, description)],
55+
..Self::default()
14356
}
14457
}
58+
}
14559

146-
let a_string = match a_string {
147-
Some(name) => name,
148-
None => {
149-
let mut err = err.unwrap_or_default();
150-
err.missing_parameters.push(String::from("a_string"));
151-
return Err(err);
152-
}
153-
};
60+
#[derive(Debug, Serialize)]
61+
struct Problem {
62+
parameter_name: String,
63+
description: String,
64+
}
15465

155-
let (file_content_type, file_name, file_data) = match (file_content_type, file_name, file_data) {
156-
(Some(file_content_type), Some(file_name), Some(file_data)) => (
157-
file_content_type.to_string(),
158-
String::from(file_name),
159-
file_data.to_vec(),
160-
),
161-
_ => {
162-
let mut err = err.unwrap_or_default();
163-
err.missing_parameters.push(String::from("file"));
164-
return Err(err);
66+
impl Problem {
67+
pub(crate) fn new(parameter_name: &str, description: &str) -> Self {
68+
Problem {
69+
parameter_name: String::from(parameter_name),
70+
description: String::from(description),
16571
}
166-
};
167-
168-
if let Some(err) = err {
169-
Err(err)
170-
} else {
171-
Ok(Json(File {
172-
a_string,
173-
description,
174-
file_content_type,
175-
file_name,
176-
file_data: String::from_utf8_lossy(&file_data).to_string(),
177-
}))
17872
}
17973
}
18074

181-
#[derive(Debug, Serialize)]
182-
struct File {
183-
a_string: String,
184-
description: Option<String>,
185-
file_content_type: String,
186-
file_name: String,
187-
file_data: String,
75+
impl IntoResponse for PublicError {
76+
fn into_response(self) -> Response {
77+
(StatusCode::BAD_REQUEST, Json(self)).into_response()
78+
}
18879
}

0 commit comments

Comments
 (0)