cornichon icon indicating copy to clipboard operation
cornichon copied to clipboard

JSON matcher *any-object* is misinterpreted

Open seugnimod opened this issue 3 years ago • 9 comments

Hi there, thanks for your work! I am using JSON matchers extensively in my application and I realised that in 2 HTTP responses with exactly the same JSON object (within another object), in a test I can match it with *any-object*, but in another test, I have to fully describe it.

In both tests i am using "Then assert body.is(...)"

The failing test tells me: "matcher any-string (checks if the field is a String) failed for input (...)"

I have tried to match with object, so why the test fails to match with string ?

This wouldn't be so weird if it never worked, but as i said, i works in another test, that tests exactly the same object. Any help would be very much appreciated

seugnimod avatar Jul 19 '22 08:07 seugnimod

Hi @seugnimod,

Thanks for the report!

This looks very weird, would it be possible for you to share some of the body assertions and JSON payloads?

agourlay avatar Jul 19 '22 09:07 agourlay

Hi, thanks for the quick answer!

JSONs in the tests

{
	"data": 
	{
		"attributes": {
		  "_kind": "urn:api:personnes",
		  "numeroPersonne": $clientId,
		  "adresseCorrespondance" : *any-object*,
		  "dematerialisation": *any-boolean*,
		  "securitesSociales": {
			"LU": {
			  "_kind": *any-string*,
			  "matriculeSocial": *any-string*
			}
		  }
		},
		"relationships" : { 
			...
		},
	}
}
{
	"data": 
	{
		"attributes": {
		  "_kind": "urn:api:personnes",
		  "numeroPersonne": $clientId,
		  "adresseCorrespondance" : *any-object*,
		  "dematerialisation": *any-boolean*,
		  "securitesSociales": *any-object*
		},
		"relationships" : { 
			...
		},
	}
}

JSON in the request

{
	...,
	"securitesSociales": {
        "LU": {
          "_kind": "urn:api:securite-sociale",
          "matriculeSocial": "00000000000"
        }
      },
	  ....
}

I have trimmed a bit because there is a lot of personal data, not sure if this helps at all.

So, In a test that parses correctly, I use the JSON 1 to compare with the response. The JSON 2, also parses correctly, in another test, with different data, but the part "securitesSociales" is exactly the same. If in the JSON 1 I try to use "securitesSociales": *any-object* instead, it doesn't work, showing the error above. (matcher any-string (checks if the field is a String) failed for input (...) )

To me it is only plausible that the issue comes from this object in the JSON, because if I describe de object, I no longer have parsing issues.

I also tried moving "securitesSociales" up and down in the JSON, but it doesn't affect the result.

I am using cornichon 0.20.3, with scala 2.11.12

Does this help at all ? Thanks in advance !

seugnimod avatar Jul 19 '22 11:07 seugnimod

Thanks for sharing your data :+1:

Is it possible to share the exact error as well?

agourlay avatar Jul 19 '22 12:07 agourlay

yes !

Then assert response body is [8 ms]

{
 Object JSON I compare my response with. This body contains  "securitesSociales": *any-object*
}

 *** FAILED ***
      matcher 'any-string' (checks if the field is a String) failed for input '{
        "LU" : {
          "_kind" : "urn:api:securite-sociale",
          "matriculeSocial" : "000000000000"
        }
      }'
      

A little bit anonymized :)

seugnimod avatar Jul 19 '22 13:07 seugnimod

Thanks, I think I have an idea :+1:

agourlay avatar Jul 19 '22 15:07 agourlay

I have a hunch regarding a possibly broken invariant in the MatcherResolver.

I have created a PR to harden it with https://github.com/agourlay/cornichon/pull/677

You can give it a try via 0.20.4-SNAPSHOT which corresponds to the current master branch plus the aforementioned PR.

You might need to add the SBT resolver for https://oss.sonatype.org.

Give it a try and tell me how it goes!

agourlay avatar Jul 20 '22 18:07 agourlay

@seugnimod pinging you gently about testing the snapshot :innocent:

agourlay avatar Aug 09 '22 08:08 agourlay

Hello @agourlay , I'm sorry for not having responded. Project priorities didn't let me test it yet, but hey, today or tomorrow i'm supposed to test a bit and Ill keep you informed, thanks ! Also, I had this error while quickly testing the latest version: [error] (integration / IntegrationTest / executeTests) java.lang.UnsupportedClassVersionError: com/github/benmanes/caffeine/cache/Caffeine

We come from v.0.19.3, and I saw something caffeine related in the breaking changes 0.19.7 Compatibility note Java 11 or above is required following the upgrade of caffeine in version 3.0

This is probably the reason, no? Thanks !

seugnimod avatar Aug 09 '22 08:08 seugnimod

No worries, I did not mean to pressure you :+1:

We come from v.0.19.3,

That's quite the upgrade jump you have got there!

Compatibility note

You are correct about the breaking change due to Caffeine. I hope you will be able to migrate.

agourlay avatar Aug 09 '22 10:08 agourlay

@seugnimod another gentle ping to know how your JDK migration upgrade is doing :)

FYI I am thinking about merging the fix PR #677 and performing a new release soon anyway as I believe it is an overall good patch.

agourlay avatar Sep 08 '22 10:09 agourlay

Hey there @agourlay , sorry for the long wait. Never had the time to check on your solution. Today I tried the 0.20.4 and I can still reproduce the issue. Is there any other thing I can provide you? For info, it is not related with being the last object in the "father object". I moved it to between 2 other items, and still have the issue.

So, i have in my matcher "securitesSociales": *any-object*,

The response is:

        "LU": {
          "_kind": "urn:api:referentiel:personnes:view:securite-sociale",
          "matriculeSocial": "999999999999"
        }
      },

And the error i'm having

[info]   with error(s):
[info]   matcher 'any-string' (checks if the field is a String) failed for input '{
[info]     "LU" : {
[info]       "_kind" : "urn:api:referentiel:personnes:view:securite-sociale",
[info]       "matriculeSocial" : "999999999999"
[info]     }
[info]   }'

seugnimod avatar Sep 12 '22 17:09 seugnimod

Happy to hear back from you, that bug is really a mystery, very interesting!

The best for me would be able to reproduce locally without any networking involved.

I propose the following trick, you can write in the session your result body and assert it with the usual DSL.

package com.github.agourlay.cornichon.framework.examples

import com.github.agourlay.cornichon.CornichonFeature
import com.github.agourlay.cornichon.http.HttpService.SessionKeys.lastResponseBodyKey

class Repro extends CornichonFeature {
  override def feature = Feature("feature bug repro") {
    Scenario("Scenario bug repro") {
      Given I save((
        lastResponseBodyKey,
        """
          |{
          |	"attributes": {
          |	    "_kind": "urn:api:personnes",
          |	    "numeroPersonne": 123,
          |	    "adresseCorrespondance" : "some_addresse_correspondance",
          |	    "dematerialisation": false,
          |	     "securitesSociales": {
          |		 "LU": {
          |		     "_kind": "some_kind",
          |		      "matriculeSocial": "some_matricule_social"
          |	          }
          |	     }
          |     }
          |}
          |""".stripMargin))

      And I show_last_body

      And assert body.is(
        """
          {
          	"attributes": {
          	  "_kind": "urn:api:personnes",
          	  "numeroPersonne": 123,
          	  "adresseCorrespondance" : "some_addresse_correspondance",
          	  "dematerialisation": false,
          	  "securitesSociales": {
          		  "LU": {
          		    "_kind": "some_kind",
          		    "matriculeSocial": "some_matricule_social"
          		  }
          	  }
            }
          }
          """)

      And assert body.is(
        """
          {
            "attributes": {
                "_kind": "urn:api:personnes",
                "numeroPersonne": 123,
                "adresseCorrespondance" : "some_addresse_correspondance",
                "dematerialisation": *any-boolean*,
                "securitesSociales": *any-object*
            }
          }
          """)
    }
  }
}

That scenario is green for me.

It would awesome if you could tweak the payloads and assertions in there to reproduce the bug and share it with me. This way you also easily anonymize the fields.

If you cannot reproduce the issue this way, it means the error is more complex.

agourlay avatar Sep 12 '22 18:09 agourlay

I am terribly ashamed and a bit angry....

The fault is in my assertion, there is a duplicated key that a colleague of mine added. I still share it with you, reduced it as much as I could to reproduce the issue.

I still think that it is interesting. I was mislead to the "securitesSociales", when actually the issue was with "langueCorrespondance" all along

This reproduces the issue

