Search code examples
pythonpython-requestspytestrequests-mock

requests-mock: how can I match POSTed payload in a mocked endpoint


What I've Done

I've written an authentication class for obtaining an application's bearer token from Twitter using the application's API Key and its API key secret as demonstrated in the Twitter developer docs.

I've mocked the appropriate endpoint using requests_mock this way:

@pytest.fixture
def mock_post_bearer_token_endpoint(
    requests_mock, basic_auth_string, bearer_token
):
    requests_mock.post(
        "https://api.twitter.com/oauth2/token",
        request_headers={
            "Authorization": f"Basic {basic_auth_string}",
            "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
        },
        json={"token_type": "bearer", "access_token": f"{bearer_token}"},
    )

And my test method is :

@pytest.mark.usefixtures("mock_post_bearer_token_endpoint")
def test_basic_auth(api_key, api_key_secret, bearer_token):
    response = requests.post(
        'https://api.twitter.com/oauth2/token',
        data={"grant_type": "client_credentials"},
        auth=TwitterBasicAuth(api_key, api_key_secret),
    )
    assert response.json()['access_token'] == bearer_token

(Where TwitterBasicAuth is the authentication class I wrote, and the fixture basic_auth_string is a hardcoded string that would be obtained from transforming the fixtures api_key and api_key_secret appropriately).

And it works.

The Problem

But I'm really bothered by the fact that the mocked endpoint doesn't check the payload. In this particular case, the payload is vital to obtain a bearer token.

I've combed through the documentation for requests_mock (and responses, too) but haven't figured out how to make the endpoint respond with a bearer token only when the correct payload is POSTed.

Please help.


Solution

  • I think the misconception here is that you need to put everything in the matcher and let NoMatchException be the thing to tell you if you got it right.

    The matcher can be the simplest thing it needs to be in order to return the right response and then you can do all the request/response checking as part of your normal unit test handling.

    additional_matchers is useful if you need to switch the response value based on the body of the request for example, and typically true/false is sufficient there.

    eg, and i made no attempt to look up twitter auth for this:

    import requests
    import requests_mock
    
    class TwitterBasicAuth(requests.auth.AuthBase):
    
        def __init__(self, api_key, api_key_secret):
            self.api_key = api_key
            self.api_key_secret = api_key_secret
    
        def __call__(self, r):
            r.headers['x-api-key'] = self.api_key
            r.headers['x-api-key-secret'] = self.api_key_secret
            return r
    
    
    with requests_mock.mock() as m:
        api_key = 'test'
        api_key_secret = 'val'
    
        m.post(
            "https://api.twitter.com/oauth2/token",
            json={"token_type": "bearer", "access_token": "token"},
        )
    
        response = requests.post(
            'https://api.twitter.com/oauth2/token',
            data={"grant_type": "client_credentials"},
            auth=TwitterBasicAuth(api_key, api_key_secret),
        )
    
        assert response.json()['token_type'] == "bearer"
        assert response.json()['access_token'] == "token"
        assert m.last_request.headers['x-api-key'] == api_key
        assert m.last_request.headers['x-api-key-secret'] == api_key_secret
    

    https://requests-mock.readthedocs.io/en/latest/history.html