spring-cloud / spring-cloud-contract

Support for Consumer Driven Contracts in Spring
https://cloud.spring.io/spring-cloud-contract
Apache License 2.0
720 stars 440 forks source link

No consistent date format between the real application, contract tests and generated stub mappings #1760

Closed virgium03 closed 10 months ago

virgium03 commented 2 years ago

Hello,

I'm using Spring Boot 2.6.3 and Spring Cloud Contract 3.1.0 and Jackson 2.11.3 when writing contract ttests. This issue might be possibly related to the Maven plugin.

I am having troubles when dealing with java.util.Date objects when writing contract tests using your wonderful library.

First of all, I noticed that the default documented setup withRestAssuredWebTestClient.standaloneSetup(controller) does not use the Jackson Object Mapper of the application context. I was unpleasantly surprised to find out that the real application was generating java.util.Date objects using the ISO 8601 offset format (2022-03-17T10:34:11.815+00:00) but the application (context) used by the tests was generating the date as timestamp format (1641551651813). I would say that here, when dealing with dates, there is a big possibility of not being aligned with what the real application would produce. I did not test the behaviour with the new Java 8 date time classes.

Please note that the real application has no additional Jackson or JSON configuration for Spring Boot.

I was able to overcome the issue by using something along the lines of manually injecting an ObjectMapper, creating an HTTP message converter and setting it on an instance of MockMvc. Maybe the documentation should stress more that the MockMvc configuration does not necessarily match the real one or give more details about how to configure it.

@SpringBootTest(classes = {MyConfiguration.class, ActorsController.class})
@AutoConfigureJson
@AutoConfigureJsonTesters
public class BaseContractTestClass {

    @Autowired
    private ActorsController actorsController;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void setup() {        
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new
                MappingJackson2HttpMessageConverter();
        mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper);
        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(actorsController)
                .setMessageConverters(mappingJackson2HttpMessageConverter)
                .build();
        RestAssuredMockMvc.mockMvc(mockMvc);
    }

}

In this way I was able to assert in a contract file that the date format matches a predefined format iso_8601_with_offset using dynamic body matchers in a yml file.

But then, when running the tests on the client consumer side, I noticed that the JSON which the client sees has the dates still in the timestamp format. I checked the generated stubs and their mappings and, indeed, the dates are not using the iso_8601_with_offset format. I don't understand why and I could not find where the problem comes from.

Once again, I would like to point out that there is no special JSON Spring Boot configuration in the application, just the default one, as far as I'm aware.

Any ideas why and how to get the dates in the same format, in the contract tests on the producer and the generated stubs used by the consumer?

Many thanks, Virgiliu

virgium03 commented 2 years ago

Jackson jackson-datatype-jdk8 and jackson-datatype-jsr310 are transitive dependencies from spring-boot-starter-json.

marcingrzejszczak commented 2 years ago

AFAIR the best way would be to pass the web application context via RestAssuredMockMvc.webAppContextSetup. That way things should be resolved from application context instead of passing them manually

virgium03 commented 2 years ago

AFAIR the best way would be to pass the web application context via RestAssuredMockMvc.webAppContextSetup. That way things should be resolved from application context instead of passing them manually

In the end, i simplified the base class like this:

@WebMvcTest
@TestPropertySource("/application-test.properties")
@Import(xxx.class)
public class BaseContractTestClass {

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setup() {       
        RestAssuredMockMvc.mockMvc(mockMvc);
    }

}

But that was not the issue that I was referring to.

The problem is in the JSON stubs which are generated as a jar file for the clients, more precisely the JSON response body under the mappings folder. There the java.util.Date objects are serialized as timestamps, which is not consistent with the format used by the server (controllers), which is the iso_8601_with_offset format. From what I've seen, these are the responses that the client will get from the Wiremock server and they do not match the actual responses received by a client on the real server.

marcingrzejszczak commented 2 years ago

Can you show the contract, the generated test and the WireMock mapping?

