mirror of https://github.com/fafhrd91/actix-web
feat: multipart testing utilities
This commit is contained in:
parent
3819767fa0
commit
7ffe1b65cc
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add testing utilities under new module `test`.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.72.
|
- Minimum supported Rust version (MSRV) is now 1.72.
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
|
@ -35,6 +35,7 @@ local-waker = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
memchr = "2.5"
|
memchr = "2.5"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
|
rand = "0.8"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_plain = "1"
|
serde_plain = "1"
|
||||||
|
@ -46,7 +47,9 @@ actix-http = "3"
|
||||||
actix-multipart-rfc7578 = "0.10"
|
actix-multipart-rfc7578 = "0.10"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1"
|
actix-test = "0.1"
|
||||||
|
actix-web = "4"
|
||||||
awc = "3"
|
awc = "3"
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||||
|
multer = "3"
|
||||||
tokio = { version = "1.24.2", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
|
@ -13,11 +13,14 @@ extern crate self as actix_multipart;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod extractor;
|
mod extractor;
|
||||||
mod server;
|
|
||||||
|
|
||||||
pub mod form;
|
pub mod form;
|
||||||
|
mod server;
|
||||||
|
pub mod test;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
error::MultipartError,
|
error::MultipartError,
|
||||||
server::{Field, Multipart},
|
server::{Field, Multipart},
|
||||||
|
test::{
|
||||||
|
create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
use actix_web::http::header::{self, HeaderMap};
|
||||||
|
use bytes::{BufMut as _, Bytes, BytesMut};
|
||||||
|
use mime::Mime;
|
||||||
|
use rand::{
|
||||||
|
distributions::{Alphanumeric, DistString as _},
|
||||||
|
thread_rng,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CRLF: &[u8] = b"\r\n";
|
||||||
|
const CRLF_CRLF: &[u8] = b"\r\n\r\n";
|
||||||
|
const HYPHENS: &[u8] = b"--";
|
||||||
|
const BOUNDARY_PREFIX: &str = "------------------------";
|
||||||
|
|
||||||
|
/// Constructs a `multipart/form-data` payload from bytes and metadata.
|
||||||
|
///
|
||||||
|
/// Returned header map can be extended or merged with existing headers.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_multipart::test::create_form_data_payload_and_headers;
|
||||||
|
/// use actix_web::test::TestRequest;
|
||||||
|
/// use bytes::Bytes;
|
||||||
|
/// use memchr::memmem::find;
|
||||||
|
///
|
||||||
|
/// let (body, headers) = create_form_data_payload_and_headers(
|
||||||
|
/// "foo",
|
||||||
|
/// Some("lorem.txt".to_owned()),
|
||||||
|
/// Some(mime::TEXT_PLAIN_UTF_8),
|
||||||
|
/// Bytes::from_static(b"Lorem ipsum."),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert!(find(&body, b"foo").is_some());
|
||||||
|
/// assert!(find(&body, b"text/plain; charset=utf-8").is_some());
|
||||||
|
/// assert!(find(&body, b"Lorem ipsum.").is_some());
|
||||||
|
///
|
||||||
|
/// let req = TestRequest::default();
|
||||||
|
///
|
||||||
|
/// // merge header map into existing test request and set multipart body
|
||||||
|
/// let req = headers
|
||||||
|
/// .into_iter()
|
||||||
|
/// .fold(req, |req, hdr| req.insert_header(hdr))
|
||||||
|
/// .set_payload(body)
|
||||||
|
/// .to_http_request();
|
||||||
|
///
|
||||||
|
/// assert!(req.headers().contains_key("content-type"));
|
||||||
|
/// ```
|
||||||
|
pub fn create_form_data_payload_and_headers(
|
||||||
|
name: &str,
|
||||||
|
filename: Option<String>,
|
||||||
|
content_type: Option<Mime>,
|
||||||
|
file: Bytes,
|
||||||
|
) -> (Bytes, HeaderMap) {
|
||||||
|
let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32);
|
||||||
|
|
||||||
|
create_form_data_payload_and_headers_with_boundary(
|
||||||
|
&boundary,
|
||||||
|
name,
|
||||||
|
filename,
|
||||||
|
content_type,
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a `multipart/form-data` payload from bytes and metadata with a fixed boundary.
|
||||||
|
///
|
||||||
|
/// See [`create_form_data_payload_and_headers`] for more details.
|
||||||
|
pub fn create_form_data_payload_and_headers_with_boundary(
|
||||||
|
boundary: &str,
|
||||||
|
name: &str,
|
||||||
|
filename: Option<String>,
|
||||||
|
content_type: Option<Mime>,
|
||||||
|
file: Bytes,
|
||||||
|
) -> (Bytes, HeaderMap) {
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
|
||||||
|
let boundary_str = [BOUNDARY_PREFIX, boundary].concat();
|
||||||
|
let boundary = boundary_str.as_bytes();
|
||||||
|
|
||||||
|
buf.put(HYPHENS);
|
||||||
|
buf.put(boundary);
|
||||||
|
buf.put(CRLF);
|
||||||
|
|
||||||
|
buf.put(format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes());
|
||||||
|
if let Some(filename) = filename {
|
||||||
|
buf.put(format!("; filename=\"{filename}\"").as_bytes());
|
||||||
|
}
|
||||||
|
buf.put(CRLF);
|
||||||
|
|
||||||
|
if let Some(ct) = content_type {
|
||||||
|
buf.put(format!("Content-Type: {ct}").as_bytes());
|
||||||
|
buf.put(CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.put(format!("Content-Length: {}", file.len()).as_bytes());
|
||||||
|
buf.put(CRLF_CRLF);
|
||||||
|
|
||||||
|
buf.put(file);
|
||||||
|
buf.put(CRLF);
|
||||||
|
|
||||||
|
buf.put(HYPHENS);
|
||||||
|
buf.put(boundary);
|
||||||
|
buf.put(HYPHENS);
|
||||||
|
buf.put(CRLF);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
format!("multipart/form-data; boundary=\"{boundary_str}\"")
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(buf.freeze(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use futures_util::stream;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn find_boundary(headers: &HeaderMap) -> String {
|
||||||
|
headers
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.parse::<mime::Mime>()
|
||||||
|
.unwrap()
|
||||||
|
.get_param("boundary")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wire_format() {
|
||||||
|
let (pl, headers) = create_form_data_payload_and_headers_with_boundary(
|
||||||
|
"qWeRtYuIoP",
|
||||||
|
"foo",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
find_boundary(&headers),
|
||||||
|
"------------------------qWeRtYuIoP",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
std::str::from_utf8(&pl).unwrap(),
|
||||||
|
"--------------------------qWeRtYuIoP\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"foo\"\r\n\
|
||||||
|
Content-Length: 26\r\n\
|
||||||
|
\r\n\
|
||||||
|
Lorem ipsum dolor\n\
|
||||||
|
sit ame.\r\n\
|
||||||
|
--------------------------qWeRtYuIoP--\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let (pl, _headers) = create_form_data_payload_and_headers_with_boundary(
|
||||||
|
"qWeRtYuIoP",
|
||||||
|
"foo",
|
||||||
|
Some("Lorem.txt".to_owned()),
|
||||||
|
Some(mime::TEXT_PLAIN_UTF_8),
|
||||||
|
Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
std::str::from_utf8(&pl).unwrap(),
|
||||||
|
"--------------------------qWeRtYuIoP\r\n\
|
||||||
|
Content-Disposition: form-data; name=\"foo\"; filename=\"Lorem.txt\"\r\n\
|
||||||
|
Content-Type: text/plain; charset=utf-8\r\n\
|
||||||
|
Content-Length: 26\r\n\
|
||||||
|
\r\n\
|
||||||
|
Lorem ipsum dolor\n\
|
||||||
|
sit ame.\r\n\
|
||||||
|
--------------------------qWeRtYuIoP--\r\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn ecosystem_compat() {
|
||||||
|
let (pl, headers) = create_form_data_payload_and_headers(
|
||||||
|
"foo",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
|
||||||
|
);
|
||||||
|
|
||||||
|
let boundary = find_boundary(&headers);
|
||||||
|
|
||||||
|
let pl = stream::once(async { Ok::<_, Infallible>(pl) });
|
||||||
|
|
||||||
|
let mut form = multer::Multipart::new(pl, boundary);
|
||||||
|
let field = form.next_field().await.unwrap().unwrap();
|
||||||
|
assert_eq!(field.name().unwrap(), "foo");
|
||||||
|
assert_eq!(field.file_name(), None);
|
||||||
|
assert_eq!(field.content_type(), None);
|
||||||
|
assert!(field.bytes().await.unwrap().starts_with(b"Lorem"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue