Hey all just a quick question. I have the followi...
# pact-python
j
Hey all just a quick question. I have the following code:
Copy code
"""pact test for OrderService provider"""

import logging

import pytest
import os

from pact import Verifier
from dotenv import load_dotenv
load_dotenv()

log = logging.getLogger(__name__)
logging.basicConfig(level=<http://logging.INFO|logging.INFO>)

# For the purposes of this example, the broker is started up as a fixture defined
# in conftest.py. For normal usage this would be self-hosted or using Pactflow.
PACT_BROKER_BASE_URL=os.getenv('PACT_USER')
PACT_BROKER_TOKEN=os.getenv('PACT_BROKER_TOKEN')

# For the purposes of this example, the Flask provider will be started up as part
# of run_pytest.sh when running the tests. Alternatives could be, for example
# running a Docker container with a database of test data configured.
# This is the "real" provider to verify against.
PROVIDER_HOST = "localhost"
PROVIDER_PORT = 5000
PROVIDER_URL = f"http://{PROVIDER_HOST}:{PROVIDER_PORT}"


@pytest.fixture
def broker_opts():
    return {
        # "broker_username": PACT_BROKER_USERNAME,
        # "broker_password": PACT_BROKER_PASSWORD,
        "broker_url": PACT_BROKER_BASE_URL,
        "publish_version": "1",
        "publish_verification_results": True,
        "broker_base_url":PACT_BROKER_BASE_URL,
        "broker_token":PACT_BROKER_TOKEN
    }


def test_user_service_provider_against_broker(broker_opts):
    verifier = Verifier(provider="KitchenService", provider_base_url=PROVIDER_URL)

    # Request all Pact(s) from the Pact Broker to verify this Provider against.
    # In the Pact Broker logs, this corresponds to the following entry:
    # PactBroker::Api::Resources::ProviderPactsForVerification -- Fetching pacts for verification by UserService -- {:provider_name=>"UserService", :params=>{}}
    success, logs = verifier.verify_with_broker(
        **broker_opts,
        verbose=True,
        provider_states_setup_url=f"{PROVIDER_URL}/_pact/provider_states",
        enable_pending=False,
    )
    # If publish_verification_results is set to True, the results will be
    # published to the Pact Broker.
    # In the Pact Broker logs, this corresponds to the following entry:
    #   PactBroker::Verifications::Service -- Creating verification 200 for \
    #   pact_version_sha=c8568cbb30d2e3933b2df4d6e1248b3d37f3be34 -- \
    #   {"success"=>true, "providerApplicationVersion"=>"3", "wip"=>false, \
    #   "pending"=>"true"}

    # Note:
    #  If "successful", then the return code here will be 0
    #  This can still be 0 and so PASS if a Pact verification FAILS, as long as
    #  it has not resulted in a REGRESSION of an already verified interaction.
    #  See <https://docs.pact.io/pact_broker/advanced_topics/pending_pacts/> for
    #  more details.
    assert success == 0


def test_KitchenService_provider_against_pact():
    verifier = Verifier(provider="KitchenService", provider_base_url=PROVIDER_URL)

    # Rather than requesting the Pact interactions from the Pact Broker, this
    # will perform the verification based on the Pact file locally.
    #
    # Because there is no way of knowing the previous state of an interaction,
    # if it has been successful in the past (since this is what the Pact Broker
    # is for), if the verification of an interaction fails then the success
    # result will be != 0, and so the test will FAIL.
    output, _ = verifier.verify_pacts(
        "../pacts/OrderService-KitchenService.json",
        verbose=False,
        provider_states_setup_url="{}/_pact/provider_states".format(PROVIDER_URL),
    )

    assert output == 0
but I get the error: I did create a new endpoint that posts the data to the KitchenService on the endpoint: So im not quite sure what the error means?
m
Take a look at the examples, where provider states are setup
If you're specifying provider states, you need the endpoint to support it
j
is that not what i did set up the providerstate endpoint: /_pact/provider_state?
i have the following provider code:
Copy code
@blueprint.response(200)
@blueprint.route('/_pact/provider_states', methods=['POST'])
def provider_states():
    data = setup_scheduled_order_for_cancellation()
    mapping = {
    "A scheduled order exists and it is not cancelled already": data,
    }
    mapping[request.json["state"]]()
    # return {"state":mapping}

    return jsonify({"result": request.json['state']})
    # return mapping

def setup_scheduled_order_for_cancellation():
    print('inside creat teest data function')
    payload = {}
    payload['id'] = '273429e2-e513-4df7-95bc-eeaad7e0e5aa'
    payload['scheduled'] = datetime.utcnow()
    payload['status'] = 'pending'
    payload['order'] = [
        {
            "product": "coffee",
            "quantity": 1,
            "size": "big"
        }
    ]
    schedules.append(payload)
    validate_schedule(payload)
    return payload
im not sure what the following lines are supposed to do:
Copy code
mapping[request.json["state"]]()
    return jsonify({"result": request.json['state']})
m
You just need to be able to return a 2xx status for the provider states, the main thing is that you are setting up the interaction for a successful test run. It looks like your code probably does that from context
As per https://pact-foundation.slack.com/archives/C9VECUP6E/p1656165624813059, it looks like the pact verifier is getting a
403
when attempting to communicate to your endpoint. Is there auth on it?
j
thre isnt any auth on the endpoint
🤔 1
m
The verifier can also emit a log file, which shows you what it’s doing/seeing
j
i see a log file
but it doesnt show much
message has been deleted
I get a log like this:
Copy code
Failed interactions:

PACT_DESCRIPTION='a request for cancellation' PACT_PROVIDER_STATE='A scheduled order exists and it is not cancelled already' /Users/johnwilliams/.local/share/virtualenvs/kitchen-75vucmml/bin/pytest provider_test.py # A request for cancellation given A scheduled order exists and it is not cancelled already
opening connection to <http://reallyusefulengine.pactflow.io:443|reallyusefulengine.pactflow.io:443>...
opened
starting SSL for <http://reallyusefulengine.pactflow.io:443|reallyusefulengine.pactflow.io:443>...
SSL established
<- "POST /pacts/provider/KitchenService/consumer/OrdersService/pact-version/151338713becd543895163268e6e1043cd4d772f/metadata/c1tdW2xdPXRydWUmc1tdW2N2XT0xNg/verification-results HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: application/hal+json, */*\r\nUser-Agent: Ruby\r\nContent-Type: application/json\r\nAuthorization: [redacted]\r\n"
<- "{\"success\":false,\"providerApplicationVersion\":\"1\",\"testResults\":{\"tests\":[{\"testDescription\":\"has status code 200\",\"testFullDescription\":\"Verifying a pact between OrdersService and KitchenService Given A scheduled order exists and it is not cancelled already a request for cancellation with GET /kitchen/schedule/273429e2-e513-4df7-95bc-eeaad7e0e5aa/cancel returns a response which has status code 200\",\"status\":\"failed\",\"interactionProviderState\":\"A scheduled order exists and it is not cancelled already\",\"interactionDescription\":\"a request for cancellation\",\"exception\":{\"class\":\"Pact::ProviderVerifier::SetUpProviderStateError\",\"message\":\"Error setting up provider state 'A scheduled order exists and it is not cancelled already' for consumer 'OrdersService' at <http://localhost:5000/_pact/provider_states>. response status=403 response body=\"}},{\"testDescription\":\"has a matching body\",\"testFullDescription\":\"Verifying a pact between OrdersService and KitchenService Given A scheduled order exists and it is not cancelled already a request for cancellation with GET /kitchen/schedule/273429e2-e513-4df7-95bc-eeaad7e0e5aa/cancel returns a response which has a matching body\",\"status\":\"failed\",\"interactionProviderState\":\"A scheduled order exists and it is not cancelled already\",\"interactionDescription\":\"a request for cancellation\",\"exception\":{\"class\":\"Pact::ProviderVerifier::SetUpProviderStateError\",\"message\":\"Error setting up provider state 'A scheduled order exists and it is not cancelled already' for consumer 'OrdersService' at <http://localhost:5000/_pact/provider_states>. response status=403 response body=\"}}],\"summary\":{\"testCount\":2,\"failureCount\":2},\"metadata\":{\"warning\":\"These test results use a beta format. Do not rely on it, as it will definitely change.\",\"pactVerificationResultsSpecification\":{\"version\":\"1.0.0-beta.1\"}}}}"
-> "HTTP/1.1 201 Created\r\n"
-> "Date: Sun, 26 Jun 2022 11:06:16 GMT\r\n"
-> "Content-Type: application/hal+json;charset=utf-8\r\n"
-> "Content-Length: 2830\r\n"
-> "Connection: keep-alive\r\n"
-> "Vary: Accept\r\n"
-> "Location: <https://reallyusefulengine.pactflow.io/pacts/provider/KitchenService/consumer/OrdersService/pact-version/151338713becd543895163268e6e1043cd4d772f/verification-results/246>\r\n"
-> "Server: Webmachine-Ruby/1.6.0 Rack/1.3\r\n"
-> "X-Pact-Broker-Version: 2.100.0\r\n"
-> "X-Content-Type-Options: nosniff\r\n"
-> "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload\r\n"
-> "X-Pact-Broker-Git-Sha: eb163ede\r\n"
-> "X-Pactflow-Git-Sha: 20b84cf0a\r\n"
-> "X-Request-Id: 1d1c887ae363993d23c5ba48cc86741d\r\n"
-> "\r\n"
reading 2830 bytes...
-> "{\"providerName\":\"KitchenService\",\"providerApplicationVersion\":\"1\",\"success\":false,\"verificationDate\":\"2022-06-26T11:06:16.390+00:00\",\"testResults\":{\"tests\":[{\"testDescription\":\"has status code 200\",\"testFullDescription\":\"Verifying a pact between OrdersService and KitchenService Given A scheduled order exists and it is not cancelled already a request for cancellation with GET /kitchen/schedule/273429e2-e513-4df7-95bc-eeaad7e0e5aa/cancel returns a response which has status code 200\",\"status\":\"failed\",\"interactionProviderState\":\"A scheduled order exists and it is not cancelled already\",\"interactionDescription\":\"a request for cancellation\",\"exception\":{\"class\":\"Pact::ProviderVerifier::SetUpProviderStateError\",\"message\":\"Error setting up provider state 'A scheduled order exists and it is not cancelled already' for consumer 'OrdersService' at <http://localhost:5000/_pact/provider_states>. response status=403 response body=\"}},{\"testDescription\":\"has a matching body\",\"testFullDescription\":\"Verifying a pact between OrdersService and KitchenService Given A scheduled order exists and it is not cancelled already a request for cancellation with GET /kitchen/schedule/273429e2-e513-4df7-95bc-eeaad7e0e5aa/cancel returns a response which has a matching body\",\"status\":\"failed\",\"interactionProviderState\":\"A scheduled order exists and it is not cancelled already\",\"interactionDescription\":\"a request for cancellation\",\"exception\":{\"class\":\"Pact::ProviderVerifier::SetUpProviderStateError\",\"message\":\"Error setting up provider state 'A scheduled order exists and it is not cancelled already' for consumer 'OrdersService' at <http://localhost:5000/_pact/provider_states>. response status=403 response body=\"}}],\"summary\":{\"testCount\":2,\"failureCount\":2},\"metadata\":{\"warning\":\"These test results use a beta format. Do not rely on it, as it will definitely change.\",\"pactVerificationResultsSpecification\":{\"version\":\"1.0.0-beta.1\"}}},\"verifiedBy\":{},\"_links\":{\"self\":{\"title\":\"Verification result\",\"name\":\"Verification result 246 for Pact between OrdersService (1) and KitchenService\",\"href\":\"<https://reallyusefulengine.pactflow.io/pacts/provider/KitchenService/consumer/OrdersService/pact-version/151338713becd543895163268e6e1043cd4d772f/verification-results/246>\"},\"pb:pact-version\":{\"title\":\"Pact\",\"name\":\"Pact between OrdersService (1) and KitchenService\",\"href\":\"<https://reallyusefulengine.pactflow.io/pacts/provider/KitchenService/consumer/OrdersService/pact-version/151338713becd543895163268e6e1043cd4d772f/metadata/Y3ZuPTE>\"},\"pb:triggered-webhooks\":{\"title\":\"Webhooks triggered by the publication of this verification result\",\"href\":\"<https://reallyusefulengine.pactflow.io/pacts/provider/KitchenService/consumer/OrdersService/pact-version/151338713becd543895163268e6e1043cd4d772f/verification-results/246/triggered-webhooks>\"}}}"
read 2830 bytes
Conn keep-alive
INFO: Verification results published to <https://reallyusefulengine.pactflow.io/pacts/provider/KitchenService/consumer/OrdersService/pact-version/151338713becd543895163268e6e1043cd4d772f/verification-results/246>
m
I don’t know the python interface very well, but looks like you can make it go to debug level: https://github.com/pact-foundation/pact-python/blob/8085be0d6f8bbed1adeaa0aad0837d7d84c3ab82/pact/verify_wrapper.py#L155
I think also setting the
VERBOSE=true
environment variable might land you with better logs
(that would just bypass the Python API)
j
Copy code
2) Verifying a pact between OrdersService and KitchenService Given A scheduled order exists and it is not cancelled already a request for cancellation with GET /kitchen/schedule/273429e2-e513-4df7-95bc-eeaad7e0e5aa/cancel returns a response which has a matching body
     Failure/Error: set_up_provider_states interaction.provider_states, options[:consumer]

     Pact::ProviderVerifier::SetUpProviderStateError:
       Error setting up provider state 'A scheduled order exists and it is not cancelled already' for consumer 'OrdersService' at <http://localhost:5000/kitchen/_pact/provider_states>. response status=403 response body=
