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

Provider State Parameters Missing in URL When Service is Both Provider and Consumer

Open somnath157 opened this issue 1 year ago • 2 comments

When running the mvn test command, the verification test for a provider fails because the URL does not have the path variables loaded from the provider state. This issue occurs when the service acts as both a provider and a consumer for different APIs.

Software versions

Java : 17 Spring boot: 3.2.1 au.com.dius.pact.consumer.junit5: 4.6.9 au.com.dius.pact.provider.spring6: 4.6.9

Expected behaviour

The provider verification test should pass, with the URL including the path variables loaded from the provider state.

Actual behaviour

The provider verification test fails because the URL does not include the path variables from the provider state.

Steps to reproduce

  1. Ensure that the BeneficiaryAPIContractVerificationTest and DocumentServiceApiContractTest classes are correctly set up as shown in the provided code.
  2. Run mvn test to execute the test cases.
  3. Observe that while the consumer test (DocumentServiceApiContractTest) runs successfully, the provider verification test (BeneficiaryAPIContractVerificationTest) fails.

Additional Information:

This issue occurs specifically when both tests are in the same package. If the tests are in different packages, the issue occurs intermittently depending on the order in which the test classes are executed.

Code Samples: `package com.statrys.bankaccount.api.contract;

import au.com.dius.pact.consumer.MockServer; import au.com.dius.pact.consumer.dsl.PactDslJsonArray; 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.V4Pact; import au.com.dius.pact.core.model.annotations.Pact; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.statrys.bankaccount.api.client.dto.DocumentInfoDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate;

import java.util.List; import java.util.Map; import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail;

@ExtendWith(PactConsumerTestExt.class) @PactTestFor(providerName = "document-service") class DocumentServiceApiContractTest { private static final UUID DOCUMENT_CODE = UUID.fromString("fe7edc38-83fd-4ae0-818c-83b5d0b9845f"); private static final UUID DOCUMENT_CODE_NOT_EXISTS = UUID.fromString("b470fdb7-0544-468e-9b7b-c3069f95f45d"); private static final String EMPTY_DOCUMENT_CODE = ""; private static final String DOCUMENT_URL = "https://www.google.com/"; private static final Map<String, String> RES_HEADERS = Map.of("Content-Type", "application/json"); private static final RestTemplate REST_TEMPLATE = new RestTemplate(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@BeforeEach
public void setUp() {
    System.setProperty("pact.writer.overwrite", "true");
    System.setProperty("pact.expressions.start", "{");
}

@Pact(consumer = "bank-account-service")
public V4Pact getDocumentList(PactDslWithProvider builder) {
    return builder
            .given("document codes exist")
            .uponReceiving("Request to get document details internally")
            .path("/internal/v1/documents")
            .queryParameterFromProviderState("codes", "codes", DOCUMENT_CODE.toString())
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(RES_HEADERS)
            .body(new PactDslJsonArray()
                    .object()
                    .uuid("code", DOCUMENT_CODE)
                    .stringType("documentUrl", DOCUMENT_URL)
                    .closeObject()
            )
            .given("document codes not exist")
            .uponReceiving("Request to get document details internally")
            .path("/internal/v1/documents")
            .queryParameterFromProviderState("codes", "codes", DOCUMENT_CODE_NOT_EXISTS.toString())
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(RES_HEADERS)
            .body(new PactDslJsonArray())
            .given("document codes missing")
            .uponReceiving("Request to get document details internally")
            .path("/internal/v1/documents")
            .queryParameterFromProviderState("codes", "codes", EMPTY_DOCUMENT_CODE.toString())
            .method("GET")
            .willRespondWith()
            .status(422)
            .body(new PactDslJsonBody()
                    .stringType("code")
                    .stringType("message")
                    .stringMatcher("raisedAt", "\\d{13}", "1714983789660")
            )
            .toPact(V4Pact.class);
}

@Test
@PactTestFor(pactMethod = "getDocumentList")
void shouldGetDocumentList(MockServer mockServer) throws JsonProcessingException {
    var baseURL = mockServer.getUrl();
    ResponseEntity<String> responseEntityPresent = REST_TEMPLATE.getForEntity(
            baseURL + "/internal/v1/documents?codes=" + DOCUMENT_CODE,
            String.class);
    assertThat(responseEntityPresent.getStatusCode()).isEqualTo(HttpStatus.OK);
    var body = responseEntityPresent.getBody();
    var documents = OBJECT_MAPPER.readValue(body, new TypeReference<List<DocumentInfoDTO>>() {
    });
    assertThat(documents).hasSize(1);
    assertThat(documents.get(0).getCode()).isEqualTo(DOCUMENT_CODE);
    assertThat(documents.get(0).getDocumentUrl()).isEqualTo(DOCUMENT_URL);
    try {
        REST_TEMPLATE.getForEntity(
                baseURL + "/internal/v1/documents?codes=" + EMPTY_DOCUMENT_CODE,
                String.class);
        fail("should have thrown exception");
    } catch (HttpClientErrorException ex) {
        assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
    }
    ResponseEntity<String> responseEntityEmpty = REST_TEMPLATE.getForEntity(
            baseURL + "/internal/v1/documents?codes=" + DOCUMENT_CODE_NOT_EXISTS,
            String.class);
    assertThat(responseEntityEmpty.getStatusCode()).isEqualTo(HttpStatus.OK);
    var emptyBody = responseEntityEmpty.getBody();
    var documentsList = OBJECT_MAPPER.readValue(emptyBody, new TypeReference<List<DocumentInfoDTO>>() {
    });
    assertThat(documentsList).isEmpty();
}

}`

