actix_multipart_test/
lib.rs

1use std::path::Path;
2
3use uuid::Uuid;
4
5/// Simple builder for multipart/form-data test
6///
7/// # Examples
8///
9/// ```
10/// #[cfg(test)]
11/// mod tests {
12///     use actix_multipart_test::MultiPartFormDataBuilder;
13///     use actix_web::{test, App};
14///     use super::*;
15///
16///     #[actix_web::test]
17///     async fn test_should_upload_file() {
18///
19///         let mut app =
20///             test::init_service(
21///                     App::new()
22///                     .service(yourmultipartformhandler)
23///                 )
24///                 .await;
25///
26///         let mut multipart_form_data_builder = MultiPartFormDataBuilder::new();
27///         multipart_form_data_builder.with_file("tests/sample.png", "sample", "image/png", "sample.png");
28///         multipart_form_data_builder.with_text("name", "some_name");
29///         let (header, body) = multipart_form_data_builder.build();
30///
31///
32///         let req = test::TestRequest::post()
33///             .uri("/someurl")
34///             .insert_header(header)
35///             .set_payload(body)
36///             .to_request();
37///         let resp = test::call_service(&mut app, req).await;
38///
39///         assert!(resp.status().is_success());
40///
41///     }
42/// }
43/// ```
44pub struct MultiPartFormDataBuilder {
45    files: Vec<(String, String, String, Box<dyn AsRef<Path>>)>,
46    texts: Vec<(String, String, String)>,
47}
48
49impl MultiPartFormDataBuilder {
50    /// Create new MultiPartFormDataBuilder
51    pub fn new() -> MultiPartFormDataBuilder {
52        MultiPartFormDataBuilder {
53            files: vec![],
54            texts: vec![],
55        }
56    }
57
58    /// Add text to multipart/form-data
59    ///
60    /// name is form name
61    ///
62    /// value is form value
63    ///
64    /// Returns &mut MultiPartFormDataBuilder
65    pub fn with_text(
66        &mut self,
67        name: impl Into<String>,
68        value: impl Into<String>,
69    ) -> &mut MultiPartFormDataBuilder {
70        self.texts
71            .push((name.into(), value.into(), "text/plain".to_string()));
72        self
73    }
74
75    /// Add file to multipart/form-data
76    ///
77    /// path is file path
78    ///
79    /// name is form name
80    ///
81    /// content_type is file content type
82    ///
83    /// file_name is file name
84    pub fn with_file(
85        &mut self,
86        path: impl AsRef<Path> + 'static,
87        name: impl Into<String>,
88        content_type: impl Into<String>,
89        file_name: impl Into<String>,
90    ) -> &mut MultiPartFormDataBuilder {
91        self.files.push((
92            name.into(),
93            file_name.into(),
94            content_type.into(),
95            Box::new(path),
96        ));
97        self
98    }
99
100    /// Build multipart/form-data
101    ///
102    /// Returns ((header_name, header_value), body)
103    ///
104    /// header_name is "Content-Type"
105    ///
106    /// header_value is "multipart/form-data; boundary=..."
107    ///
108    /// body is binary data
109    pub fn build(&self) -> ((String, String), Vec<u8>) {
110        let boundary = Uuid::new_v4().to_string();
111
112        let mut body = vec![];
113
114        for file in self.files.iter() {
115            body.extend(format!("--{}\r\n", boundary).as_bytes());
116            body.extend(
117                format!(
118                    "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n",
119                    file.0, file.1
120                )
121                .as_bytes(),
122            );
123            body.extend(format!("Content-Type: {}\r\n", file.2).as_bytes());
124            let data = std::fs::read(file.3.as_ref()).unwrap();
125            body.extend(format!("Content-Length: {}\r\n\r\n", data.len()).as_bytes());
126            body.extend(data);
127            body.extend("\r\n".as_bytes());
128        }
129
130        for text in self.texts.iter() {
131            body.extend(format!("--{}\r\n", boundary).as_bytes());
132            body.extend(
133                format!("Content-Disposition: form-data; name=\"{}\"\r\n", text.0).as_bytes(),
134            );
135            body.extend(format!("Content-Type: {}\r\n", text.2).as_bytes());
136            let data = text.1.as_bytes();
137            body.extend(format!("Content-Length: {}\r\n\r\n", data.len()).as_bytes());
138            body.extend(data);
139            body.extend("\r\n".as_bytes());
140        }
141
142        body.extend(format!("--{}--\r\n", boundary).as_bytes());
143
144        let header_value = format!("multipart/form-data; boundary={}", boundary);
145        let header = ("Content-Type".to_string(), header_value);
146
147        (header, body)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_should_build_multipart_form_with_text() {
157        let mut multipart_form_data_builder = MultiPartFormDataBuilder::new();
158        multipart_form_data_builder.with_file(
159            "tests/sample.png",
160            "sample",
161            "image/png",
162            "sample.png",
163        );
164        multipart_form_data_builder.with_text("name", "some_name");
165        let (header, body) = multipart_form_data_builder.build();
166
167        assert_eq!(header.0, "Content-Type");
168        assert!(header.1.starts_with("multipart/form-data; boundary="));
169        assert!(body.len() > 0);
170    }
171}