m
hmm thanks.
Copy code
<http://localhost:5000/kitchen/_pact/provider_states>
That doesn’t seem to line up with the code I see above. Specifically, I can’t see where
/kitchen
is set on the states endpoint?
j
ah yeah sorry i just changed that
so the provide has
provider_states_setup_url="{}/kitchen/_pact/provider_states".format(PROVIDER_URL)
@blueprint.route('/kitchen/_pact/provider_states', methods=['POST']) def provider_states():
one thing Matt if I use swagger to manually post to that _pact/provider_states endpoint i get an error
message has been deleted
m
one thing Matt if I use swagger to manually post to that _pact/provider_states endpoint i get an error
This is your code right? So perhaps you have validation to ensure you have a valid JSON object sent to that endpoint
As a starting point, i’d suggest adding logging to your
kitchen/_pact/provider_states
endpoint to a) ensure it’s actually called b) see what the shape of the payload from Pact is c) ensure it’s not failing mid way
(I have to run sorry - but I’d be inclined to assume the verifier is not lying to you, and is receiving a
403
from some endpoint - you just need to figure out which one!)
j
no worries Matt, enjoy the rest of your evening
🙌 1
the error seems to be because of this:
Copy code
mapping[request.json["state"]]()
TypeError: 'dict' object is not callable
but im not sure how to fix that. Anyhow pretty late so ill go to bed
ahh man
i fixed it buy doing this:
Copy code
@blueprint.response(200)
@blueprint.route('/kitchen/_pact/provider_states', methods=['Post'])
def provider_state():
    print(request)
    data = setup_scheduled_order_for_cancellation()
    mapping = {
    "A scheduled order exists and it is not cancelled already": data,
    }
    # mapping[request.json["state"]]()
    return {"state":mapping}
