pact-foundation / pact-jvm

JVM version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
https://docs.pact.io
Apache License 2.0
1.08k stars 479 forks source link

Encoding error when contract generating for request with binary body (for images) #1110

Open weofferservice opened 4 years ago

weofferservice commented 4 years ago

When request has binary body (for images) contract is generating to JSON in UTF-8 encoding. This is error, because UTF-8 irreversible. You must use Base64 or ISO_8859_1 I found this bug and fix it. I made pull request to fix this error (https://github.com/DiUS/pact-jvm/pull/1111).

I attached to this Issue my test data which helped me to find bug: 1) face-with-smile.jpg 2) PhotoQualityOkEndpointTest.java

package com.example;

import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.annotations.Pact;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.api.Assertions;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;

import static com.example.PactFisengineConstants.*;

@ExtendWith({PactConsumerTestExt.class, SpringExtension.class})
@ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class)
@PactTestFor(providerName = PROVIDER, hostInterface = "localhost", port = "8080")
public class PhotoQualityOkEndpointTest {

    @Value("${cs.service.fisEngine.photoQualityEndpoint:#{new Object()}}") // mandatory value
    private String photoQualityEndpoint;

    private static byte[] photoQualityOkRequestBody;
    private static String photoQualityOkResponseBody;

    static {
        try {
            photoQualityOkRequestBody = PhotoQualityOkEndpointTest.class.getResourceAsStream("/photos/face-with-smile.jpg").readAllBytes();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }

        try {
            photoQualityOkResponseBody = new String(
                    PhotoQualityOkEndpointTest.class.getResourceAsStream("/fis-responses/picture-checker-ok.json").readAllBytes(),
                    StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    @Pact(consumer = CONSUMER)
    public RequestResponsePact createPhotoQualityEndpointPact(PactDslWithProvider builder) throws JSONException, IOException {
        photoQualityEndpoint = "/" + StringUtils.strip(photoQualityEndpoint, "/");

        PactDslJsonBody pactDslOkJsonBody = new PactDslJsonBody();
        JSONObject jsonOkObject = new JSONObject(photoQualityOkResponseBody);
        ReflectionTestUtils.setField(pactDslOkJsonBody, "body", jsonOkObject);
        pactDslOkJsonBody.stringMatcher("version", ".+", "1");

        return builder

                // Request
                .given(PHOTO_QUALITY_STATE)
                .uponReceiving("Photo Quality Endpoint - OK")
                .path(photoQualityEndpoint)
                .withFileUpload(
                        "photo",
                        "face-with-smile.jpg",
                        MediaType.IMAGE_JPEG_VALUE,
                        photoQualityOkRequestBody)
                .method(HttpMethod.POST.name())

                // Response
                .willRespondWith()
                .status(HttpStatus.OK.value())
                .headers(
                        Map.ofEntries(
                                Map.entry(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
                        )
                )
                .body(pactDslOkJsonBody)

                .toPact();
    }

    @Test
    public void photoQuality(MockServer mockServer) throws JSONException {
        //Given
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);

        RestTemplate restTemplate = new RestTemplate();

        JSONObject expectedResponseBody =
                new JSONObject()
                        .put("version", "1")
                        .put("issues", new JSONArray())
                        .put("errorMessage", "")
                        .put("status", 0);

        MultiValueMap<String, Object> body
                = new LinkedMultiValueMap<>();
        body.add("photo", new ByteArrayResource(photoQualityOkRequestBody) {
            @Override
            public String getFilename() {
                return "face-with-smile.jpg"; // to set Content-Type header to "image/jpeg"
            }
        });

        //When
        ResponseEntity<String> response = restTemplate
                .postForEntity(
                        mockServer.getUrl() + photoQualityEndpoint,
                        new HttpEntity<>(body, httpHeaders),
                        String.class);

        //Then
        Assertions.assertThat(response)
                .isNotNull();
        Assertions.assertThat(response.getStatusCode())
                .isEqualTo(HttpStatus.OK);
        Assertions.assertThat(response.getHeaders().containsKey(HttpHeaders.CONTENT_TYPE))
                .isTrue();
        Assertions.assertThat(response.getHeaders().get(HttpHeaders.CONTENT_TYPE))
                .contains(MediaType.APPLICATION_JSON_UTF8_VALUE);

        JSONAssert.assertEquals(
                expectedResponseBody.toString(),
                response.getBody(),
                true);
    }

}

3) picture-checker-ok.json

{
  "issues": [],
  "status": 0,
  "errorMessage": "",
  "version": "14.0.0.119"
}
uglyog commented 4 years ago

There is another problem, the multipart body is not being base64 encoded before being written to the pact file

uglyog commented 4 years ago

I think this has been addressed now. If you run PostImageBodyTest.groovy and take the body from the persisted pact file, base64 decode it and remove the multipart header it loads correctly in an image editor.