pact-reference icon indicating copy to clipboard operation
pact-reference copied to clipboard

Matchers: Numeric matcher passing test sending String value

Open surpher opened this issue 9 months ago • 1 comments
trafficstars

🌎 Environment

  • Platform: pact_ffi
  • Version/Release: 0.4.25
  • Pact spec: v3, v4
  • FFI used:
bool pactffi_with_body(
   InteractionHandle interaction,
   enum InteractionPart part,
   const char *content_type,
   const char *body
);

💬 Description

Setting up unit tests for PactSwift verifying matchers in request body, specifically Numeric matcher. When executing the API request and Pact verification, verification succeeds with false positive:

2025-01-28T03:42:33.886570Z  INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
2025-01-28T03:42:33.886572Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: 
          ----------------------------------------------------------------------------------------
           status: 200
           headers: None
           body: Missing
          ----------------------------------------------------------------------------------------
          
2025-01-28T03:42:33.886586Z TRACE tokio-runtime-worker encode_headers: hyper::proto::h1::role: Server::encode status=200, body=None, req_method=Some(POST)

🦶 Reproduction Steps

Steps to reproduce the behaviour:

  1. Setup a pact interaction that expects request body with Numeric value, ie
"{\"value\”:{
   \"key2\":{\"value\":321,\"pact:matcher:type\":\"number\”},
   \"key1\”:{\"pact:matcher:type\":\"number\",\"value\":123.1}},
   \"pact:matcher:type\":\"type\”
}"
  1. Prepare an encodable type Body(key1: String, key2: String) that takes strings for property’s values,
  2. Send a POST request with the Body type and string values,
  3. Verify pact interaction,

🤔 Expected Results

Pact test should fail due to invalid type being passed for the body keys’ values.

😲 Actual Results

Pact test succeeds with Request matched, sending response

----------------------------------------------------------------------------------------
           status: 200
           headers: None
           body: Missing
----------------------------------------------------------------------------------------

🌳 Logs

...
2025-01-28T04:09:13.515623Z TRACE ThreadId(02) pact_ffi::mock_server::handles: with_interaction - inner = PactHandleInner { pact: V4Pact { consumer: Consumer { name: "Consumer" }, provider: Provider { name: "Provider" }, interactions: [SynchronousHttp { id: None, key: None, description: "a request for a known path with a body matching number", provider_states: [], request: HttpRequest { method: "POST", path: "/jsonbody", query: None, headers: Some({"Content-Type": ["application/json"]}), body: Present(b"{\"key1\":123.1,\"key2\":321}", Some(ContentType { main_type: "application", sub_type: "json", attributes: {}, suffix: None }), None), matching_rules: MatchingRules { rules: {PATH: MatchingRuleCategory { name: PATH, rules: {} }, BODY: MatchingRuleCategory { name: BODY, rules: {DocPath { path_tokens: [Root, Field("key2")], expr: "$.key2" }: RuleList { rules: [Number], rule_logic: And, cascaded: false }, DocPath { path_tokens: [Root, Field("key1")], expr: "$.key1" }: RuleList { rules: [Number], rule_logic: And, cascaded: false }, DocPath { path_tokens: [Root], expr: "$" }: RuleList { rules: [Type], rule_logic: And, cascaded: false }} }} }, generators: Generators { categories: {} } }, response: HttpResponse { status: 200, headers: None, body: Missing, matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } }, comments: {}, pending: false, plugin_config: {}, interaction_markup: InteractionMarkup { markup: "", markup_type: "" }, transport: None }], metadata: {"namespace1": Object {"name1": String("value1")}, "namespace2": Object {"name2": String("value2")}, "pactRust": Object {"ffi": String("0.4.25")}}, plugin_data: [] }, mock_server_started: false, specification_version: V4 }

…

2025-01-28T04:09:54.935771Z  INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request POST /jsonbody
2025-01-28T04:09:54.935775Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: 
      ----------------------------------------------------------------------------------------
       method: POST
       path: /jsonbody
       query: None
       headers: Some({"content-length": ["29"], "accept-encoding": ["gzip", "deflate"], "accept": ["*/*"], "accept-language": ["en-AU", "en;q=0.9"], "user-agent": ["xctest/23600 CFNetwork/1568.300.101 Darwin/24.2.0"], "host": ["127.0.0.1:4516"], "content-type": ["application/json"], "connection": ["keep-alive"]})
       body: Present(29 bytes, application/json) '{"key2":"456","key1":"321.1"}'
      ----------------------------------------------------------------------------------------

…

2025-01-28T04:09:54.935896Z DEBUG tokio-runtime-worker pact_matching: comparing to expected HTTP Request ( method: POST, path: /jsonbody, query: None, headers: Some({"Content-Type": ["application/json"]}), body: Present(25 bytes, application/json) )
2025-01-28T04:09:54.935904Z DEBUG tokio-runtime-worker pact_matching:      body: '{"key1":123.1,"key2":321}'
2025-01-28T04:09:54.935907Z DEBUG tokio-runtime-worker pact_matching:      matching_rules: MatchingRules { rules: {PATH: MatchingRuleCategory { name: PATH, rules: {} }, BODY: MatchingRuleCategory { name: BODY, rules: {DocPath { path_tokens: [Root, Field("key2")], expr: "$.key2" }: RuleList { rules: [Number], rule_logic: And, cascaded: false }, DocPath { path_tokens: [Root, Field("key1")], expr: "$.key1" }: RuleList { rules: [Number], rule_logic: And, cascaded: false }, DocPath { path_tokens: [Root], expr: "$" }: RuleList { rules: [Type], rule_logic: And, cascaded: false }} }} }

…

2025-01-28T04:09:54.962229Z DEBUG tokio-runtime-worker pact_matching: --> Mismatches: []
2025-01-28T04:09:54.962254Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Test context = {"mockServer": Object {"port": Number(4516), "url": String("http://127.0.0.1:4516")}}
2025-01-28T04:09:54.962257Z TRACE tokio-runtime-worker pact_matching: generate_response response=HttpResponse { status: 200, headers: None, body: Missing, matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } } mode=Consumer context={"mockServer": Object {"port": Number(4516), "url": String("http://127.0.0.1:4516")}}
2025-01-28T04:09:54.962262Z  INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
2025-01-28T04:09:54.962521Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: 
          ----------------------------------------------------------------------------------------
           status: 200
           headers: None
           body: Missing
          ----------------------------------------------------------------------------------------
          
2025-01-28T04:09:54.962539Z TRACE tokio-runtime-worker encode_headers: hyper::proto::h1::role: Server::encode status=200, body=None, req_method=Some(POST)
2025-01-28T04:09:54.962570Z DEBUG tokio-runtime-worker hyper::proto::h1::io: flushed 319 bytes

….

📄 Stack Traces

See attached false-positive-numeric-body-value for full standardOut: .trace log:

false-positive-numeric-body-value.log

🤝 Relationships

  • Related PRs or Issues:

surpher avatar Jan 28 '25 04:01 surpher

This could also be related:

https://github.com/pact-foundation/pact-reference/issues/298

There are places where it does implicit conversions between strings and numbers if the number looks like a string, if I remember correctly. It's caused problems in PactNet before in the past also.

Your example is sending the body:

body: Present(29 bytes, application/json) '{"key2":"456","key1":"321.1"}'

Note how the values "look like" numbers, even though they're clearly strings. I suspect that it's getting the same implicit conversion behaviour as in the linked issue that I raised.

adamrodger avatar Mar 23 '25 18:03 adamrodger