kevin1024 / vcrpy

Automatically mock your HTTP interactions to simplify and speed up testing
MIT License
2.72k stars 388 forks source link

Add support for matching request body multipart form data #647

Open mparent61 opened 2 years ago

mparent61 commented 2 years ago

Request bodies of content type multipart/form-data can have different "boundary" separators, but contain identical data. This can cause VCRPy's "body" matcher to fail on otherwise identical requests.

This adds support for parsing the encoded body data, essentially stripping out the "boundary" values, and comparing the contents.

Also, a similar approach could be used for handling "replace_post_data_parameters" mentioned in https://github.com/kevin1024/vcrpy/issues/521.

Testing Notes

Added 3 unit tests to confirm matching behavior, using sample 1x1 PNG image data. and differing "boundary" delimiters.

Implementation Notes

mezhaka commented 1 year ago

@kevin1024 Any plans for integrating this?

@mparent61 Is there any workaround right now? I have a multpart/form-data and I cannot use the cassete I record (which is actually a sending and recieving a small file).

mparent61 commented 1 year ago

@mezhaka - I've been using this workaround --

import re

import pytest
from requests_toolbelt.multipart import decoder
from vcr.matchers import _get_transformer, _identity
from vcr.util import read_body

def decode_multipart_request_body(request):
    return decoder.MultipartDecoder(
        content=request.body, content_type=request.headers["content-type"]
    )

def is_multipart_form_data_request(request):
    return re.match(
        r"multipart/form-data(;|$)", request.headers.get("content-type", "").lower()
    )

# This is a workaround for request body "multipart form" handling
def request_body_matcher(r1, r2) -> None:
    if is_multipart_form_data_request(r1) and is_multipart_form_data_request(r2):
        decoded1 = decode_multipart_request_body(r1)
        decoded2 = decode_multipart_request_body(r2)
        assert decoded1.encoding == decoded2.encoding
        assert len(decoded1.parts) == len(decoded2.parts)
        for part1, part2 in zip(decoded1.parts, decoded2.parts):
            assert part1.headers == part2.headers
            assert (
                part1.content == part2.content
            ), f"Multipart data doesn't match for part with content type: {part1.headers}"
    else:
        # Copied directly from VCRPy's `body` matcher. For some reason PyTest's custom ASSERT handlers (with
        # much better diagnostic info) does not work when the assert fires inside VCRPy.
        transformer = _get_transformer(r1)
        r2_transformer = _get_transformer(r2)
        if transformer != r2_transformer:
            transformer = _identity
        body1 = read_body(r1)
        body2 = read_body(r2)
        assert transformer(body1) == transformer(body2)

and then to setup -

@pytest.fixture(scope="module")
def vcr(vcr):
    # Override VCRPy's default body matcher to support Multipart form data, and provide better ASSERT messages.
    vcr.register_matcher("body", request_body_matcher)
    return vcr