spring-cloud-issues commented 2 years ago

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

virgium03 commented 2 years ago

Here is the contract file:

name: search actors using criteria
request:
  method: GET
  url: /secure/v1.0/actors
  queryParameters:
    uuId: nsupuser

response:
  status: 200
  body:
    - id: 1
      uuId: nsupuser
      lastReactivationDate: 2022-01-07T10:34:11.813+00:00
      recUserId: hodor
      recDate: 2022-01-07T10:34:11.815+00:00
      modUserId: hodor
      modDate: 2022-03-10T13:45:44.097+00:00
  headers:
    Content-Type: application/json
  matchers:
    body:
      - path: $.[0].lastReactivationDate
        type: by_regex
        predefined: iso_8601_with_offset
      - path: $.[0].recDate
        type: by_regex
        predefined: iso_8601_with_offset
      - path: $.[0].modDate
        type: by_regex
        predefined: iso_8601_with_offset
virgium03 commented 2 years ago

here is the generated producer test:

import identity.domain.rest.server.IdentityRestBaseContractTestClass;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import io.restassured.response.ResponseOptions;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;

@SuppressWarnings("rawtypes")
public class ActorsTest extends IdentityRestBaseContractTestClass {

        @Test
    public void validate_search_actors_using_criteria() throws Exception {
        // given:
            MockMvcRequestSpecification request = given();

        // when:
            ResponseOptions response = given().spec(request)
                    .queryParam("uuId","nsupuser")
                    .get("/secure/v1.0/actors");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).isEqualTo("application/json");

        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).array().contains("['id']").isEqualTo(1);
            assertThatJson(parsedJson).array().contains("['uuId']").isEqualTo("nsupuser");
            assertThatJson(parsedJson).array().contains("['recUserId']").isEqualTo("hodor");
            assertThatJson(parsedJson).array().contains("['modUserId']").isEqualTo("hodor");

        // and:
            assertThat(parsedJson.read("$.[0].lastReactivationDate", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.\\d+)?(Z|[+-][01]\\d:[0-5]\\d)");
            assertThat(parsedJson.read("$.[0].recDate", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.\\d+)?(Z|[+-][01]\\d:[0-5]\\d)");
            assertThat(parsedJson.read("$.[0].modDate", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.\\d+)?(Z|[+-][01]\\d:[0-5]\\d)");
    }
}
virgium03 commented 2 years ago

and the generated json stub:

{
  "id" : "114d2908-ecd5-4d51-af4c-3e4ffb360fd8",
  "request" : {
    "urlPath" : "/secure/v1.0/actors",
    "method" : "GET",
    "queryParameters" : {
      "uuId" : {
        "equalTo" : "nsupuser"
      }
    }
  },
  "response" : {
    "status" : 200,
    "body" : "[{\"id\":1,\"uuId\":\"nsupuser\",\"lastReactivationDate\":1641551651813,\"recUserId\":\"hodor\",\"recDate\":1641551651815,\"modUserId\":\"hodor\",\"modDate\":1646919944097}]",
    "headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template", "spring-cloud-contract" ]
  },
  "uuid" : "114d2908-ecd5-4d51-af4c-3e4ffb360fd8"
}
virgium03 commented 2 years ago

is anybody taking a look at this? it's almost two months since the last update.

marcingrzejszczak commented 2 years ago

I didn't have time but if you're willing to help PRs are more than welcome!

marcingrzejszczak commented 11 months ago

Is this still a problem? Can we ask for a project that replicates this issue?

spring-cloud-issues commented 11 months ago

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

spring-cloud-issues commented 10 months ago

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.

belaicher-abdelhadi commented 8 months ago

I'm facing an issue like this for an upgrade feature and I'm able to fix this by spying jackson bean like this :

    @SpyBean
    private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;

    @BeforeEach
    void setup() {
        RestAssuredMockMvc.standaloneSetup(MockMvcBuilders.webAppContextSetup(applicationContext));
    }