mirror of
https://github.com/actix/actix-web.git
synced 2024-11-25 11:01:14 +00:00
feat: multipart testing utilities (#3288)
This commit is contained in:
parent
3819767fa0
commit
82f8ddc38f
6 changed files with 287 additions and 24 deletions
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Add testing utilities under new module `test`.
|
||||
- Minimum supported Rust version (MSRV) is now 1.72.
|
||||
|
||||
## 0.6.1
|
||||
|
|
|
@ -35,6 +35,7 @@ local-waker = "0.1"
|
|||
log = "0.4"
|
||||
memchr = "2.5"
|
||||
mime = "0.3"
|
||||
rand = "0.8"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
serde_plain = "1"
|
||||
|
@ -46,7 +47,9 @@ actix-http = "3"
|
|||
actix-multipart-rfc7578 = "0.10"
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1"
|
||||
actix-web = "4"
|
||||
awc = "3"
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
multer = "3"
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
|
|
@ -131,14 +131,13 @@ impl Default for JsonConfig {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_multipart_rfc7578::client::multipart;
|
||||
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::form::{
|
||||
json::{Json, JsonConfig},
|
||||
tests::send_form,
|
||||
MultipartForm,
|
||||
};
|
||||
|
||||
|
@ -155,6 +154,8 @@ mod tests {
|
|||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
const TEST_JSON: &str = r#"{"key1": "value1", "key2": "value2"}"#;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_json_without_content_type() {
|
||||
let srv = actix_test::start(|| {
|
||||
|
@ -163,10 +164,16 @@ mod tests {
|
|||
.app_data(JsonConfig::default().validate_content_type(false))
|
||||
});
|
||||
|
||||
let mut form = multipart::Form::default();
|
||||
form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
|
||||
let response = send_form(&srv, form, "/").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let (body, headers) = crate::test::create_form_data_payload_and_headers(
|
||||
"json",
|
||||
None,
|
||||
None,
|
||||
Bytes::from_static(TEST_JSON.as_bytes()),
|
||||
);
|
||||
let mut req = srv.post("/");
|
||||
*req.headers_mut() = headers;
|
||||
let res = req.send_body(body).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -178,17 +185,27 @@ mod tests {
|
|||
});
|
||||
|
||||
// Deny because wrong content type
|
||||
let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
|
||||
let mut form = multipart::Form::default();
|
||||
form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_OCTET_STREAM);
|
||||
let response = send_form(&srv, form, "/").await;
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let (body, headers) = crate::test::create_form_data_payload_and_headers(
|
||||
"json",
|
||||
None,
|
||||
Some(mime::APPLICATION_OCTET_STREAM),
|
||||
Bytes::from_static(TEST_JSON.as_bytes()),
|
||||
);
|
||||
let mut req = srv.post("/");
|
||||
*req.headers_mut() = headers;
|
||||
let res = req.send_body(body).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// Allow because correct content type
|
||||
let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
|
||||
let mut form = multipart::Form::default();
|
||||
form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_JSON);
|
||||
let response = send_form(&srv, form, "/").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let (body, headers) = crate::test::create_form_data_payload_and_headers(
|
||||
"json",
|
||||
None,
|
||||
Some(mime::APPLICATION_JSON),
|
||||
Bytes::from_static(TEST_JSON.as_bytes()),
|
||||
);
|
||||
let mut req = srv.post("/");
|
||||
*req.headers_mut() = headers;
|
||||
let res = req.send_body(body).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,14 @@ extern crate self as actix_multipart;
|
|||
|
||||
mod error;
|
||||
mod extractor;
|
||||
mod server;
|
||||
|
||||
pub mod form;
|
||||
mod server;
|
||||
pub mod test;
|
||||
|
||||
pub use self::{
|
||||
error::MultipartError,
|
||||
server::{Field, Multipart},
|
||||
test::{
|
||||
create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -863,13 +863,15 @@ mod tests {
|
|||
test::TestRequest,
|
||||
FromRequest,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use bytes::{BufMut as _, Bytes};
|
||||
use futures_util::{future::lazy, StreamExt as _};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use super::*;
|
||||
|
||||
const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_boundary() {
|
||||
let headers = HeaderMap::new();
|
||||
|
@ -965,6 +967,26 @@ mod tests {
|
|||
}
|
||||
|
||||
fn create_simple_request_with_header() -> (Bytes, HeaderMap) {
|
||||
let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary(
|
||||
BOUNDARY,
|
||||
"file",
|
||||
Some("fn.txt".to_owned()),
|
||||
Some(mime::TEXT_PLAIN_UTF_8),
|
||||
Bytes::from_static(b"data"),
|
||||
);
|
||||
|
||||
let mut buf = BytesMut::with_capacity(body.len() + 14);
|
||||
|
||||
// add junk before form to test pre-boundary data rejection
|
||||
buf.put("testasdadsad\r\n".as_bytes());
|
||||
|
||||
buf.put(body);
|
||||
|
||||
(buf.freeze(), headers)
|
||||
}
|
||||
|
||||
// TODO: use test utility when multi-file support is introduced
|
||||
fn create_double_request_with_header() -> (Bytes, HeaderMap) {
|
||||
let bytes = Bytes::from(
|
||||
"testasdadsad\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
||||
|
@ -990,7 +1012,7 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_multipart_no_end_crlf() {
|
||||
let (sender, payload) = create_stream();
|
||||
let (mut bytes, headers) = create_simple_request_with_header();
|
||||
let (mut bytes, headers) = create_double_request_with_header();
|
||||
let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf
|
||||
|
||||
sender.send(Ok(bytes_stripped)).unwrap();
|
||||
|
@ -1017,7 +1039,7 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_multipart() {
|
||||
let (sender, payload) = create_stream();
|
||||
let (bytes, headers) = create_simple_request_with_header();
|
||||
let (bytes, headers) = create_double_request_with_header();
|
||||
|
||||
sender.send(Ok(bytes)).unwrap();
|
||||
|
||||
|
@ -1080,7 +1102,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_stream() {
|
||||
let (bytes, headers) = create_simple_request_with_header();
|
||||
let (bytes, headers) = create_double_request_with_header();
|
||||
let payload = SlowStream::new(bytes);
|
||||
|
||||
let mut multipart = Multipart::new(&headers, payload);
|
||||
|
@ -1319,7 +1341,7 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_drop_field_awaken_multipart() {
|
||||
let (sender, payload) = create_stream();
|
||||
let (bytes, headers) = create_simple_request_with_header();
|
||||
let (bytes, headers) = create_double_request_with_header();
|
||||
sender.send(Ok(bytes)).unwrap();
|
||||
drop(sender); // eof
|
||||
|
||||
|
|
217
actix-multipart/src/test.rs
Normal file
217
actix-multipart/src/test.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
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.
|
||||
///
|
||||
/// Multipart boundary used is a random alphanumeric string.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// 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"lorem.txt").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()
|
||||
/// .get("content-type")
|
||||
/// .unwrap()
|
||||
/// .to_str()
|
||||
/// .unwrap()
|
||||
/// .starts_with("multipart/form-data; boundary=\"")
|
||||
/// );
|
||||
/// ```
|
||||
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::with_capacity(file.len() + 128);
|
||||
|
||||
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(mime::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",
|
||||
);
|
||||
}
|
||||
|
||||
/// Test using an external library to prevent the two-wrongs-make-a-right class of errors.
|
||||
#[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 a new issue