Last active
October 1, 2023 05:33
-
-
Save thebino/1a49b31bbe2400b957b3c261fa7edf12 to your computer and use it in GitHub Desktop.
[Rust] Testing multipart/form-data with axum
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "test-multipart" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
axum = { version = "0.6.20", features = ["multipart"] } | |
tokio = { version = "1.32.0", features = ["full"] } | |
mime = "0.3.17" | |
log = "0.4.20" | |
[dev-dependencies] | |
hyper = { version = "0.14.27", features = ["full"] } | |
tower = { version = "0.4.13", features = ["util"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! Handle a simple multipart/form-data request with two text input fields | |
//! | |
//! <form action="http://localhost:3000" method="post" enctype="multipart/form-data"> | |
//! <p><input type="text" name="foo" value="text1"> | |
//! <p><input type="text" name="bar" value="text2"> | |
//! <p><button type="submit">Submit</button> | |
//! </form> | |
use axum::extract::Multipart; | |
use axum::http::StatusCode; | |
use axum::routing::{get, post}; | |
use axum::Router; | |
use log::debug; | |
async fn get_foo() -> Result<String, StatusCode> { | |
Ok("foo".to_string()) | |
} | |
async fn post_foo(mut multipart: Multipart) -> Result<String, StatusCode> { | |
let mut foo = None; | |
let mut bar = None; | |
while let Some(field) = multipart.next_field().await.unwrap() { | |
if let Some(field_name) = field.name() { | |
match field_name { | |
"foo" => { | |
foo = Some(field.text().await.unwrap()); | |
debug!("foo={}", foo.clone().unwrap()); | |
} | |
"bar" => { | |
bar = Some(field.text().await.unwrap()); | |
debug!("bar={}", bar.clone().unwrap()); | |
} | |
_ => continue, | |
} | |
} | |
} | |
// early return on missing input | |
if foo.is_none() || bar.is_none() { | |
return Err(StatusCode::BAD_REQUEST); | |
} | |
Ok("Placeholder".to_string()) | |
} | |
pub async fn routes<S>() -> Router<S> | |
where | |
S: Send + Sync + Clone + 'static, | |
{ | |
Router::new() | |
.route("/", get(get_foo)) | |
.route("/", post(post_foo)) | |
} | |
#[tokio::main] | |
async fn main() -> Result<(), Box<dyn std::error::Error>> { | |
let app = Router::new().nest("/", routes().await); | |
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) | |
.serve(app.into_make_service()) | |
.await | |
.unwrap(); | |
Ok(()) | |
} | |
#[cfg(test)] | |
mod tests { | |
use std::io; | |
use std::io::Write; | |
use axum::http::header::CONTENT_TYPE; | |
use axum::http::{Request, StatusCode}; | |
use axum::Router; | |
use hyper::Body; | |
use mime::BOUNDARY; | |
use tower::ServiceExt; | |
use super::*; | |
#[tokio::test] | |
async fn get_foo() { | |
// given | |
let router = Router::new().nest("/", routes().await); | |
// when | |
let response = router | |
.oneshot( | |
Request::builder() | |
.method("GET") | |
.uri("/") | |
.body(Body::empty()) | |
.unwrap(), | |
) | |
.await | |
.unwrap(); | |
// then | |
assert_eq!(response.status(), StatusCode::OK); | |
} | |
#[tokio::test] | |
async fn post_multipart_success() { | |
// given | |
let router = Router::new().nest("/", routes().await); | |
let data = media_item_form_data_full().unwrap(); | |
// when | |
let response = router | |
.oneshot( | |
Request::builder() | |
.method("POST") | |
.uri("/") | |
.header( | |
CONTENT_TYPE, | |
format!("multipart/form-data; boundary={}", BOUNDARY), | |
) | |
.body(data.into()) | |
.unwrap(), | |
) | |
.await | |
.unwrap(); | |
// then | |
assert_eq!(response.status(), StatusCode::OK); | |
} | |
#[tokio::test] | |
async fn post_multipart_fail_without_first_field() { | |
// given | |
let router = Router::new().nest("/", routes().await); | |
let data = media_item_form_data_first_field_missing().unwrap(); | |
// when | |
let response = router | |
.oneshot( | |
Request::builder() | |
.method("POST") | |
.uri("/") | |
.header( | |
CONTENT_TYPE, | |
format!("multipart/form-data; boundary={}", BOUNDARY), | |
) | |
.body(data.into()) | |
.unwrap(), | |
) | |
.await | |
.unwrap(); | |
// then | |
assert_eq!(response.status(), StatusCode::BAD_REQUEST); | |
} | |
fn media_item_form_data_full() -> io::Result<Vec<u8>> { | |
let mut data: Vec<u8> = Vec::new(); | |
write!(data, "--{}\r\n", BOUNDARY)?; | |
write!(data, "Content-Disposition: form-data; name=\"foo\";\r\n")?; | |
write!(data, "\r\n")?; | |
write!(data, "text1")?; | |
write!(data, "\r\n")?; | |
write!(data, "--{}\r\n", BOUNDARY)?; | |
write!(data, "Content-Disposition: form-data; name=\"bar\";\r\n")?; | |
write!(data, "\r\n")?; | |
write!(data, "test2")?; | |
write!(data, "\r\n")?; | |
write!(data, "--{}--\r\n", BOUNDARY)?; | |
Ok(data) | |
} | |
fn media_item_form_data_first_field_missing() -> io::Result<Vec<u8>> { | |
let mut data: Vec<u8> = Vec::new(); | |
write!(data, "--{}\r\n", BOUNDARY)?; | |
write!(data, "Content-Disposition: form-data; name=\"bar\";\r\n")?; | |
write!(data, "\r\n")?; | |
write!(data, "test2")?; | |
write!(data, "\r\n")?; | |
write!(data, "--{}--\r\n", BOUNDARY)?; | |
Ok(data) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment