Skip to content

Instantly share code, notes, and snippets.

@thebino
Last active October 1, 2023 05:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thebino/1a49b31bbe2400b957b3c261fa7edf12 to your computer and use it in GitHub Desktop.
Save thebino/1a49b31bbe2400b957b3c261fa7edf12 to your computer and use it in GitHub Desktop.
[Rust] Testing multipart/form-data with axum
[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"] }
//! 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