pact_mock_server 2.2.1

Pact-Rust support library that implements in process mock server
Documentation
use expectest::expect;
use expectest::prelude::*;
use maplit::hashmap;
use pact_matching::Mismatch;
use pact_models::bodies::OptionalBody;
use pact_models::matchingrules;
use pact_models::matchingrules::MatchingRule;
use pact_models::prelude::v4::{SynchronousHttp, V4Pact};
use pact_models::v4::http_parts::{HttpRequest, HttpResponse};
use pact_models::v4::interaction::V4Interaction;
use reqwest::header::ACCEPT;

use crate::builder::MockServerBuilder;
use crate::matching::{match_request, MatchResult};

use super::*;

#[tokio::test]
async fn match_request_returns_a_match_for_identical_requests() {
    let request = HttpRequest::default();
    let interaction = SynchronousHttp { request: request.clone(), .. SynchronousHttp::default() };
    let interactions = vec![interaction.boxed_v4()];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(MatchResult::RequestMatch(interaction.request.clone(),
      interaction.response.clone(), request.clone())));
}

#[tokio::test]
async fn match_request_returns_a_not_found_for_no_interactions() {
    let request = HttpRequest::default();
    let interactions = vec![];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(MatchResult::RequestNotFound(request)));
}

#[tokio::test]
async fn match_request_returns_a_match_for_multiple_identical_requests() {
    let request = HttpRequest::default();
    let interaction = SynchronousHttp { request: request.clone(), .. SynchronousHttp::default() };
    let interaction2 = SynchronousHttp {
      description: "test2".to_string(),
      request: request.clone(),
      ..SynchronousHttp::default()
    };
    let interactions = vec![
      interaction.boxed_v4(),
      interaction2.boxed_v4()
    ];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(
      MatchResult::RequestMatch(interaction.request, interaction.response, request.clone())));
}

#[tokio::test]
async fn match_request_returns_a_match_for_multiple_requests() {
    let request = HttpRequest { method: "GET".to_string(), .. HttpRequest::default() };
    let request2 = HttpRequest { method: "POST".to_string(), path: "/post".to_string(), .. HttpRequest::default() };
    let interaction = SynchronousHttp { request: request.clone(), .. SynchronousHttp::default() };
    let interaction2 = SynchronousHttp {
      description: "test2".to_string(),
      request: request2.clone(),
      ..SynchronousHttp::default()
    };
    let interactions = vec![
      interaction.boxed_v4(),
      interaction2.boxed_v4()
    ];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(
      MatchResult::RequestMatch(interaction.request, interaction.response, request.clone())));
}

#[tokio::test]
async fn match_request_returns_a_mismatch_for_incorrect_request() {
    let request = HttpRequest::default();
    let expected_request = HttpRequest { query: Some(hashmap!{ "QueryA".to_string() => vec![Some("Value A".to_string())] }),
        .. HttpRequest::default() };
    let interaction = SynchronousHttp {
      request: expected_request,
      ..SynchronousHttp::default()
    };
    let interactions = vec![
      interaction.boxed_v4()
    ];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result.match_key()).to(be_equal_to("Request-Mismatch".to_string()));
}

#[tokio::test]
async fn match_request_returns_request_not_found_if_method_or_path_do_not_match() {
    let request = HttpRequest { method: "GET".to_string(), path: "/path".to_string(), .. HttpRequest::default() };
    let expected_request = HttpRequest { method: "POST".to_string(), path: "/otherpath".to_string(),
        .. HttpRequest::default() };
    let interaction = SynchronousHttp {
      request: expected_request,
      ..SynchronousHttp::default()
    };
    let interactions = vec![
      interaction.boxed_v4()
    ];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(MatchResult::RequestNotFound(request)));
}

#[tokio::test]
async fn match_request_returns_the_most_appropriate_mismatch_for_multiple_requests() {
    let request = HttpRequest { method: "GET".to_string(), path: "/".to_string(), body: OptionalBody::Present("This is a body".into(), None, None),
      .. HttpRequest::default() };
    let request2 = HttpRequest { method: "GET".to_string(), path: "/".to_string(), query: Some(hashmap!{
        "QueryA".to_string() => vec![Some("Value A".to_string())]
        }), body: OptionalBody::Present("This is a body".into(), None, None),
      .. HttpRequest::default() };
    let request3 = HttpRequest { method: "GET".to_string(), path: "/".to_string(), query: Some(hashmap!{
        "QueryA".to_string() => vec![Some("Value A".to_string())]
        }), body: OptionalBody::Missing, .. HttpRequest::default() };
    let interaction = SynchronousHttp { description: "test".to_string(), request: request.clone(), .. SynchronousHttp::default() };
    let interaction2 = SynchronousHttp { description: "test2".to_string(), request: request2.clone(), .. SynchronousHttp::default() };
    let interactions = vec![interaction.boxed_v4(), interaction2.boxed_v4()];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request3, &pact).await;
    expect!(result).to(be_equal_to(MatchResult::RequestMismatch(interaction2.request, request3.clone(),
        vec![Mismatch::BodyMismatch { path: "/".to_string(), expected: Some("This is a body".into()), actual: None,
        mismatch: "Expected body \'This is a body\' but was missing".to_string() }])));
}

#[tokio::test]
async fn match_request_supports_v2_matchers() {
    let request = HttpRequest { method: "GET".to_string(), path: "/".to_string(),
        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), body: OptionalBody::Present(
            r#"
            {
                "a": 100,
                "b": "one hundred"
            }
            "#.into(), None, None
        ), .. HttpRequest::default() };
    let expected_request = HttpRequest { method: "GET".to_string(), path: "/".to_string(),
        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }),
        body: OptionalBody::Present(
            r#"
            {
                "a": 1000,
                "b": "One Thousand"
            }
            "#.into(), None, None
        ), matching_rules: matchingrules!{
          "body" => {
            "$.*" => [ MatchingRule::Type ]
          }
        },
      .. HttpRequest::default()
    };
    let interaction = SynchronousHttp { request: expected_request, .. SynchronousHttp::default() };
    let interactions = vec![interaction.boxed_v4()];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(
      MatchResult::RequestMatch(interaction.request, interaction.response, request.clone())));
}

#[tokio::test]
async fn match_request_supports_v2_matchers_with_xml() {
    let request = HttpRequest { method: "GET".to_string(), path: "/".to_string(), query: None,
        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/xml".to_string()] }), body: OptionalBody::Present(
            r#"<?xml version="1.0" encoding="UTF-8"?>
            <foo>hello<bar/>world</foo>
            "#.into(), None, None
        ), .. HttpRequest::default() };
    let expected_request = HttpRequest { method: "GET".to_string(), path: "/".to_string(), query: None,
        headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/xml".to_string()] }),
        body: OptionalBody::Present(
            r#"<?xml version="1.0" encoding="UTF-8"?>
            <foo>hello<bar/>mars </foo>
            "#.into(), None, None
        ), matching_rules: matchingrules!{
          "body" => {
            "$.foo['#text']" => [ MatchingRule::Regex("[a-z]+".into()) ]
          }
        },
      .. HttpRequest::default()
    };
    let interaction = SynchronousHttp { request: expected_request, .. SynchronousHttp::default() };
    let interactions = vec![interaction.boxed_v4()];
    let pact = V4Pact { interactions, .. V4Pact::default() };
    let result = match_request(&request, &pact).await;
    expect!(result).to(be_equal_to(
      MatchResult::RequestMatch(interaction.request, interaction.response, request.clone())));
}

