1use std::path::Path;
4
5use anyhow::{anyhow, bail};
6use bytes::{Bytes, BytesMut};
7use either::Either;
8use lazy_static::lazy_static;
9use multipart_2021 as multipart;
10use regex::Regex;
11use serde_json::{Map, Value};
12use tracing::{debug, error, trace};
13
14use pact_models::bodies::OptionalBody;
15use pact_models::content_types::ContentTypeHint;
16use pact_models::generators::{Generator, GeneratorCategory, Generators};
17use pact_models::json_utils::json_to_string;
18use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory, RuleLogic};
19use pact_models::matchingrules::expressions::{is_matcher_def, parse_matcher_def};
20use pact_models::path_exp::DocPath;
21use pact_models::v4::http_parts::{HttpRequest, HttpResponse};
22
23use crate::mock_server::generator_category;
24
25const CONTENT_TYPE_HEADER: &str = "Content-Type";
26
27lazy_static! {
28 static ref MULTIPART_MARKER: Regex = Regex::new("\\-\\-([a-zA-Z0-9'\\(\\)+_,-.\\/:=? ]*)\r\n").unwrap();
29}
30
31pub fn process_array(
33 array: &[Value],
34 matching_rules: &mut MatchingRuleCategory,
35 generators: &mut Generators,
36 path: DocPath,
37 type_matcher: bool,
38 skip_matchers: bool
39) -> Value {
40 trace!(">>> process_array(array={array:?}, matching_rules={matching_rules:?}, generators={generators:?}, path={path}, type_matcher={type_matcher}, skip_matchers={skip_matchers})");
41 debug!("Path = {path}");
42 Value::Array(array.iter().enumerate().map(|(index, val)| {
43 let mut item_path = path.clone();
44 if type_matcher {
45 item_path.push_star_index();
46 } else {
47 item_path.push_index(index);
48 }
49 match val {
50 Value::Object(map) => process_object(map, matching_rules, generators, item_path, skip_matchers),
51 Value::Array(array) => process_array(array.as_slice(), matching_rules, generators, item_path, false, skip_matchers),
52 _ => val.clone()
53 }
54 }).collect())
55}
56
57pub fn process_object(
59 obj: &Map<String, Value>,
60 matching_rules: &mut MatchingRuleCategory,
61 generators: &mut Generators,
62 path: DocPath,
63 type_matcher: bool
64) -> Value {
65 trace!(">>> process_object(obj={obj:?}, matching_rules={matching_rules:?}, generators={generators:?}, path={path}, type_matcher={type_matcher})");
66 debug!("Path = {path}");
67 let result = if let Some(matcher_type) = obj.get("pact:matcher:type") {
68 debug!("detected pact:matcher:type, will configure a matcher");
69 process_matcher(obj, matching_rules, generators, &path, type_matcher, &matcher_type.clone())
70 } else {
71 debug!("Configuring a normal object");
72 Value::Object(obj.iter()
73 .filter(|(key, _)| !key.starts_with("pact:"))
74 .map(|(key, val)| {
75 let item_path = if type_matcher {
76 path.join("*")
77 } else {
78 path.join(key)
79 };
80 (key.clone(), match val {
81 Value::Object(ref map) => process_object(map, matching_rules, generators, item_path, false),
82 Value::Array(ref array) => process_array(array, matching_rules, generators, item_path, false, false),
83 _ => val.clone()
84 })
85 }).collect())
86 };
87 trace!("-> result = {result:?}");
88 result
89}
90
91fn process_matcher(
93 obj: &Map<String, Value>,
94 matching_rules: &mut MatchingRuleCategory,
95 generators: &mut Generators,
96 path: &DocPath,
97 skip_matchers: bool,
98 matcher_type: &Value
99) -> Value {
100 let is_array_contains = match matcher_type {
101 Value::String(s) => s == "arrayContains" || s == "array-contains",
102 _ => false
103 };
104
105 let matching_rule_result = if is_array_contains {
106 match obj.get("variants") {
107 Some(Value::Array(variants)) => {
108 let mut json_values = vec![];
109
110 let values = variants.iter().enumerate().map(|(index, variant)| {
111 let mut category = MatchingRuleCategory::empty("body");
112 let mut generators = Generators::default();
113 let value = match variant {
114 Value::Object(map) => {
115 process_object(map, &mut category, &mut generators, DocPath::root(), false)
116 }
117 Value::Array(arr) => {
118 process_array(arr, &mut category, &mut generators, DocPath::root(), false, false)
119 }
120 _ => {
121 variant.clone()
122 }
123 };
124 json_values.push(value);
125 (index, category, generators.categories.get(&GeneratorCategory::BODY).cloned().unwrap_or_default())
126 }).collect();
127
128 Ok((vec!(MatchingRule::ArrayContains(values)), Value::Array(json_values)))
129 }
130 _ => Err(anyhow!("ArrayContains 'variants' attribute is missing or not an array"))
131 }
132 } else {
133 matchers_from_integration_json(obj).map(|(rules, generator)| {
134 let has_values_matcher = rules.iter().any(MatchingRule::is_values_matcher);
135
136 let json_value = match obj.get("value") {
137 Some(inner) => match inner {
138 Value::Object(ref map) => process_object(map, matching_rules, generators, path.clone(), has_values_matcher),
139 Value::Array(ref array) => process_array(array, matching_rules, generators, path.clone(), true, skip_matchers),
140 _ => inner.clone()
141 },
142 None => Value::Null
143 };
144
145 if let Some(generator) = generator {
146 let category = generator_category(matching_rules);
147 generators.add_generator_with_subcategory(category, path.clone(), generator);
148 }
149
150 (rules, json_value)
151 })
152 };
153
154 if let Some(gen) = obj.get("pact:generator:type") {
155 debug!("detected pact:generator:type, will configure a generators");
156 if let Some(generator) = Generator::from_map(&json_to_string(gen), obj) {
157 let category = generator_category(matching_rules);
158 generators.add_generator_with_subcategory(category, path.clone(), generator);
159 }
160 }
161
162 trace!("matching_rules = {matching_rule_result:?}");
163 match &matching_rule_result {
164 Ok((rules, value)) => {
165 for rule in rules {
166 matching_rules.add_rule(path.clone(), rule.clone(), RuleLogic::And);
167 }
168 value.clone()
169 },
170 Err(err) => {
171 error!("Failed to parse matching rule from JSON - {}", err);
172 Value::Null
173 }
174 }
175}
176
177#[deprecated(note = "Replace with MatchingRule::create or matchers_from_integration_json")]
179pub fn matcher_from_integration_json(m: &Map<String, Value>) -> Option<MatchingRule> {
180 match m.get("pact:matcher:type") {
181 Some(value) => {
182 let val = json_to_string(value);
183 MatchingRule::create(val.as_str(), &Value::Object(m.clone()))
184 .map_err(|err| error!("Failed to create matching rule from JSON '{:?}': {}", m, err))
185 .ok()
186 },
187 _ => None
188 }
189}
190
191pub fn matchers_from_integration_json(m: &Map<String, Value>) -> anyhow::Result<(Vec<MatchingRule>, Option<Generator>)> {
193 match m.get("pact:matcher:type") {
194 Some(value) => {
195 let json_str = value.to_string();
196 match value {
197 Value::Array(arr) => {
198 let mut rules = vec![];
199 for v in arr.clone() {
200 match v.get("pact:matcher:type") {
201 Some(t) => {
202 let val = json_to_string(t);
203 let rule = MatchingRule::create(val.as_str(), &v)
204 .map_err(|err| {
205 error!("Failed to create matching rule from JSON '{:?}': {}", m, err);
206 err
207 })?;
208 rules.push(rule);
209 }
210 None => {
211 error!("Failed to create matching rule from JSON '{:?}': there is no 'pact:matcher:type' attribute", json_str);
212 bail!("Failed to create matching rule from JSON '{:?}': there is no 'pact:matcher:type' attribute", json_str);
213 }
214 }
215 }
216 Ok((rules, None))
217 }
218 _ => {
219 let val = json_to_string(value);
220 if val != "eachKey" && val != "eachValue" && val != "notEmpty" && is_matcher_def(val.as_str()) {
221 let mut rules = vec![];
222 let def = parse_matcher_def(val.as_str())?;
223 for rule in def.rules {
224 match rule {
225 Either::Left(rule) => rules.push(rule),
226 Either::Right(reference) => if m.contains_key(reference.name.as_str()) {
227 rules.push(MatchingRule::Type);
228 } else {
230 error!("Failed to create matching rules from JSON '{:?}': reference '{}' was not found", json_str, reference.name);
231 bail!("Failed to create matching rules from JSON '{:?}': reference '{}' was not found", json_str, reference.name);
232 }
233 }
234 }
235 Ok((rules, def.generator))
236 } else {
237 MatchingRule::create(val.as_str(), &Value::Object(m.clone()))
238 .map(|r| (vec![r], None))
239 .map_err(|err| {
240 error!("Failed to create matching rule from JSON '{:?}': {}", json_str, err);
241 err
242 })
243 }
244 }
245 }
246 },
247 _ => Ok((vec![], None))
248 }
249}
250
251pub fn process_json(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> String {
253 trace!("process_json");
254 match serde_json::from_str(&body) {
255 Ok(json) => match json {
256 Value::Object(ref map) => process_object(map, matching_rules, generators, DocPath::root(), false).to_string(),
257 Value::Array(ref array) => process_array(array, matching_rules, generators, DocPath::root(), false, false).to_string(),
258 _ => body
259 },
260 Err(_) => body
261 }
262}
263
264pub fn process_json_value(body: &Value, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> String {
266 match body {
267 Value::Object(ref map) => process_object(map, matching_rules, generators, DocPath::root(), false).to_string(),
268 Value::Array(ref array) => process_array(array, matching_rules, generators, DocPath::root(), false, false).to_string(),
269 _ => body.to_string()
270 }
271}
272
273pub fn request_multipart(
275 request: &mut HttpRequest,
276 boundary: &str,
277 body: OptionalBody,
278 content_type: &str,
279 part_name: &str
280) {
281 if let Some(parts) = add_part_to_multipart(&request.body, &body, boundary) {
282 debug!("Found existing multipart with the same boundary marker, will append to it");
285 request.body = OptionalBody::Present(parts, request.body.content_type(), get_content_type_hint(&request.body));
286 } else {
287 let multipart = format!("multipart/form-data; boundary={}", boundary);
290 request.set_header(CONTENT_TYPE_HEADER, &[multipart.as_str()]);
291 request.body = body;
292
293 request.matching_rules.add_category("header")
294 .add_rule(DocPath::new_unwrap("Content-Type"),
295 MatchingRule::Regex(r"multipart/form-data;(\s*charset=[^;]*;)?\s*boundary=.*".into()), RuleLogic::And);
296 }
297
298 let mut path = DocPath::root();
299 path.push_field(part_name);
300 request.matching_rules.add_category("body")
301 .add_rule(path, MatchingRule::ContentType(content_type.into()), RuleLogic::And);
302}
303
304fn add_part_to_multipart(body: &OptionalBody, new_part: &OptionalBody, boundary: &str) -> Option<Bytes> {
305 if let Some(boundary_marker) = contains_existing_multipart(body) {
306 let existing_parts = body.value().unwrap_or_default();
307 let end_marker = format!("--{}--\r\n", boundary_marker);
308 let base = existing_parts.strip_suffix(end_marker.as_bytes()).unwrap_or(&existing_parts);
309 let new_part = part_body_replace_marker(new_part, boundary, &boundary_marker.as_str());
310
311 let mut bytes = BytesMut::from(base);
312 bytes.extend(new_part);
313 Some(bytes.freeze())
314 } else {
315 None
316 }
317}
318
319pub fn part_body_replace_marker(body: &OptionalBody, boundary: &str, new_boundary: &str) -> Bytes {
321 let marker = format!("--{}\r\n", new_boundary);
322 let end_marker = format!("--{}--\r\n", new_boundary);
323
324 let marker_to_replace = format!("--{}\r\n", boundary);
325 let end_marker_to_replace = format!("--{}--\r\n", boundary);
326 let body = body.value().unwrap_or_default();
327 let body = body.strip_prefix(marker_to_replace.as_bytes()).unwrap_or(&body);
328 let body = body.strip_suffix(end_marker_to_replace.as_bytes()).unwrap_or(&body);
329
330 let mut bytes = BytesMut::new();
331 bytes.extend(marker.as_bytes());
332 bytes.extend(body);
333 bytes.extend(end_marker.as_bytes());
334 bytes.freeze()
335}
336
337pub fn get_content_type_hint(body: &OptionalBody) -> Option<ContentTypeHint> {
339 match &body {
340 OptionalBody::Present(_, _, hint) => *hint,
341 _ => None
342 }
343}
344
345fn contains_existing_multipart(body: &OptionalBody) -> Option<String> {
346 if let OptionalBody::Present(body, ..) = &body {
347 let body_str = String::from_utf8_lossy(&body);
348 if let Some(captures) = MULTIPART_MARKER.captures(&body_str) {
349 captures.get(1).map(|marker| marker.as_str().to_string())
350 } else {
351 None
352 }
353 } else {
354 None
355 }
356}
357
358pub fn response_multipart(
360 response: &mut HttpResponse,
361 boundary: &str,
362 body: OptionalBody,
363 content_type: &str,
364 part_name: &str
365) {
366 if let Some(parts) = add_part_to_multipart(&response.body, &body, boundary) {
367 debug!("Found existing multipart with the same boundary marker, will append to it");
370 response.body = OptionalBody::Present(parts, response.body.content_type(), get_content_type_hint(&response.body));
371 } else {
372 let multipart = format!("multipart/form-data; boundary={}", boundary);
375 response.set_header(CONTENT_TYPE_HEADER, &[multipart.as_str()]);
376 response.body = body;
377
378 response.matching_rules.add_category("header")
379 .add_rule(DocPath::new_unwrap("Content-Type"),
380 MatchingRule::Regex(r"multipart/form-data;(\s*charset=[^;]*;)?\s*boundary=.*".into()), RuleLogic::And);
381 }
382
383 let mut path = DocPath::root();
384 path.push_field(part_name);
385 response.matching_rules.add_category("body")
386 .add_rule(path, MatchingRule::ContentType(content_type.into()), RuleLogic::And);
387}
388
389#[derive(Clone, Debug)]
391pub struct MultipartBody {
392 pub body: OptionalBody,
394
395 pub boundary: String,
397}
398
399pub fn file_as_multipart_body(file: &str, part_name: &str) -> Result<MultipartBody, String> {
401 let mut multipart = multipart::client::Multipart::from_request(multipart::mock::ClientRequest::default()).unwrap();
402
403 multipart.write_file(part_name, Path::new(file)).map_err(format_multipart_error)?;
404 let http_buffer = multipart.send().map_err(format_multipart_error)?;
405
406 Ok(MultipartBody {
407 body: OptionalBody::Present(Bytes::from(http_buffer.buf), Some("multipart/form-data".into()), None),
408 boundary: http_buffer.boundary
409 })
410}
411
412pub fn empty_multipart_body() -> Result<MultipartBody, String> {
414 let multipart = multipart::client::Multipart::from_request(multipart::mock::ClientRequest::default()).unwrap();
415 let http_buffer = multipart.send().map_err(format_multipart_error)?;
416
417 Ok(MultipartBody {
418 body: OptionalBody::Present(Bytes::from(http_buffer.buf), Some("multipart/form-data".into()), None),
419 boundary: http_buffer.boundary
420 })
421}
422
423fn format_multipart_error(e: std::io::Error) -> String {
424 format!("convert_ptr_to_mime_part_body: Failed to generate multipart body: {}", e)
425}
426
427#[cfg(test)]
428mod test {
429 use std::collections::HashMap;
430
431use expectest::prelude::*;
432 use maplit::hashmap;
433 use pact_models::prelude::Category;
434use pretty_assertions::assert_eq;
435 use rstest::rstest;
436 use serde_json::json;
437
438 use pact_models::{generators, HttpStatus, matchingrules_list};
439 use pact_models::content_types::ContentType;
440 use pact_models::generators::{Generator, Generators};
441 use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory, RuleList};
442 use pact_models::matchingrules::expressions::{MatchingRuleDefinition, ValueType};
443 use pact_models::path_exp::DocPath;
444
445 #[allow(deprecated)]
446 use crate::mock_server::bodies::{matcher_from_integration_json, process_object};
447
448 use super::*;
449
450 #[test]
451 fn process_object_with_normal_json_test() {
452 let json = json!({
453 "a": "b",
454 "c": [100, 200, 300]
455 });
456 let mut matching_rules = MatchingRuleCategory::default();
457 let mut generators = Generators::default();
458 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
459 &mut generators, DocPath::root(), false);
460
461 expect!(result).to(be_equal_to(json));
462 }
463
464 #[test]
465 fn process_object_with_matching_rule_test() {
466 let json = json!({
467 "a": {
468 "pact:matcher:type": "regex",
469 "regex": "\\w+",
470 "value": "b"
471 },
472 "c": [100, 200, {
473 "pact:matcher:type": "integer",
474 "pact:generator:type": "RandomInt",
475 "value": 300
476 }]
477 });
478 let mut matching_rules = MatchingRuleCategory::empty("body");
479 let mut generators = Generators::default();
480 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
481 &mut generators, DocPath::root(), false);
482
483 expect!(result).to(be_equal_to(json!({
484 "a": "b",
485 "c": [100, 200, 300]
486 })));
487 expect!(matching_rules).to(be_equal_to(matchingrules_list!{
488 "body";
489 "$.a" => [ MatchingRule::Regex("\\w+".into()) ],
490 "$.c[2]" => [ MatchingRule::Integer ]
491 }));
492 expect!(generators).to(be_equal_to(generators! {
493 "BODY" => {
494 "$.c[2]" => Generator::RandomInt(0, 10)
495 }
496 }));
497 }
498
499 #[test]
500 fn process_object_with_primitive_json_value() {
501 let json = json!({
502 "pact:matcher:type": "regex",
503 "regex": "\\w+",
504 "value": "b"
505 });
506 let mut matching_rules = MatchingRuleCategory::empty("body");
507 let mut generators = Generators::default();
508 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
509 &mut generators, DocPath::root(), false);
510
511 expect!(result).to(be_equal_to(json!("b")));
512 expect!(matching_rules).to(be_equal_to(matchingrules_list!{
513 "body";
514 "$" => [ MatchingRule::Regex("\\w+".into()) ]
515 }));
516 expect!(generators).to(be_equal_to(Generators::default()));
517 }
518
519 #[test_log::test]
521 fn process_object_with_nested_object_has_the_same_property_name_as_a_parent_object() {
522 let json = json!({
523 "result": {
524 "pact:matcher:type": "type",
525 "value": {
526 "details": {
527 "pact:matcher:type": "type",
528 "value": [
529 {
530 "type": {
531 "pact:matcher:type": "regex",
532 "value": "Information",
533 "regex": "(None|Information|Warning|Error)"
534 }
535 }
536 ],
537 "min": 1
538 },
539 "findings": {
540 "pact:matcher:type": "type",
541 "value": [
542 {
543 "details": {
544 "pact:matcher:type": "type",
545 "value": [
546 {
547 "type": {
548 "pact:matcher:type": "regex",
549 "value": "Information",
550 "regex": "(None|Information|Warning|Error)"
551 }
552 }
553 ],
554 "min": 1
555 },
556 "type": {
557 "pact:matcher:type": "regex",
558 "value": "Unspecified",
559 "regex": "(None|Unspecified)"
560 }
561 }
562 ],
563 "min": 1
564 }
565 }
566 }
567 });
568 let mut matching_rules = MatchingRuleCategory::default();
569 let mut generators = Generators::default();
570 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
571 &mut generators, DocPath::root(), false);
572
573 expect!(result).to(be_equal_to(json!({
574 "result": {
575 "details": [
576 {
577 "type": "Information"
578 }
579 ],
580 "findings": [
581 {
582 "details": [
583 {
584 "type": "Information"
585 }
586 ],
587 "type": "Unspecified"
588 }
589 ]
590 }
591 })));
592 expect!(matching_rules.to_v3_json().to_string()).to(be_equal_to(matchingrules_list!{
593 "body";
594 "$.result" => [ MatchingRule::Type ],
595 "$.result.details" => [ MatchingRule::MinType(1) ],
596 "$.result.details[*].type" => [ MatchingRule::Regex("(None|Information|Warning|Error)".into()) ],
597 "$.result.findings" => [ MatchingRule::MinType(1) ],
598 "$.result.findings[*].details" => [ MatchingRule::MinType(1) ],
599 "$.result.findings[*].details[*].type" => [ MatchingRule::Regex("(None|Information|Warning|Error)".into()) ],
600 "$.result.findings[*].type" => [ MatchingRule::Regex("(None|Unspecified)".into()) ]
601 }.to_v3_json().to_string()));
602 expect!(generators).to(be_equal_to(Generators::default()));
603 }
604
605 #[test_log::test]
607 fn process_object_with_nested_object_with_type_matchers_and_decimal_matcher() {
608 let json = json!({
609 "pact:matcher:type": "type",
610 "value": {
611 "name": {
612 "pact:matcher:type": "type",
613 "value": "APL"
614 },
615 "price": {
616 "pact:matcher:type": "decimal",
617 "value": 1.23
618 }
619 }
620 });
621 let mut matching_rules = MatchingRuleCategory::default();
622 let mut generators = Generators::default();
623 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
624 &mut generators, DocPath::root(), false);
625
626 expect!(result).to(be_equal_to(json!({
627 "name": "APL",
628 "price": 1.23
629 })));
630 expect!(matching_rules).to(be_equal_to(matchingrules_list!{
631 "body";
632 "$" => [ MatchingRule::Type ],
633 "$.name" => [ MatchingRule::Type ],
634 "$.price" => [ MatchingRule::Decimal ]
635 }));
636 expect!(generators).to(be_equal_to(Generators::default()));
637 }
638
639 #[test_log::test]
641 fn process_object_with_each_value_matcher_on_object() {
642 let json = json!({
643 "pact:matcher:type": "each-value",
644 "value": {
645 "price": 1.23
646 },
647 "rules": [
648 {
649 "pact:matcher:type": "decimal"
650 }
651 ]
652 });
653 let mut matching_rules = MatchingRuleCategory::default();
654 let mut generators = Generators::default();
655 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
656 &mut generators, DocPath::root(), false);
657
658 expect!(result).to(be_equal_to(json!({
659 "price": 1.23
660 })));
661 expect!(matching_rules).to(be_equal_to(matchingrules_list!{
662 "body";
663 "$" => [ MatchingRule::EachValue(MatchingRuleDefinition::new("{\"price\":1.23}".to_string(),
664 ValueType::Unknown, MatchingRule::Decimal, None, "".to_string())) ]
665 }));
666 expect!(generators).to(be_equal_to(Generators::default()));
667 }
668
669 #[test_log::test]
671 fn process_object_with_each_key_matcher_on_object() {
672 let json = json!({
673 "pact:matcher:type": "each-key",
674 "value": {
675 "123": "cool book"
676 },
677 "rules": [
678 {
679 "pact:matcher:type": "regex",
680 "regex": "\\d+"
681 }
682 ]
683 });
684 let mut matching_rules = MatchingRuleCategory::default();
685 let mut generators = Generators::default();
686 let result = process_object(json.as_object().unwrap(), &mut matching_rules,
687 &mut generators, DocPath::root(), false);
688
689 expect!(result).to(be_equal_to(json!({
690 "123": "cool book"
691 })));
692 expect!(matching_rules).to(be_equal_to(matchingrules_list!{
693 "body";
694 "$" => [ MatchingRule::EachKey(MatchingRuleDefinition::new("{\"123\":\"cool book\"}".to_string(),
695 ValueType::Unknown, MatchingRule::Regex("\\d+".to_string()), None, "".to_string())) ]
696 }));
697 expect!(generators).to(be_equal_to(Generators::default()));
698 }
699
700 #[test_log::test]
701 #[allow(deprecated)]
702 fn matcher_from_integration_json_test() {
703 expect!(matcher_from_integration_json(&Map::default())).to(be_none());
704 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "Other" }).as_object().unwrap()))
705 .to(be_none());
706 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "regex" }).as_object().unwrap()))
707 .to(be_none());
708 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "regex", "regex": "[a-z]" }).as_object().unwrap()))
709 .to(be_some().value(MatchingRule::Regex("[a-z]".to_string())));
710 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "equality" }).as_object().unwrap()))
711 .to(be_some().value(MatchingRule::Equality));
712 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "include" }).as_object().unwrap()))
713 .to(be_none());
714 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "include", "value": "[a-z]" }).as_object().unwrap()))
715 .to(be_some().value(MatchingRule::Include("[a-z]".to_string())));
716 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "type" }).as_object().unwrap()))
717 .to(be_some().value(MatchingRule::Type));
718 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "type", "min": 100 }).as_object().unwrap()))
719 .to(be_some().value(MatchingRule::MinType(100)));
720 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "type", "max": 100 }).as_object().unwrap()))
721 .to(be_some().value(MatchingRule::MaxType(100)));
722 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "type", "min": 10, "max": 100 }).as_object().unwrap()))
723 .to(be_some().value(MatchingRule::MinMaxType(10, 100)));
724 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "number" }).as_object().unwrap()))
725 .to(be_some().value(MatchingRule::Number));
726 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "integer" }).as_object().unwrap()))
727 .to(be_some().value(MatchingRule::Integer));
728 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "decimal" }).as_object().unwrap()))
729 .to(be_some().value(MatchingRule::Decimal));
730 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "real" }).as_object().unwrap()))
731 .to(be_some().value(MatchingRule::Decimal));
732 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "min" }).as_object().unwrap()))
733 .to(be_none());
734 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "min", "min": 100 }).as_object().unwrap()))
735 .to(be_some().value(MatchingRule::MinType(100)));
736 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "max" }).as_object().unwrap()))
737 .to(be_none());
738 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "max", "max": 100 }).as_object().unwrap()))
739 .to(be_some().value(MatchingRule::MaxType(100)));
740 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "timestamp" }).as_object().unwrap()))
741 .to(be_some().value(MatchingRule::Timestamp("".to_string())));
742 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "timestamp", "format": "yyyy-MM-dd" }).as_object().unwrap()))
743 .to(be_some().value(MatchingRule::Timestamp("yyyy-MM-dd".to_string())));
744 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "timestamp", "timestamp": "yyyy-MM-dd" }).as_object().unwrap()))
745 .to(be_some().value(MatchingRule::Timestamp("yyyy-MM-dd".to_string())));
746 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "datetime" }).as_object().unwrap()))
747 .to(be_some().value(MatchingRule::Timestamp("".to_string())));
748 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "datetime", "format": "yyyy-MM-dd" }).as_object().unwrap()))
749 .to(be_some().value(MatchingRule::Timestamp("yyyy-MM-dd".to_string())));
750 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "datetime", "datetime": "yyyy-MM-dd" }).as_object().unwrap()))
751 .to(be_some().value(MatchingRule::Timestamp("yyyy-MM-dd".to_string())));
752 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "date" }).as_object().unwrap()))
753 .to(be_some().value(MatchingRule::Date("".to_string())));
754 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "date", "format": "yyyy-MM-dd" }).as_object().unwrap()))
755 .to(be_some().value(MatchingRule::Date("yyyy-MM-dd".to_string())));
756 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "date", "date": "yyyy-MM-dd" }).as_object().unwrap()))
757 .to(be_some().value(MatchingRule::Date("yyyy-MM-dd".to_string())));
758 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "time" }).as_object().unwrap()))
759 .to(be_some().value(MatchingRule::Time("".to_string())));
760 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "time", "format": "yyyy-MM-dd" }).as_object().unwrap()))
761 .to(be_some().value(MatchingRule::Time("yyyy-MM-dd".to_string())));
762 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "time", "time": "yyyy-MM-dd" }).as_object().unwrap()))
763 .to(be_some().value(MatchingRule::Time("yyyy-MM-dd".to_string())));
764 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "null" }).as_object().unwrap()))
765 .to(be_some().value(MatchingRule::Null));
766
767 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "boolean" }).as_object().unwrap()))
769 .to(be_some().value(MatchingRule::Boolean));
770 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "contentType" }).as_object().unwrap()))
771 .to(be_none());
772 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "contentType", "value": "text/plain" }).as_object().unwrap()))
773 .to(be_some().value(MatchingRule::ContentType("text/plain".to_string())));
774 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "content-type" }).as_object().unwrap()))
775 .to(be_none());
776 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "content-type", "value": "text/plain" }).as_object().unwrap()))
777 .to(be_some().value(MatchingRule::ContentType("text/plain".to_string())));
778 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "arrayContains" }).as_object().unwrap()))
779 .to(be_none());
780 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "arrayContains", "variants": "text" }).as_object().unwrap()))
781 .to(be_none());
782 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "arrayContains", "variants": [] }).as_object().unwrap()))
783 .to(be_some().value(MatchingRule::ArrayContains(vec![])));
784 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "array-contains" }).as_object().unwrap()))
785 .to(be_none());
786 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "array-contains", "variants": "text" }).as_object().unwrap()))
787 .to(be_none());
788 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "array-contains", "variants": [] }).as_object().unwrap()))
789 .to(be_some().value(MatchingRule::ArrayContains(vec![])));
790 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "values" }).as_object().unwrap()))
791 .to(be_some().value(MatchingRule::Values));
792 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "statusCode" }).as_object().unwrap()))
793 .to(be_some().value(MatchingRule::StatusCode(HttpStatus::Success)));
794 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "statusCode", "status": [200] }).as_object().unwrap()))
795 .to(be_some().value(MatchingRule::StatusCode(HttpStatus::StatusCodes(vec![200]))));
796 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "status-code" }).as_object().unwrap()))
797 .to(be_some().value(MatchingRule::StatusCode(HttpStatus::Success)));
798 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "status-code", "status": "success" }).as_object().unwrap()))
799 .to(be_some().value(MatchingRule::StatusCode(HttpStatus::Success)));
800 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "notEmpty" }).as_object().unwrap()))
801 .to(be_some().value(MatchingRule::NotEmpty));
802 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "not-empty" }).as_object().unwrap()))
803 .to(be_some().value(MatchingRule::NotEmpty));
804 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "semver" }).as_object().unwrap()))
805 .to(be_some().value(MatchingRule::Semver));
806 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "eachKey" }).as_object().unwrap()))
807 .to(be_some().value(MatchingRule::EachKey(MatchingRuleDefinition {
808 value: "".to_string(),
809 value_type: ValueType::Unknown,
810 rules: vec![],
811 generator: None,
812 expression: "".to_string()
813 })));
814 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "each-key" }).as_object().unwrap()))
815 .to(be_some().value(MatchingRule::EachKey(MatchingRuleDefinition {
816 value: "".to_string(),
817 value_type: ValueType::Unknown,
818 rules: vec![],
819 generator: None,
820 expression: "".to_string()
821 })));
822 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "eachValue" }).as_object().unwrap()))
823 .to(be_some().value(MatchingRule::EachValue(MatchingRuleDefinition {
824 value: "".to_string(),
825 value_type: ValueType::Unknown,
826 rules: vec![],
827 generator: None,
828 expression: "".to_string()
829 })));
830 expect!(matcher_from_integration_json(&json!({ "pact:matcher:type": "each-value" }).as_object().unwrap()))
831 .to(be_some().value(MatchingRule::EachValue(MatchingRuleDefinition {
832 value: "".to_string(),
833 value_type: ValueType::Unknown,
834 rules: vec![],
835 generator: None,
836 expression: "".to_string()
837 })));
838 }
839
840 #[rstest]
841 #[case(json!({}), vec![])]
842 #[case(json!({ "pact:matcher:type": "regex", "regex": "[a-z]" }), vec![MatchingRule::Regex("[a-z]".to_string())])]
843 #[case(json!({ "pact:matcher:type": "equality" }), vec![MatchingRule::Equality])]
844 #[case(json!({ "pact:matcher:type": "include", "value": "[a-z]" }), vec![MatchingRule::Include("[a-z]".to_string())])]
845 #[case(json!({ "pact:matcher:type": "type" }), vec![MatchingRule::Type])]
846 #[case(json!({ "pact:matcher:type": "type", "min": 100 }), vec![MatchingRule::MinType(100)])]
847 #[case(json!({ "pact:matcher:type": "type", "max": 100 }), vec![MatchingRule::MaxType(100)])]
848 #[case(json!({ "pact:matcher:type": "type", "min": 10, "max": 100 }), vec![MatchingRule::MinMaxType(10, 100)])]
849 #[case(json!({ "pact:matcher:type": "number" }), vec![MatchingRule::Number])]
850 #[case(json!({ "pact:matcher:type": "integer" }), vec![MatchingRule::Integer])]
851 #[case(json!({ "pact:matcher:type": "decimal" }), vec![MatchingRule::Decimal])]
852 #[case(json!({ "pact:matcher:type": "real" }), vec![MatchingRule::Decimal])]
853 #[case(json!({ "pact:matcher:type": "min", "min": 100 }), vec![MatchingRule::MinType(100)])]
854 #[case(json!({ "pact:matcher:type": "max", "max": 100 }), vec![MatchingRule::MaxType(100)])]
855 #[case(json!({ "pact:matcher:type": "timestamp" }), vec![MatchingRule::Timestamp("".to_string())])]
856 #[case(json!({ "pact:matcher:type": "timestamp", "format": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])]
857 #[case(json!({ "pact:matcher:type": "timestamp", "timestamp": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])]
858 #[case(json!({ "pact:matcher:type": "datetime" }), vec![MatchingRule::Timestamp("".to_string())])]
859 #[case(json!({ "pact:matcher:type": "datetime", "format": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])]
860 #[case(json!({ "pact:matcher:type": "datetime", "datetime": "yyyy-MM-dd" }), vec![MatchingRule::Timestamp("yyyy-MM-dd".to_string())])]
861 #[case(json!({ "pact:matcher:type": "date" }), vec![MatchingRule::Date("".to_string())])]
862 #[case(json!({ "pact:matcher:type": "date", "format": "yyyy-MM-dd" }), vec![MatchingRule::Date("yyyy-MM-dd".to_string())])]
863 #[case(json!({ "pact:matcher:type": "date", "date": "yyyy-MM-dd" }), vec![MatchingRule::Date("yyyy-MM-dd".to_string())])]
864 #[case(json!({ "pact:matcher:type": "time" }), vec![MatchingRule::Time("".to_string())])]
865 #[case(json!({ "pact:matcher:type": "time", "format": "yyyy-MM-dd" }), vec![MatchingRule::Time("yyyy-MM-dd".to_string())])]
866 #[case(json!({ "pact:matcher:type": "time", "time": "yyyy-MM-dd" }), vec![MatchingRule::Time("yyyy-MM-dd".to_string())])]
867 #[case(json!({ "pact:matcher:type": "null" }), vec![MatchingRule::Null])]
868 #[case(json!({ "pact:matcher:type": "boolean" }), vec![MatchingRule::Boolean])]
869 #[case(json!({ "pact:matcher:type": "contentType", "value": "text/plain" }), vec![MatchingRule::ContentType("text/plain".to_string())])]
870 #[case(json!({ "pact:matcher:type": "content-type", "value": "text/plain" }), vec![MatchingRule::ContentType("text/plain".to_string())])]
871 #[case(json!({ "pact:matcher:type": "arrayContains", "variants": [] }), vec![MatchingRule::ArrayContains(vec![])])]
872 #[case(json!({ "pact:matcher:type": "array-contains", "variants": [] }), vec![MatchingRule::ArrayContains(vec![])])]
873 #[case(json!({ "pact:matcher:type": "array-contains", "variants": ["Thing1", "Thing2"] }), vec![
874 MatchingRule::ArrayContains(vec![
875 (0, MatchingRuleCategory{name: Category::BODY, rules: HashMap::from([ (DocPath::empty(), RuleList::equality()) ]) }, std::collections::HashMap::default()),
876 (0, MatchingRuleCategory{name: Category::BODY, rules: HashMap::from([ (DocPath::empty(), RuleList::equality()) ]) }, std::collections::HashMap::default())
877 ])
878 ])]
879 #[case(json!({ "pact:matcher:type": "values" }), vec![MatchingRule::Values])]
880 #[case(json!({ "pact:matcher:type": "statusCode" }), vec![MatchingRule::StatusCode(HttpStatus::Success)])]
881 #[case(json!({ "pact:matcher:type": "statusCode" }), vec![MatchingRule::StatusCode(HttpStatus::StatusCodes(vec![200]))])]
882 #[case(json!({ "pact:matcher:type": "status-code" }), vec![MatchingRule::StatusCode(HttpStatus::Success)])]
883 #[case(json!({ "pact:matcher:type": "status-code" }), vec![MatchingRule::StatusCode(HttpStatus::StatusCodes(vec![200]))])]
884 #[case(json!({ "pact:matcher:type": "notEmpty" }), vec![MatchingRule::NotEmpty])]
885 #[case(json!({ "pact:matcher:type": "not-empty" }), vec![MatchingRule::NotEmpty])]
886 #[case(json!({ "pact:matcher:type": "semver" }), vec![MatchingRule::Semver])]
887 #[case(json!({ "pact:matcher:type": "eachKey" }), vec![MatchingRule::EachKey(MatchingRuleDefinition {
888 value: "".to_string(),
889 value_type: ValueType::Unknown,
890 rules: vec![],
891 generator: None,
892 expression: "".to_string()
893 })])]
894 #[case(json!({ "pact:matcher:type": "each-key" }), vec![MatchingRule::EachKey(MatchingRuleDefinition {
895 value: "".to_string(),
896 value_type: ValueType::Unknown,
897 rules: vec![],
898 generator: None,
899 expression: "".to_string()
900 })])]
901 #[case(json!({ "pact:matcher:type": "eachValue" }), vec![MatchingRule::EachValue(MatchingRuleDefinition {
902 value: "".to_string(),
903 value_type: ValueType::Unknown,
904 rules: vec![],
905 generator: None,
906 expression: "".to_string()
907 })])]
908 #[case(json!({ "pact:matcher:type": "each-value" }), vec![MatchingRule::EachValue(MatchingRuleDefinition {
909 value: "".to_string(),
910 value_type: ValueType::Unknown,
911 rules: vec![],
912 generator: None,
913 expression: "".to_string()
914 })])]
915 #[case(json!({ "pact:matcher:type": [{"pact:matcher:type": "regex", "regex": "[a-z]"}] }), vec![MatchingRule::Regex("[a-z]".to_string())])]
916 #[case(json!({ "pact:matcher:type": [
917 { "pact:matcher:type": "regex", "regex": "[a-z]" },
918 { "pact:matcher:type": "equality" },
919 { "pact:matcher:type": "include", "value": "[a-z]" }
920 ] }), vec![MatchingRule::Regex("[a-z]".to_string()), MatchingRule::Equality, MatchingRule::Include("[a-z]".to_string())])]
921 fn matchers_from_integration_json_ok_test(#[case] json: Value, #[case] value: Vec<MatchingRule>) {
922 expect!(matchers_from_integration_json(&json.as_object().unwrap())).to(be_ok().value((value, None)));
923 }
924
925 #[rstest]
926 #[case(json!({ "pact:matcher:type": "Other" }), "Other is not a valid matching rule type")]
927 #[case(json!({ "pact:matcher:type": "regex" }), "Regex matcher missing 'regex' field")]
928 #[case(json!({ "pact:matcher:type": "include" }), "Include matcher missing 'value' field")]
929 #[case(json!({ "pact:matcher:type": "min" }), "Min matcher missing 'min' field")]
930 #[case(json!({ "pact:matcher:type": "max" }), "Max matcher missing 'max' field")]
931 #[case(json!({ "pact:matcher:type": "contentType" }), "ContentType matcher missing 'value' field")]
932 #[case(json!({ "pact:matcher:type": "content-type" }), "ContentType matcher missing 'value' field")]
933 #[case(json!({ "pact:matcher:type": "arrayContains" }), "ArrayContains matcher missing 'variants' field")]
934 #[case(json!({ "pact:matcher:type": "array-contains" }), "ArrayContains matcher missing 'variants' field")]
935 #[case(json!({ "pact:matcher:type": "arrayContains", "variants": "text" }), "ArrayContains matcher 'variants' field is not an Array")]
936 #[case(json!({ "pact:matcher:type": "array-contains", "variants": "text" }), "ArrayContains matcher 'variants' field is not an Array")]
937 #[case(json!({ "pact:matcher:type": [
938 { "pact:matcher:type": "regex", "regex": "[a-z]" },
939 { "pact:matcher:type": "equality" },
940 { "pact:matcher:type": "include" }
941 ]}), "Include matcher missing 'value' field")]
942 fn matchers_from_integration_json_error_test(#[case] json: Value, #[case] error: &str) {
943 expect!(matchers_from_integration_json(&json.as_object().unwrap())
944 .unwrap_err().to_string())
945 .to(be_equal_to(error));
946 }
947
948 #[test_log::test]
949 fn request_multipart_test() {
950 let mut request = HttpRequest::default();
951 let body = Bytes::from_static(b"--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\nContent-Type: application/json\r\n\r\n{}\r\n--ABCD--\r\n");
952 let ct = ContentType::parse("application/json").unwrap();
953
954 request_multipart(&mut request, "ABCD", OptionalBody::Present(body, Some(ct.clone()), None), &ct.to_string(), "part-1");
955
956 expect!(request.headers.unwrap()).to(be_equal_to(hashmap!{
957 "Content-Type".to_string() => vec!["multipart/form-data; boundary=ABCD".to_string()]
958 }));
959 assert_eq!("--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\n\
960Content-Type: application/json\r\n\r\n{}\r\n--ABCD--\r\n",
961 request.body.value_as_string().unwrap());
962 }
963
964 #[test_log::test]
966 fn request_multipart_allows_multiple_parts() {
967 let mut request = HttpRequest::default();
968 let body1 = Bytes::from_static(b"--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\nContent-Type: application/json\r\n\r\n{}\r\n--ABCD--\r\n");
969 let ct1 = ContentType::parse("application/json").unwrap();
970 let body2 = Bytes::from_static(b"--ABCD\r\nContent-Disposition: form-data; name=\"part-2\"; filename=\"2.txt\"\r\nContent-Type: text/plain\r\n\r\nTEXT\r\n--ABCD--\r\n");
971 let ct2 = ContentType::parse("text/plain").unwrap();
972
973 request_multipart(&mut request, "ABCD", OptionalBody::Present(body1, Some(ct1.clone()), None), &ct1.to_string(), "part-1");
974 request_multipart(&mut request, "ABCD", OptionalBody::Present(body2, Some(ct2.clone()), None), &ct2.to_string(), "part-2");
975
976 expect!(request.headers.unwrap()).to(be_equal_to(hashmap!{
977 "Content-Type".to_string() => vec!["multipart/form-data; boundary=ABCD".to_string()]
978 }));
979 assert_eq!("--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\n\
980Content-Type: application/json\r\n\r\n{}\r\n--ABCD\r\nContent-Disposition: form-data; \
981name=\"part-2\"; filename=\"2.txt\"\r\nContent-Type: text/plain\r\n\r\nTEXT\r\n--ABCD--\r\n",
982 request.body.value_as_string().unwrap());
983 }
984
985 #[test_log::test]
986 fn response_multipart_test() {
987 let mut response = HttpResponse::default();
988 let body = Bytes::from_static(b"--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\nContent-Type: application/json\r\n\r\n{}\r\n--ABCD--\r\n");
989 let ct = ContentType::parse("application/json").unwrap();
990
991 response_multipart(&mut response, "ABCD", OptionalBody::Present(body, Some(ct.clone()), None), &ct.to_string(), "part-1");
992
993 expect!(response.headers.unwrap()).to(be_equal_to(hashmap!{
994 "Content-Type".to_string() => vec!["multipart/form-data; boundary=ABCD".to_string()]
995 }));
996 assert_eq!("--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\n\
997Content-Type: application/json\r\n\r\n{}\r\n--ABCD--\r\n",
998 response.body.value_as_string().unwrap());
999 }
1000
1001 #[test_log::test]
1003 fn response_multipart_allows_multiple_parts() {
1004 let mut response = HttpResponse::default();
1005 let body1 = Bytes::from_static(b"--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\nContent-Type: application/json\r\n\r\n{}\r\n--ABCD--\r\n");
1006 let ct1 = ContentType::parse("application/json").unwrap();
1007 let body2 = Bytes::from_static(b"--ABCD\r\nContent-Disposition: form-data; name=\"part-2\"; filename=\"2.txt\"\r\nContent-Type: text/plain\r\n\r\nTEXT\r\n--ABCD--\r\n");
1008 let ct2 = ContentType::parse("text/plain").unwrap();
1009
1010 response_multipart(&mut response, "ABCD", OptionalBody::Present(body1, Some(ct1.clone()), None), &ct1.to_string(), "part-1");
1011 response_multipart(&mut response, "ABCD", OptionalBody::Present(body2, Some(ct2.clone()), None), &ct2.to_string(), "part-2");
1012
1013 expect!(response.headers.unwrap()).to(be_equal_to(hashmap!{
1014 "Content-Type".to_string() => vec!["multipart/form-data; boundary=ABCD".to_string()]
1015 }));
1016 assert_eq!("--ABCD\r\nContent-Disposition: form-data; name=\"part-1\"; filename=\"1.json\"\r\n\
1017Content-Type: application/json\r\n\r\n{}\r\n--ABCD\r\nContent-Disposition: form-data; \
1018name=\"part-2\"; filename=\"2.txt\"\r\nContent-Type: text/plain\r\n\r\nTEXT\r\n--ABCD--\r\n",
1019 response.body.value_as_string().unwrap());
1020 }
1021}