spring-cloud-contract icon indicating copy to clipboard operation
spring-cloud-contract copied to clipboard

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

Open virgium03 opened this issue 4 years ago • 10 comments

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 avatar Mar 17 '22 14:03 virgium03

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

virgium03 avatar Mar 17 '22 14:03 virgium03

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

marcingrzejszczak avatar Apr 11 '22 12:04 marcingrzejszczak

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.

virgium03 avatar Apr 14 '22 12:04 virgium03

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

marcingrzejszczak avatar Apr 15 '22 11:04 marcingrzejszczak

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 avatar Apr 22 '22 11:04 spring-cloud-issues

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 avatar Apr 28 '22 14:04 virgium03

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 avatar Apr 28 '22 14:04 virgium03

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 avatar Apr 28 '22 14:04 virgium03

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

virgium03 avatar Jun 16 '22 14:06 virgium03

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

marcingrzejszczak avatar Jun 16 '22 15:06 marcingrzejszczak

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

marcingrzejszczak avatar Dec 13 '23 15:12 marcingrzejszczak

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 avatar Dec 20 '23 15:12 spring-cloud-issues

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.

spring-cloud-issues avatar Dec 27 '23 15:12 spring-cloud-issues

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));
    }

belaicher-abdelhadi avatar Mar 20 '24 14:03 belaicher-abdelhadi