`package com.statrys.bankaccount.api.contract;

import au.com.dius.pact.provider.junit5.PactVerificationContext; import au.com.dius.pact.provider.junitsupport.Provider; import au.com.dius.pact.provider.junitsupport.State; import au.com.dius.pact.provider.junitsupport.loader.PactBroker; import au.com.dius.pact.provider.junitsupport.loader.PactBrokerAuth; import au.com.dius.pact.provider.junitsupport.loader.PactBrokerConsumerVersionSelectors; import au.com.dius.pact.provider.junitsupport.loader.SelectorBuilder; import au.com.dius.pact.provider.spring.spring6.PactVerificationSpring6Provider; import au.com.dius.pact.provider.spring.spring6.Spring6MockMvcTestTarget; import com.statrys.bankaccount.api.contract.mocks.BeneficiaryMocks; import com.statrys.bankaccount.api.controller.BeneficiaryApiController; import com.statrys.bankaccount.configuration.ContractConfig; import com.statrys.bankaccount.service.BeneficiaryService; import com.statrys.bankaccount.type.BankServiceMessage; import com.statrys.common.exception.ServiceException; import com.statrys.common.web.handler.CustomResponseEntityExceptionHandler; import com.statrys.testing.contract.config.ContractVerificationConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.MessageSource;

import java.util.HashMap; import java.util.Map; import java.util.UUID;

import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when;

@SpringBootTest(classes = ContractConfig.class, properties = {"aws.paramstore.enabled=false", "aws.secretsmanager.enabled=false"}) @ExtendWith(MockitoExtension.class) @Provider("bank-account-service") @PactBroker(url = "${pact.broker.url}", authentication = @PactBrokerAuth(username = "${pact.broker.username}", password = "${pact.broker.password}")) public class BeneficiaryAPIContractVerificationTest {

private final BeneficiaryMocks mocks = new BeneficiaryMocks();
@Mock
private BeneficiaryService beneficiaryService;
@InjectMocks
private BeneficiaryApiController controller;
@Mock
private MessageSource messageSource;

private CustomResponseEntityExceptionHandler customResponseEntityExceptionHandler;

@PactBrokerConsumerVersionSelectors
public static SelectorBuilder consumerVersionSelectors() {
    ContractVerificationConfig.setUpEnvironment();
    return ContractVerificationConfig.getSelector();
}

@TestTemplate
@ExtendWith(PactVerificationSpring6Provider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
    context.verifyInteraction();
}

@BeforeEach
void before(PactVerificationContext context) {
    customResponseEntityExceptionHandler = new CustomResponseEntityExceptionHandler(messageSource);
    Spring6MockMvcTestTarget testTarget = new Spring6MockMvcTestTarget();
    testTarget.setControllers(controller);
    testTarget.setControllerAdvices(customResponseEntityExceptionHandler);
    testTarget.setPrintRequestResponse(false);
    context.setTarget(testTarget);
}

@State("beneficiary is present")
public Map<String, Object> shouldReturnBeneficiaryDetails() {
    var params = new HashMap<String, Object>();
    String clientCode = UUID.randomUUID().toString();
    String beneficiaryCode = UUID.randomUUID().toString();

    params.put("client-code", clientCode);
    params.put("beneficiary-code", beneficiaryCode);
    when(beneficiaryService.getBeneficiaryWithComplianceDetails(any(), any(), any(), any())).thenReturn(mocks.getBeneficiaryWithComplianceDetailDTO());
    return params;
}

@State("beneficiary is not present")
public Map<String, Object> shouldThrowExceptionWhenBeneficiaryIsNotPresent() {
    var params = new HashMap<String, Object>();
    String clientCode = UUID.randomUUID().toString();
    String beneficiaryCode = UUID.randomUUID().toString();

    params.put("client-code", clientCode);
    params.put("beneficiary-code", beneficiaryCode);
    when(messageSource.getMessage(any(), any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
    doThrow(new ServiceException(BankServiceMessage.INVALID_BENEFICIARY_CODE)).when(beneficiaryService).getBeneficiaryWithComplianceDetails(any(), any(), any(), any());
    return params;
}

} `