package personne.independant

import com.github.agourlay.cornichon.CornichonFeature
import com.github.agourlay.cornichon.core.FeatureDef
import com.github.agourlay.cornichon.http.HttpService.SessionKeys.lastResponseBodyKey

class Repro extends CornichonFeature {
  override def feature: FeatureDef = Feature("feature bug repro") {
    Scenario("Scenario bug repro") {

      print_step(s"testing CT view")

      Given I save((lastResponseBodyKey, ctStub()))

      Then assert body.is(ctView())

    }
  }

  def ctView(): String =
    s"""
       |{
       |   "langueCorrespondance":{
       |      "code":*any-string*,
       |      "label":*any-string*
       |   },
       |   "langueCorrespondance":{
       |      "code":*any-string*,
       |      "label":*any-string*
       |   },
       |   "_kind":"urn:api:personnes:view",
       |   "securitesSociales":*any-object*
       |}
       |""".stripMargin

  def ctStub(): String =
    s"""
       |{
       |   "_kind":"urn:api:personnes:view",
       |   "langueCorrespondance":{
       |      "code":"fr",
       |      "label":"Français"
       |   },
       |   "securitesSociales":{
       |      "LU":{
       |         "_kind":"urn:api:personnes:view:securite-sociale",
       |         "matriculeSocial":"9999999999999"
       |      }
       |   }
       |}
       |""".stripMargin
}


Some thing I found funny however, is if I try this, I am all green, when I assume I shouldn't,

package personne.independant

import com.github.agourlay.cornichon.CornichonFeature
import com.github.agourlay.cornichon.core.FeatureDef
import com.github.agourlay.cornichon.http.HttpService.SessionKeys.lastResponseBodyKey

class Repro extends CornichonFeature {
  override def feature: FeatureDef = Feature("feature bug repro") {
    Scenario("Scenario bug repro") {

      print_step(s"testing CT view")

      Given I save((lastResponseBodyKey, ctStub()))

      Then assert body.is(ctView())

    }
  }

  def ctView(): String =
    s"""
       |{
       |   "langueCorrespondance":{
       |      "code":*any-string*,
       |      "label":*any-string*
       |   },
       |   "langueCorrespondance":{
       |      "code":*any-string*,
       |      "label":*any-string*
       |   },
       |   "_kind":"urn:api:personnes:view",
       |   "securitesSociales":{
       |      "LU":{
       |         "_kind":"urn:api:personnes:view:securite-sociale",
       |         "matriculeSocial":"9999999999999"
       |      }
       |   }
       |}
       |""".stripMargin

  def ctStub(): String =
    s"""
       |{
       |   "_kind":"urn:api:personnes:view",
       |   "langueCorrespondance":{
       |      "code":"fr",
       |      "label":"Français"
       |   },
       |   "securitesSociales":{
       |      "LU":{
       |         "_kind":"urn:api:personnes:view:securite-sociale",
       |         "matriculeSocial":"9999999999999"
       |      }
       |   }
       |}
       |""".stripMargin
}

I thank you very much for your help, and I'm really sory for the time you spent :(

seugnimod avatar Sep 13 '22 16:09 seugnimod

Hey @seugnimod, happy to see some progress here!

I am terribly ashamed and a bit angry....

No need to worry, the outcome of this issue will mostly likely be at least an improvement to the error reporting. There seems to be rough edges regarding duplicate keys in JSON objects.

I want this library to have a really smooth UX which is the opposite of leaving users confused by error messages for days :)

The first snippet your shared is green for me

feature bug repro:
Starting scenario 'Scenario bug repro'
- Scenario bug repro [178 ms]

   Scenario : Scenario bug repro
      main steps
      testing CT view
      Given I add value ' [302 μs]
      {
         "_kind":"urn:api:personnes:view",
         "langueCorrespondance":{
            "code":"fr",
            "label":"Français"
         },
         "securitesSociales":{
            "LU":{
               "_kind":"urn:api:personnes:view:securite-sociale",
               "matriculeSocial":"9999999999999"
            }
         }
      }
      ' to session under key 'last-response-body'
      Then assert response body is [99 ms]
      
      {
         "langueCorrespondance":{
            "code":*any-string*,
            "label":*any-string*
         },
         "langueCorrespondance":{
            "code":*any-string*,
            "label":*any-string*
         },
         "_kind":"urn:api:personnes:view",
         "securitesSociales":*any-object*
      }
  replay only this scenario with the command:
  testOnly *Repro -- "Scenario bug repro" "--seed=1663094617644"
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1

Is it supposed to fail? Which version of cornichon are you using there?

The second snippet is also green as you mentioned.

feature bug repro:
Starting scenario 'Scenario bug repro'
- Scenario bug repro [162 ms]

   Scenario : Scenario bug repro
      main steps
      testing CT view
      Given I add value ' [300 μs]
      {
         "_kind":"urn:api:personnes:view",
         "langueCorrespondance":{
            "code":"fr",
            "label":"Français"
         },
         "securitesSociales":{
            "LU":{
               "_kind":"urn:api:personnes:view:securite-sociale",
               "matriculeSocial":"9999999999999"
            }
         }
      }
      ' to session under key 'last-response-body'
      Then assert response body is [84 ms]
      
      {
         "langueCorrespondance":{
            "code":*any-string*,
            "label":*any-string*
         },
         "langueCorrespondance":{
            "code":*any-string*,
            "label":*any-string*
         },
         "_kind":"urn:api:personnes:view",
         "securitesSociales":{
            "LU":{
               "_kind":"urn:api:personnes:view:securite-sociale",
               "matriculeSocial":"9999999999999"
            }
         }
      }
  replay only this scenario with the command:
  testOnly *Repro -- "Scenario bug repro" "--seed=1663095418097"

Some thing I found funny however, is if I try this, I am all green, when I assume I shouldn't,

I agree that it is surprising to see the test green because the body does not in fact contain a duplicated langueCorrespondance key.

I did a quick investigation and it appears Circe uses the Jawn parser with allowDuplicateKeys https://github.com/circe/circe/blob/f15be0dfc2706ffc227f99ec00f0e681a5ca467e/modules/jawn/shared/src/main/scala/io/circe/jawn/JawnParser.scala#L54

It could be interesting to add a configuration knob to cornichon to enable users to disallow duplicate keys. The matchers logic also needs some special treatment of duplicate keys.

agourlay avatar Sep 13 '22 19:09 agourlay

Hi there !

Is it supposed to fail? Which version of cornichon are you using there?

Yes, it was supposed to fail, with the error I pointed out earlier

      *** FAILED ***
      matcher 'any-string' (checks if the field is a String) failed for input '{
        "LU" : {
          "_kind" : "urn:api:personnes:view:securite-sociale",
          "matriculeSocial" : "9999999999999"
        }
      }'

It's red on my side. I am using com.github.agourlay:cornichon-core_2.12:0.20.4:jar com.github.agourlay:cornichon-kafka_2.12:0.20.4:jar com.github.agourlay:cornichon-scalatest_2.12:0.19.3:jar with scala 2.12.10

Can I provide you some more info to help you ?

seugnimod avatar Sep 14 '22 12:09 seugnimod

com.github.agourlay:cornichon-scalatest_2.12:0.19.3:jar

This needs to be 0.20.4 as well, all artifacts are released together.

agourlay avatar Sep 14 '22 12:09 agourlay

Hello! I'd like to give you a little update. Excuse-me, about the versions, that's my fault, I had no requirement for cornichon-scalatest and I thought there would be an internal dependance between them if needed. Yes, it is now green for me the first snippet also ;) Thanks for your support !

seugnimod avatar Sep 19 '22 09:09 seugnimod

Happy to hear this!

The mystery is not entirely solved as I was never able to reproduce the issue but the most important is that it works for you now.

To avoid having someone else struggle like we did, I will try improving:

  • the error reporting for matcher failures (https://github.com/agourlay/cornichon/commit/52e15a0ea3ef128a50722d5a06136efa0cfb9ede)
  • the experience of having JSON duplicate keys via a configuration knob
  • the detection of duplicate matchers

Out of curiosity, what is preventing you to upgrade to Scala 2.13? You could try bumping to the freshly released 2.12.17 as well.

agourlay avatar Sep 19 '22 10:09 agourlay

Actually, we have a fork of play 2.5 and that wouldn't work for us. I have to push the Core team to get up to date with that, and also with the version of your lib, cause we use currently the 0.19.3, but I want all the recent goodies :)

seugnimod avatar Sep 19 '22 13:09 seugnimod