pact-foundation / pact-reference

Reference implementations for the pact specifications
https://pact.io
MIT License
91 stars 46 forks source link

Nested array-contains returns null in mock server - with possible fix #324

Closed diestrin closed 7 months ago

diestrin commented 1 year ago

Software versions

Related Issue

https://github.com/pact-foundation/pact-js/issues/841

Expected behaviour

Using array-contains matcher should work regardless of nesting level (e.g. array-contains including a nested property which is also matched by array-contains)

Actual behaviour

An array-contains nested inside another array-contains causes the mocked server to return null for the inner array.

Steps to reproduce

I created a test to reproduce the bug in https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/tests/tests.rs

fn array_contains_matcher() {
  let consumer_name = CString::new("array_contains_matcher-consumer").unwrap();
  let provider_name = CString::new("array_contains_matcher-provider").unwrap();
  let pact_handle = pactffi_new_pact(consumer_name.as_ptr(), provider_name.as_ptr());
  let description = CString::new("array_contains_matcher").unwrap();
  let interaction = pactffi_new_interaction(pact_handle.clone(), description.as_ptr());

  let content_type = CString::new("application/json").unwrap();
  let path = CString::new("/book").unwrap();
  let json = json!({
    "pact:matcher:type": "array-contains",
    "variants": [
      {
        "users": {
          "pact:matcher:type": "array-contains",
          "variants": [
            {
              "id": {
                "value": 1
              }
            },
            {
              "id": {
                "value": 2
              }
            },
          ]
        }
      },
    ]
  });
  let body = CString::new(json.to_string()).unwrap();
  let address = CString::new("127.0.0.1:0").unwrap();
  let method = CString::new("GET").unwrap();

  pactffi_upon_receiving(interaction.clone(), description.as_ptr());
  pactffi_with_request(interaction.clone(), method.as_ptr(), path.as_ptr());
  pactffi_with_body(interaction.clone(), InteractionPart::Response, content_type.as_ptr(), body.as_ptr());
  pactffi_response_status(interaction.clone(), 200);

  let port = pactffi_create_mock_server_for_pact(pact_handle.clone(), address.as_ptr(), false);

  expect!(port).to(be_greater_than(0));

  let client = Client::default();
  let result = client.get(format!("http://127.0.0.1:{}/book", port).as_str())
    .header("Content-Type", "application/json")
    .send();

  pactffi_cleanup_mock_server(port);

  match result {
    Ok(ref res) => {
      expect!(res.status()).to(be_eq(200));
    },
    Err(err) => {
      panic!("expected 200 response but request failed: {}", err);
    }
  };

  let json: Value = result.unwrap().json().unwrap();
  let users = json.as_array().unwrap().first().unwrap().as_object()
    .unwrap().get("users").unwrap();

  if users.is_null() {
    panic!("'users' field is null in JSON");
  }
}

Possible Cause

I found out the problem might be here https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/src/mock_server/bodies.rs#L111 where it process the MatchingRule::ArrayContains and sends true for the skip_matchers flag. This makes it not process the inner array-contains, nor actually any other matcher I suppose.

      let (value, skip_matchers) = if let Ok(rule) = &matching_rule {
        match rule {
          MatchingRule::ArrayContains(_) => (obj.get("variants"), true),
          _ => (obj.get("value"), false)
        }
      } else {
        (obj.get("value"), false)
      };

I felt tempted to submit a PR, but I am not sure if this behaviour is intended.

rholshausen commented 1 year ago

Thanks for the test!

I don't think this is due to the skip_matchers flag, but a bug where the values from the inner matcher are not being copied to the outer matcher. In particular, this semi-colon looks suspicious: image