pact_ffi/mock_server/
bodies.rs

1//! Functions to support processing request/response bodies
2
3use 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
31/// Process an array with embedded matching rules and generators
32pub 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
57/// Process an object (map) with embedded matching rules and generators
58pub 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
91// Process a matching rule definition from the JSON and returns the reified JSON
92fn 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/// Builds a `MatchingRule` from a `Value` struct used by language integrations
178#[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
191/// Builds a list of `MatchingRule` from a `Value` struct used by language integrations
192pub 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                  // TODO: We need to somehow drop the reference otherwise the matching will try compare it
229                } 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
251/// Process a JSON body with embedded matching rules and generators
252pub 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
264/// Process a JSON body with embedded matching rules and generators
265pub 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
273/// Setup the request as a multipart form upload
274pub 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    // Exiting part with the same boundary marker found, just add the new part to the end
283    // This assumes that the previous call will have correctly setup headers and matching rules etc.
284    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    // Either no existing multipart exists, or there is one with a different marker, so we
288    // overwrite it.
289    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
319/// Replace multipart marker in body
320pub 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
337/// Get content type hint from body
338pub 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
358/// Setup the response as a multipart form upload
359pub 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    // Exiting part with the same boundary marker found, just add the new part to the end
368    // This assumes that the previous call will have correctly setup headers and matching rules etc.
369    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    // Either no existing multipart exists, or there is one with a different marker, so we
373    // overwrite it.
374    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/// Representation of a multipart body
390#[derive(Clone, Debug)]
391pub struct MultipartBody {
392  /// The actual body
393  pub body: OptionalBody,
394
395  /// The boundary used in the multipart encoding
396  pub boundary: String,
397}
398
399/// Loads an example file as a MIME Multipart body
400pub 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
412/// Create an empty MIME Multipart body
413pub 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  // Issue #179
520  #[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  // Issue #179
606  #[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  // Issue #299
640  #[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  // Issue #299
670  #[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    // V4 matching rules
768    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  // Issue #314
965  #[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  // Issue #314
1002  #[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}