wiremock_multipart/matchers/
contains_part.rs

1use std::borrow::Cow;
2
3use wiremock::{Match, Request};
4
5use crate::request_utils::RequestUtils;
6
7/// Matcher builder to assert the presence of a matching part in the request.
8///
9/// ## Example
10///
11/// ```rust
12/// use wiremock::{MockServer, Mock, ResponseTemplate};
13/// use wiremock::matchers::method;
14/// use wiremock_multipart::prelude::*;
15///
16/// #[async_std::main]
17/// async fn main() {
18///     // Start a background HTTP server on a random local port
19///     let mock_server = MockServer::start().await;
20///
21///     Mock::given(method("POST"))
22///         .and(ContainsPart::new()
23///             .with_name("data-part")
24///             .with_content_type("text/plain")
25///             .with_body("simple text".as_bytes()))
26///         .respond_with(ResponseTemplate::new(200))
27///         .mount(&mock_server)
28///         .await;
29/// }
30/// ```
31#[derive(Default, Debug, PartialEq, Eq)]
32pub struct ContainsPart<'a, 'b, 'c, 'd> {
33    pub name: Option<Cow<'a, str>>,
34    pub filename: Option<Cow<'b, str>>,
35    pub content_type: Option<Cow<'c, str>>,
36    pub body: Option<Cow<'d, [u8]>>,
37}
38
39impl<'a, 'b, 'c, 'd> ContainsPart<'a, 'b, 'c, 'd> {
40    pub fn new() -> Self { Self::default() }
41
42    pub fn with_name<T: Into<Cow<'a, str>>>(self, name: T) -> Self {
43        ContainsPart {
44            name: Some(name.into()),
45            ..self
46        }
47    }
48
49    pub fn with_filename<T: Into<Cow<'b, str>>>(self, filename: T) -> Self {
50        ContainsPart {
51            filename: Some(filename.into()),
52            ..self
53        }
54    }
55
56    pub fn with_content_type<T: Into<Cow<'c, str>>>(self, content_type: T) -> Self {
57        ContainsPart {
58            content_type: Some(content_type.into()),
59            ..self
60        }
61    }
62
63    pub fn with_body<T: Into<Cow<'d, [u8]>>>(self, body: T) -> Self {
64        ContainsPart {
65            body: Some(body.into()),
66            ..self
67        }
68    }
69}
70
71impl<'a, 'b, 'c, 'd> Match for ContainsPart<'a, 'b, 'c, 'd> {
72    fn matches(&self, request: &Request) -> bool {
73        request.parts().iter()
74            .any(|part| {
75                let name = self.name.as_ref()
76                    .map(|required_name| {
77                        part.name()
78                            .map(|part_name| required_name == part_name)
79                            .unwrap_or(false)
80                    })
81                    .unwrap_or(true);
82
83                let filename = self.filename.as_ref()
84                    .map(|required_filename| {
85                        part.filename()
86                            .map(|part_filename| required_filename == part_filename)
87                            .unwrap_or(false)
88                    })
89                    .unwrap_or(true);
90
91                let content_type = self.content_type.as_ref()
92                    .map(|required_content_type| {
93                        part.content_type()
94                            .map(|part_content_type| required_content_type == part_content_type)
95                            .unwrap_or(false)
96                    })
97                    .unwrap_or(true);
98
99                let body = self.body.as_ref()
100                    .map(|required_body| {
101                        part.body()
102                            .map(|part_body| required_body.as_ref() == part_body)
103                            .unwrap_or(false)
104                    })
105                    .unwrap_or(true);
106
107                name && filename && content_type && body
108            })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use indoc::indoc;
115    use maplit::hashmap;
116
117    use crate::test_utils::{multipart_header, name, requestb, values};
118
119    use super::*;
120
121    #[test]
122    fn default_should_be_all_none() {
123        assert_eq!(
124            ContainsPart::default(),
125            ContainsPart { name: None, filename: None, content_type: None, body: None }
126        );
127    }
128
129    #[test]
130    fn new_should_be_default() {
131        assert_eq!(
132            ContainsPart::default(),
133            ContainsPart::new(),
134        );
135    }
136
137    #[test]
138    fn should_add_name() {
139        assert_eq!(
140            ContainsPart::new().with_name("name"),
141            ContainsPart {
142                name: Some(Cow::Borrowed("name")),
143                ..Default::default()
144            }
145        );
146    }
147
148    #[test]
149    fn should_add_filename() {
150        assert_eq!(
151            ContainsPart::new().with_filename("filename"),
152            ContainsPart {
153                filename: Some(Cow::Borrowed("filename")),
154                ..Default::default()
155            }
156        );
157    }
158
159    #[test]
160    fn should_add_content_type() {
161        assert_eq!(
162            ContainsPart::new().with_content_type("application/json"),
163            ContainsPart {
164                content_type: Some(Cow::Borrowed("application/json")),
165                ..Default::default()
166            }
167        );
168    }
169
170    #[test]
171    fn should_add_body() {
172        assert_eq!(
173            ContainsPart::new().with_body("the body".as_bytes()),
174            ContainsPart {
175                body: Some(Cow::Borrowed("the body".as_bytes())),
176                ..Default::default()
177            }
178        );
179    }
180
181    #[test]
182    fn empty_should_match_any() {
183        assert_eq!(
184            ContainsPart::new().matches(
185                &requestb(
186                    multipart_header(),
187                    indoc!{r#"
188                    --xyz
189                    Content-Disposition: form-data; name="part"
190
191                    content
192                    --xyz--
193                "#}.as_bytes().into()
194                ),
195            ),
196            true
197        );
198    }
199
200    #[test]
201    fn empty_should_not_match_request_without_parts() {
202        assert_eq!(
203            ContainsPart::new().matches(
204                &requestb(
205                    hashmap!{
206                        name("content-type") => values("text/plain"),
207                    },
208                    "not a multipart request".as_bytes().into(),
209                ),
210            ),
211            false
212        );
213    }
214
215    #[test]
216    fn should_match_on_name() {
217        assert_eq!(
218            ContainsPart::new().with_name("part-a").matches(
219                &requestb(
220                    multipart_header(),
221                    indoc!{r#"
222                    --xyz
223                    Content-Disposition: form-data; name="not-the-part"
224
225                    content
226                    --xyz--
227                "#}.as_bytes().into()
228                ),
229            ),
230            false
231        );
232
233        assert_eq!(
234            ContainsPart::new().with_name("part-a").matches(
235                &requestb(
236                    multipart_header(),
237                    indoc!{r#"
238                    --xyz
239                    Content-Disposition: form-data; name="part-a"
240
241                    content
242                    --xyz--
243                "#}.as_bytes().into()
244                ),
245            ),
246            true
247        );
248    }
249
250    #[test]
251    fn should_match_on_filename() {
252        assert_eq!(
253            ContainsPart::new().with_filename("file-a").matches(
254                &requestb(
255                    multipart_header(),
256                    indoc!{r#"
257                    --xyz
258                    Content-Disposition: form-data; name="not-the-part"; filename="not-the-file"
259
260                    content
261                    --xyz--
262                "#}.as_bytes().into()
263                ),
264            ),
265            false
266        );
267
268        assert_eq!(
269            ContainsPart::new().with_filename("file-a").matches(
270                &requestb(
271                    multipart_header(),
272                    indoc!{r#"
273                    --xyz
274                    Content-Disposition: form-data; name="part-a"; filename="file-a"
275
276                    content
277                    --xyz--
278                "#}.as_bytes().into()
279                ),
280            ),
281            true
282        );
283    }
284
285    #[test]
286    fn should_match_on_content_type() {
287        assert_eq!(
288            ContainsPart::new().with_content_type("application/json").matches(
289                &requestb(
290                    multipart_header(),
291                    indoc!{r#"
292                    --xyz
293                    Content-Disposition: form-data; name="not-the-part" filename="not-the-file"
294                    Content-Type: application/xml
295
296                    content
297                    --xyz--
298                "#}.as_bytes().into()
299                ),
300            ),
301            false
302        );
303
304        assert_eq!(
305            ContainsPart::new().with_content_type("application/json").matches(
306                &requestb(
307                    multipart_header(),
308                    indoc!{r#"
309                    --xyz
310                    Content-Disposition: form-data; name="part-a" filename="file-a"
311                    Content-Type: application/json
312
313                    content
314                    --xyz--
315                "#}.as_bytes().into()
316                ),
317            ),
318            true
319        );
320    }
321
322    #[test]
323    fn should_match_on_body() {
324        assert_eq!(
325            ContainsPart::new().with_body("content".as_bytes()).matches(
326                &requestb(
327                    multipart_header(),
328                    indoc!{r#"
329                    --xyz
330                    Content-Disposition: form-data; name="not-the-part" filename="not-the-file"
331                    Content-Type: application/xml
332
333                    not the right content
334                    --xyz--
335                "#}.as_bytes().into()
336                ),
337            ),
338            false
339        );
340
341        assert_eq!(
342            ContainsPart::new().with_body("content".as_bytes()).matches(
343                &requestb(
344                    multipart_header(),
345                    indoc!{r#"
346                    --xyz
347                    Content-Disposition: form-data; name="part-a" filename="file-a"
348                    Content-Type: application/json
349
350                    content
351                    --xyz--
352                "#}.as_bytes().into()
353                ),
354            ),
355            true
356        );
357    }
358
359}