Apologize if this isn't the right channel, just pi...
# pact-js
m
Apologize if this isn't the right channel, just picked one of the 2 in play....I am having an issue between a consumer using pact-js:0.9.4 and a provider using junit5spring:4.3.7 where the verification is failing saying:
Copy code
Header:
Content-Type: Expected a header 'Content-Type' but was missing
Body:
Expected a response type of 'application/json' but the actual type was 'null'
I have verified that the header is in the contract on the consumer, and that the provider is producing a content-type...any insight what might be going on here?
m
Are you sure that's the right version of JS client? Strange though. If you could share debug logs of the JVM run and ideally the pact file (redacted as necessary) that'd help
m
Thanks Matt, I’ll share more in the morning my time as I’m not on a work computer at the moment…the under-documented question was in hopes this was a simple oversight on our part
👍 1
m
no probs! It might well be, or it could be a bug (hopefully not 😉 )
m
Ok, finally getting back to this @Matt (pactflow.io / pact-js / pact-go) The pact file:
Copy code
{
  "consumer": {
    "name": "partner-signup-fe"
  },
  "provider": {
    "name": "campaign-manager"
  },
  "interactions": [
    {
      "description": "a request for campaigns",
      "providerState": "list of available campaigns is returned",
      "request": {
        "method": "GET",
        "path": "/"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": [
          {
            "active": true,
            "campaignName": "Campaign Name",
            "createdDate": "2022-06-24T17:23:57.232253Z",
            "endDate": "2022-06-24T17:23:57.232253Z",
            "id": "ce118b6e-d8e1-11e7-9296-cec278b6b50a",
            "startDate": "2022-06-24T17:23:57.232253Z",
            "updatedBy": "SomeUser",
            "updatedDate": "2022-06-24T17:23:57.232253Z"
          }
        ],
        "matchingRules": {
          "$.body": {
            "min": 1
          },
          "$.body[*].*": {
            "match": "type"
          },
          "$.body[*].active": {
            "match": "type"
          },
          "$.body[*].campaignName": {
            "match": "type"
          },
          "$.body[*].createdDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body[*].endDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body[*].id": {
            "match": "regex",
            "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
          },
          "$.body[*].startDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body[*].updatedBy": {
            "match": "type"
          },
          "$.body[*].updatedDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          }
        }
      }
    },
    {
      "description": "a request for a campaign that exists",
      "providerState": "the campaign is returned",
      "request": {
        "method": "GET",
        "path": "/ce118b6e-d8e1-11e7-9296-cec278b6b50a"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": {
          "active": true,
          "campaignName": "Campaign Name",
          "createdDate": "2022-06-24T17:23:57.232253Z",
          "endDate": "2022-06-24T17:23:57.232253Z",
          "id": "ce118b6e-d8e1-11e7-9296-cec278b6b50a",
          "startDate": "2022-06-24T17:23:57.232253Z",
          "updatedBy": "SomeUser",
          "updatedDate": "2022-06-24T17:23:57.232253Z"
        },
        "matchingRules": {
          "$.body.active": {
            "match": "type"
          },
          "$.body.campaignName": {
            "match": "type"
          },
          "$.body.createdDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.endDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.id": {
            "match": "regex",
            "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
          },
          "$.body.startDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.updatedBy": {
            "match": "type"
          },
          "$.body.updatedDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          }
        }
      }
    },
    {
      "description": "a request to create a campaign",
      "providerState": "campaign is created",
      "request": {
        "method": "POST",
        "path": "/",
        "body": {
          "active": true,
          "campaignName": "Campaign Name",
          "endDate": "2022-06-24T17:23:57.232253Z",
          "startDate": "2022-06-24T17:23:57.232253Z",
          "updatedBy": "Some User"
        }
      },
      "response": {
        "status": 200,
        "headers": {},
        "body": {
          "active": true,
          "campaignName": "Campaign Name",
          "createdDate": "2022-06-24T17:23:57.232253Z",
          "endDate": "2022-06-24T17:23:57.232253Z",
          "id": "ce118b6e-d8e1-11e7-9296-cec278b6b50a",
          "startDate": "2022-06-24T17:23:57.232253Z",
          "updatedBy": "SomeUser",
          "updatedDate": "2022-06-24T17:23:57.232253Z"
        },
        "matchingRules": {
          "$.body.active": {
            "match": "type"
          },
          "$.body.campaignName": {
            "match": "type"
          },
          "$.body.createdDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.endDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.id": {
            "match": "regex",
            "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
          },
          "$.body.startDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.updatedBy": {
            "match": "type"
          },
          "$.body.updatedDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          }
        }
      }
    },
    {
      "description": "a request to update a campaign",
      "providerState": "the campaign is updated",
      "request": {
        "method": "PUT",
        "path": "/ce118b6e-d8e1-11e7-9296-cec278b6b50a",
        "body": {
          "active": true,
          "campaignName": "Campaign Name",
          "endDate": "2022-06-24T17:23:57.232253Z",
          "startDate": "2022-06-24T17:23:57.232253Z",
          "updatedBy": "Some User",
          "id": "ce118b6e-d8e1-11e7-9296-cec278b6b50a"
        }
      },
      "response": {
        "status": 200,
        "headers": {},
        "body": {
          "active": true,
          "campaignName": "Campaign Name",
          "createdDate": "2022-06-24T17:23:57.232253Z",
          "endDate": "2022-06-24T17:23:57.232253Z",
          "id": "ce118b6e-d8e1-11e7-9296-cec278b6b50a",
          "startDate": "2022-06-24T17:23:57.232253Z",
          "updatedBy": "SomeUser",
          "updatedDate": "2022-06-24T17:23:57.232253Z"
        },
        "matchingRules": {
          "$.body.active": {
            "match": "type"
          },
          "$.body.campaignName": {
            "match": "type"
          },
          "$.body.createdDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.endDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.id": {
            "match": "regex",
            "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
          },
          "$.body.startDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          },
          "$.body.updatedBy": {
            "match": "type"
          },
          "$.body.updatedDate": {
            "match": "regex",
            "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
          }
        }
      }
    },
    {
      "description": "a request to delete a campaign",
      "providerState": "campaign-manager can delete a campaign",
      "request": {
        "method": "DELETE",
        "path": "/ce118b6e-d8e1-11e7-9296-cec278b6b50a"
      },
      "response": {
        "status": 204,
        "headers": {}
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}
Debug output of one of the provider calls (the rest are the same, delete passes):
Copy code
09:55:42.674 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@331d645a
09:55:42.674 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Detected org.springframework.web.servlet.support.SessionFlashMapManager@49b7eb3
09:55:42.674 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
09:55:42.674 [Test worker] INFO org.springframework.test.web.servlet.TestDispatcherServlet - Completed initialization in 2 ms
09:55:42.676 [Test worker] DEBUG au.com.dius.pact.provider.junit5.PactVerificationStateChangeExtension - beforeEach for interaction 'a request for campaigns'
09:55:42.677 [Test worker] INFO au.com.dius.pact.provider.junit5.PactVerificationStateChangeExtension - Invoking state change method 'list of available campaigns is returned':SETUP

Verifying a pact between partner-signup-fe (53d2605be7c514206dfae6909caa2d658f254f5d) and campaign-manager [PENDING]

  Notices:
    1) The pact at <https://pax8.pactflow.io/pacts/provider/campaign-manager/consumer/partner-signup-fe/pact-version/c719ba4772ec6631e6fb052e8ef165153207b0b6> is being verified because the pact content belongs to the consumer version matching the following criterion:
    * latest version of partner-signup-fe that has a pact with campaign-manager (53d2605be7c514206dfae6909caa2d658f254f5d)
    2) This pact is in pending state for this version of campaign-manager because a successful verification result for a version of campaign-manager with tag 'local' has not yet been published. If this verification fails, it will not cause the overall build to fail. Read more at <https://docs.pact.io/go/pending>

  [from Pact Broker <https://pax8.pactflow.io/pacts/provider/campaign-manager/consumer/partner-signup-fe/pact-version/c719ba4772ec6631e6fb052e8ef165153207b0b6/metadata/c1tdW2xdPXRydWUmc1tdW2N2XT01MTAmcD10cnVl>]
  Given list of available campaigns is returned
  a request for campaigns
09:55:42.792 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - GET "/", parameters={}
09:55:42.795 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped to com.pax8.campaignmanager.endpoint.CampaignEndpoints#getAllCampaignsByFilter(Optional, Optional, Pageable)
09:55:42.838 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json]
09:55:42.862 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Nothing to write: null body
09:55:42.863 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Completed 200 OK
09:55:42.864 [Test worker] DEBUG au.com.dius.pact.provider.spring.junit5.MockMvcTestTarget - Received response: 200
09:55:42.867 [Test worker] DEBUG au.com.dius.pact.provider.spring.junit5.MockMvcTestTarget - Response: ProviderResponse(statusCode=200, headers={}, contentType=application/json, body=)
09:55:42.876 [Test worker] DEBUG au.com.dius.pact.core.matchers.Matching - matchBody: context=MatchingContext(matchers=MatchingRuleCategory(name=body, matchingRules={$=MatchingRuleGroup(rules=[MinTypeMatcher(min=1)], ruleLogic=AND, cascaded=false), $[*].*=MatchingRuleGroup(rules=[au.com.dius.pact.core.model.matchingrules.TypeMatcher@4c829699], ruleLogic=AND, cascaded=false), $[*].active=MatchingRuleGroup(rules=[au.com.dius.pact.core.model.matchingrules.TypeMatcher@4c829699], ruleLogic=AND, cascaded=false), $[*].campaignName=MatchingRuleGroup(rules=[au.com.dius.pact.core.model.matchingrules.TypeMatcher@4c829699], ruleLogic=AND, cascaded=false), $[*].createdDate=MatchingRuleGroup(rules=[RegexMatcher(regex=^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d(:?[0-5]\d)?|Z)$, example=null)], ruleLogic=AND, cascaded=false), $[*].endDate=MatchingRuleGroup(rules=[RegexMatcher(regex=^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d(:?[0-5]\d)?|Z)$, example=null)], ruleLogic=AND, cascaded=false), $[*].id=MatchingRuleGroup(rules=[RegexMatcher(regex=^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$, example=null)], ruleLogic=AND, cascaded=false), $[*].startDate=MatchingRuleGroup(rules=[RegexMatcher(regex=^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d(:?[0-5]\d)?|Z)$, example=null)], ruleLogic=AND, cascaded=false), $[*].updatedBy=MatchingRuleGroup(rules=[au.com.dius.pact.core.model.matchingrules.TypeMatcher@4c829699], ruleLogic=AND, cascaded=false), $[*].updatedDate=MatchingRuleGroup(rules=[RegexMatcher(regex=^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d(:?[0-5]\d)?|Z)$, example=null)], ruleLogic=AND, cascaded=false)}), allowUnexpectedKeys=true, pluginConfiguration={})
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (FAILED)
      has a matching body (FAILED)