at least it is one step forward
m
nice work! Also if I read the above, you are execute the state for the cancellation there on every call to that endpoint - probably not what you want? Really, you only want to seed the cancellation info when the “A scheduled order exists and it is not cancelled already” state is fired. In the current handler, the data is always setup even if the cancellation state isn’t intended to be setup
Copy code
mapping = {
    "A scheduled order exists and it is not cancelled already": setup_scheduled_order_for_cancellation(),
    }
    mapping[request.json["state"]]() # <- this now only fires the function if found. You should properly guard this to avoid errors for states that aren't mapped
j
Im glad it is kind of working now. I added Like(expected to my assertion and it doesn't seem to complain about differences in the date now, so that is cool. The reason why it didn't seem to work before was in the provider test I was pointing to localhost but the expectation was 127.0.0.1. Also when I tried to use the code for that only runnning state data setup (from the example) I got the error:
Copy code
mapping[request.json["state"]]()
TypeError: 'dict' object is not callable
m
Yes, it’s because you didn’t pass a function to that mapping dict, you executed the function and so you were trying to call a function that was not a function
i.e.
Copy code
@blueprint.response(200)
@blueprint.route('/_pact/provider_states', methods=['POST'])
def provider_states():
    data = setup_scheduled_order_for_cancellation() # -< You already called the function here
    mapping = {
    "A scheduled order exists and it is not cancelled already": data, # <- data is not a function now, it's the data. You should just pass the function here
    }
    mapping[request.json["state"]]() # <- can't call this, because it's just data not a function. 
    # return {"state":mapping}

    return jsonify({"result": request.json['state']})
    # return mapping
j
Let me try that again
ill try to get that working
m
Also last one. You have used a like matcher for a string date. A like matcher just looks at the underlying json type and only fails if the type is different. So any string would make that test pass. You probably want a regex on a particular string format
👍 1
j
yes good point, is something im going to investigate next
as the point of the test is to make sure orders can be canclled