#[test_log::test]
fn match_request_with_header_with_multiple_values() -> anyhow::Result<()> {
  let pact = V4Pact {
    interactions: vec![
      SynchronousHttp {
        request: HttpRequest {
          headers: Some(hashmap! {
            "accept".to_string() => vec!["application/hal+json".to_string(), "application/json".to_string()]
          }),
          .. HttpRequest::default()
        },
        .. SynchronousHttp::default()
      }.boxed_v4()
    ],
    .. V4Pact::default()
  };
  let mut manager = ServerManager::new();
  let id = "match_request_with_header_with_multiple_values".to_string();
  let mock_server_builder = MockServerBuilder::new()
    .with_v4_pact(pact)
    .with_id(id.clone())
    .bind_to("127.0.0.1:0");
  let result = manager.spawn_http_mock_server(mock_server_builder);
  let mock_server = result.unwrap();
  let port = mock_server.port();

  info!("Mock server port = {}", port);
  let client = reqwest::blocking::Client::new();
  let response = client.get(format!("http://127.0.0.1:{}", port).as_str())
    .header(ACCEPT, "application/hal+json, application/json").send();

  let mismatches = manager.find_mock_server_by_id(&id, &|_, ms| {
    ms.unwrap_left().mismatches()
  }).unwrap();
  manager.shutdown_mock_server_by_port(port);

  expect!(mismatches.iter()).to(be_empty());
  expect!(response.unwrap().status()).to(be_equal_to(200));

  Ok(())
}

#[tokio::test]
async fn match_request_with_more_specific_request() {
  let request1 = HttpRequest { path: "/animals/available".into(), .. HttpRequest::default() };
  let request2 = HttpRequest { path: "/animals/available".into(), headers: Some(hashmap! {
      "Authorization".to_string() => vec!["Bearer token".to_string()]
    }),
    .. HttpRequest::default() };
  let interaction1 = SynchronousHttp {
    description: "test_more_general_request".into(),
    request: request1.clone(),
    response: HttpResponse { status: 401, .. HttpResponse::default() },
    .. SynchronousHttp::default()
  };
  let interaction2 = SynchronousHttp {
    description: "test_more_specific_request".into(),
    request: request2.clone(),
    response: HttpResponse { status: 200, .. HttpResponse::default() },
    .. SynchronousHttp::default()
  };

  let expected = interaction1.clone();
  let interactions = vec![interaction1.boxed_v4(), interaction2.boxed_v4()];
  let pact = V4Pact { interactions, .. V4Pact::default() };
  let result1 = match_request(&request1.clone(), &pact).await;
  expect!(result1).to(be_equal_to(
    MatchResult::RequestMatch(expected.request, expected.response, request1.clone())));

  let expected = interaction2.clone();
  let result2 = match_request(&request2.clone(), &pact).await;
  expect!(result2).to(be_equal_to(
    MatchResult::RequestMatch(expected.request, expected.response, request2.clone())));
}

#[test_log::test]
#[cfg(feature = "plugins")]
fn basic_mock_server_test() -> anyhow::Result<()> {
  let pact = V4Pact {
    interactions: vec![
      SynchronousHttp {
        request: HttpRequest {
          headers: Some(hashmap! {
            "accept".to_string() => vec!["application/json".to_string()]
          }),
          .. HttpRequest::default()
        },
        .. SynchronousHttp::default()
      }.boxed_v4()
    ],
    .. V4Pact::default()
  };
  let id = "basic_mock_server_test".to_string();
  let addr = "127.0.0.1:0";

  let runtime = tokio::runtime::Builder::new_multi_thread()
    .enable_all()
    .build()
    .unwrap();

  let mut mock_server = runtime.block_on(MockServerBuilder::new()
      .with_v4_pact(pact)
      .with_id(id)
      .bind_to(addr)
      .with_transport("http")?
      .start())?;

  let port = mock_server.port();
  let client = reqwest::blocking::Client::new();
  let response = client.get(format!("http://127.0.0.1:{}", port).as_str())
    .header(ACCEPT, "application/json").send();

  let all_matched = mock_server.all_matched();
  let mismatches = mock_server.mismatches();
  mock_server.shutdown()?;

  expect!(all_matched).to(be_true());
  expect!(mismatches.is_empty()).to(be_true());
  expect!(response.unwrap().status()).to(be_equal_to(200));

  Ok(())
}