Pending Failures:

1) Verifying a pact between partner-signup-fe and campaign-manager - a request for campaigns includes headers "Content-Type" with value "[application/json]"

    1.1) header: Expected a header 'Content-Type' but was missing

    1.2) body-content-type: Expected a response type of 'application/json' but the actual type was 'null'


09:55:42.886 [Test worker] DEBUG au.com.dius.pact.provider.DefaultTestResultAccumulator - Received test result 'Failed(results=[{attribute=header, description=Expected a header 'Content-Type' but was missing, identifier=Content-Type, interactionId=b1b9d02b26684cf6bf742ad95229d7485b42f006}, {attribute=body-content-type, description=Expected a response type of 'application/json' but the actual type was 'null', interactionId=b1b9d02b26684cf6bf742ad95229d7485b42f006}], description=Headers had differences, Body had differences)' for Pact campaign-manager-partner-signup-fe and a request for campaigns (Pact Broker <https://pax8.pactflow.io/pacts/provider/campaign-manager/consumer/partner-signup-fe/pact-version/c719ba4772ec6631e6fb052e8ef165153207b0b6/metadata/c1tdW2xdPXRydWUmc1tdW2N2XT01MTAmcD10cnVl>)
09:55:42.889 [Test worker] DEBUG au.com.dius.pact.provider.DefaultTestResultAccumulator - Number of interactions #5 and results: [Failed(results=[{attribute=header, description=Expected a header 'Content-Type' but was missing, identifier=Content-Type, interactionId=b1b9d02b26684cf6bf742ad95229d7485b42f006}, {attribute=body-content-type, description=Expected a response type of 'application/json' but the actual type was 'null', interactionId=b1b9d02b26684cf6bf742ad95229d7485b42f006}], description=Headers had differences, Body had differences)]
09:55:42.889 [Test worker] WARN au.com.dius.pact.provider.DefaultTestResultAccumulator - Not all of the 5 were verified. The following were missing:
09:55:42.889 [Test worker] WARN au.com.dius.pact.provider.DefaultTestResultAccumulator -     a request for a campaign that exists
09:55:42.889 [Test worker] WARN au.com.dius.pact.provider.DefaultTestResultAccumulator -     a request to create a campaign
09:55:42.889 [Test worker] WARN au.com.dius.pact.provider.DefaultTestResultAccumulator -     a request to update a campaign
09:55:42.889 [Test worker] WARN au.com.dius.pact.provider.DefaultTestResultAccumulator -     a request to delete a campaign
09:55:42.889 [Test worker] DEBUG au.com.dius.pact.provider.junit5.PactVerificationStateChangeExtension - afterEach for interaction 'a request for campaigns'
09:55:42.895 [Test worker] DEBUG _org.springframework.web.servlet.HandlerMapping.Mappings - 
	c.p.c.e.CampaignEndpoints:
	{POST [/], consumes [application/json], produces [application/json]}: createCampaign(CampaignDTO)
	{DELETE [/{id}]}: deleteCampaign(String)
	{POST [/populate-campaigns]}: populateWithContractorData()
	{GET [/], produces [application/json]}: getAllCampaignsByFilter(Optional,Optional,Pageable)
	{GET [/{id}], produces [application/json]}: getCampaignById(String)
	{PUT [/{id}], consumes [application/json], produces [application/json]}: updatePartner(CampaignDTO,String)
09:55:42.896 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - 6 mappings in org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
09:55:42.900 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter - ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
09:55:42.901 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
09:55:42.901 [Test worker] INFO org.springframework.mock.web.MockServletContext - Initializing Spring TestDispatcherServlet ''
m
Thanks @MiKey Looking at the above, it seems like it got a null body and no `content-type`t header (I’m not sure what
contentType
is there in the debug log, whether it’s the expected type or detected, but you can see the headers map is empty)
Copy code
9:55:42.792 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - GET "/", parameters={}
09:55:42.795 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped to com.pax8.campaignmanager.endpoint.CampaignEndpoints#getAllCampaignsByFilter(Optional, Optional, Pageable)
09:55:42.838 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json]
09:55:42.862 [Test worker] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Nothing to write: null body
09:55:42.863 [Test worker] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Completed 200 OK
09:55:42.864 [Test worker] DEBUG au.com.dius.pact.provider.spring.junit5.MockMvcTestTarget - Received response: 200
09:55:42.867 [Test worker] DEBUG au.com.dius.pact.provider.spring.junit5.MockMvcTestTarget - Response: ProviderResponse(statusCode=200, headers={}, contentType=application/json, body=)
m
That's the part that blows my mind, I can use curl or postman and make the same call and I get the right headers back
And body
not in the contract test, but in the normal endpoint functional test we also ensure the header is set:
Copy code
}.andExpect {
            status { isOk() }
            content { contentType(MediaType.APPLICATION_JSON) }
m
any ideas @uglyog? Am I reading the output correctly?
u
I think this issue is being caused by MockMVC. MockMVC creates a mock request or response environment, and it seems to not be setting the content type header (as a real response would have).
🤔 1
m
ah!
The content type is an attribute on the mock response. We could try force a content type header is there is not one set
👍 1
m
do I need to have
@SpringBootTest
to use MockMVC like this? Guessing because I don't is why the header doesn't show there