Logs:

2024-08-22 16:39:34.735 INFO --- [main] [] [] [] a.c.d.p.p.j.PactVerificationStateChangeExtension$Companion : Invoking state change method 'beneficiary is not present':SETUP

Verifying a pact between transfer-service (1065) and bank-account-service

Notices: 1) The pact at https://contract.test.dev.statrys.com/pacts/provider/bank-account-service/consumer/transfer-service/pact-version/abbcad97a4f0640fc52899af62b9851a43d0837d is being verified because the pact content belongs to the consumer version matching the following criterion: * latest version tagged 'dev' (1065)

[from Pact Broker https://contract.test.dev.statrys.com/pacts/provider/bank-account-service/consumer/transfer-service/pact-version/abbcad97a4f0640fc52899af62b9851a43d0837d/metadata/c1tdW3RdPWRldiZzW11bbF09dHJ1ZSZzW11bY3ZdPTg0Ng] Given beneficiary is not present Request to get beneficiary details internally 2024-08-22 16:39:34.985 INFO --- [main] [] [] [] o.s.mock.web.MockServletContext : Initializing Spring TestDispatcherServlet '' 2024-08-22 16:39:34.985 INFO --- [main] [] [] [] o.s.t.w.s.TestDispatcherServlet : Initializing Servlet '' 2024-08-22 16:39:34.985 INFO --- [main] [] [] [] o.s.t.w.s.TestDispatcherServlet : Completed initialization in 0 ms 2024-08-22 16:39:34.995 WARN --- [main] [] [] [] o.s.web.servlet.PageNotFound : No mapping for GET /internal/v1/clients/beneficiaries/ 2024-08-22 16:39:34.999 ERROR --- [main] [] [] [] c.s.c.w.h.CustomResponseEntityExceptionHandler : Un categorised error. Message [No endpoint GET /internal/v1/clients/beneficiaries/.]. See service log file for detail stacktrace 2024-08-22 16:39:34.999 INFO --- [main] [] [] [] c.s.c.w.h.CustomResponseEntityExceptionHandler : Stacktrace of un-categorised error org.springframework.web.servlet.NoHandlerFoundException: No endpoint GET /internal/v1/clients/beneficiaries/. at org.springframework.web.servlet.DispatcherServlet.noHandlerFound(DispatcherServlet.java:1304) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)

somnath157 avatar Aug 22 '24 10:08 somnath157

Moving to pact-jvm (this repo is pact-js for javascript based projects).

YOU54F avatar Aug 22 '24 10:08 YOU54F

I've identified the cause of the issue. The problem was that in the consumer test case, I was setting the property pact.expressions.start to "{" which persisted into the provider verification phase, causing conflicts when resolving dynamic variables. The solution I implemented involves clearing these properties after the consumer test is executed.

@BeforeEach
    public void setUp() {
        System.setProperty("pact.writer.overwrite", "true");
        System.setProperty("pact.expressions.start", "{");
    }
@AfterAll
    public static void cleanUp() {
        System.clearProperty("pact.writer.overwrite");
        System.clearProperty("pact.expressions.start");
    }

somnath157 avatar Aug 26 '24 04:08 somnath157