简体   繁体   English

请求模拟:如何在模拟端点中匹配 POSTed 有效负载

[英]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 .我编写了一个身份验证类,用于使用应用程序的API 密钥及其API 密钥秘密从 Twitter 获取应用程序的不记名令牌,如Twitter 开发人员文档中所示

I've mocked the appropriate endpoint using requests_mock this way:我以这种方式使用requests_mock模拟了适当的端点:

@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). (其中TwitterBasicAuth是我编写的身份验证类,而装置basic_auth_string是一个硬编码的字符串,可以通过适当地转换装置api_keyapi_key_secret获得)。

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.我已经梳理了requests_mock (以及responses )的文档,但还没有弄清楚如何让端点仅在发布正确的有效负载时使用承载令牌进行响应。

Please help.请帮忙。

Updated Answer更新答案

I went with gold_cy's comment and wrote a custom matcher that takes a request and returns an appropriately crafted OK response if the request has the correct url path, headers and json payload.我使用了gold_cy 的评论并编写了一个自定义匹配器,如果请求具有正确的 url 路径、标头和 json 有效负载,则该匹配器接受请求并返回适当制作的 OK 响应。 It returns a 403 response otherwise, as I'd expect from the Twitter API.否则它会返回 403 响应,正如我对 Twitter API 所期望的那样。

@pytest.fixture
def mock_post_bearer_token_endpoint(
    requests_mock, basic_auth_string, bearer_token
):
    def matcher(req):
        if req.path != "/oauth2/token":
            # no mock address
            return None
        if req.headers.get("Authorization") != f"Basic {basic_auth_string}":
            return create_forbidden_response()
        if (
            req.headers.get("Content-Type")
            != "application/x-www-form-urlencoded;charset=UTF-8"
        ):
            return create_forbidden_response()
        if req.json().get("grant_type") != "client_credentials":
            return create_forbidden_response()

        resp = requests.Response()
        resp._content = json.dumps(
            {"token_type": "bearer", "access_token": f"{bearer_token}"}
        ).encode()
        resp.status_code = 200

        return resp

    requests_mock._adapter.add_matcher(matcher)
    yield

def create_forbidden_response():
    resp = requests.Response()
    resp.status_code = 403
    return resp

Older Answer较旧的答案

I went with gold_cy's comment and wrote an additional matcher that takes the request and checks for the presence of the data of interest in the payload.我使用了gold_cy 的评论并编写了一个额外的匹配器,它接受请求并检查有效负载中是否存在感兴趣的数据。

@pytest.fixture(name="mock_post_bearer_token_endpoint")
def fixture_mock_post_bearer_token_endpoint(
    requests_mock, basic_auth_string, bearer_token
):
    def match_grant_type_in_payload(request):
        if request.json().get("grant_type") == "client_credentials":
            return True
        resp = Response()
        resp.status_code = 403
        resp.raise_for_status()

    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}"},
        additional_matcher=match_grant_type_in_payload,
    )

I opted to raise an Http403 error (instead of just returning False) in order to reduce the cognitive load of determining the reason exceptions are raised — returning False would lead to a requests_mock.exceptions.NoMockAddress being raised, which I don't think is descriptive enough in this case.我选择引发Http403错误(而不是仅仅返回 False),以减少确定引发异常原因的认知负担——返回 False 会导致requests_mock.exceptions.NoMockAddress被引发,我认为这不是在这种情况下足够描述。

I still think there's a better way around this, and I'll keep searching for it.我仍然认为有更好的方法来解决这个问题,我会继续寻找它。

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.我认为这里的误解是您需要将所有内容都放入匹配器中,并让 NoMatchException 告诉您是否正确。

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.例如,如果您需要根据请求正文切换响应值,则 additional_matchers 很有用,并且通常 true/false 就足够了。

eg, and i made no attempt to look up twitter auth for this:例如,我没有尝试为此查找 twitter auth:

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 https://requests-mock.readthedocs.io/en/latest/history.html

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM