diff --git a/sdk/core/azure-core/azure/core/pipeline/_base.py b/sdk/core/azure-core/azure/core/pipeline/_base.py index 9c92eaea76b0..f442fc88dedb 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/_base.py @@ -175,6 +175,9 @@ def _prepare_multipart_mixed_request(request): import concurrent.futures def prepare_requests(req): + if req.multipart_mixed_info: + # Recursively update changeset "sub requests" + Pipeline._prepare_multipart_mixed_request(req) context = PipelineContext(None, **pipeline_options) pipeline_request = PipelineRequest(req, context) for policy in policies: diff --git a/sdk/core/azure-core/azure/core/pipeline/_base_async.py b/sdk/core/azure-core/azure/core/pipeline/_base_async.py index dcf0cdd9b6f3..4ce843176932 100644 --- a/sdk/core/azure-core/azure/core/pipeline/_base_async.py +++ b/sdk/core/azure-core/azure/core/pipeline/_base_async.py @@ -183,6 +183,9 @@ async def _prepare_multipart_mixed_request(self, request): pipeline_options = multipart_mixed_info[3] # type: Dict[str, Any] async def prepare_requests(req): + if req.multipart_mixed_info: + # Recursively update changeset "sub requests" + await self._prepare_multipart_mixed_request(req) context = PipelineContext(None, **pipeline_options) pipeline_request = PipelineRequest(req, context) for policy in policies: diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py index dbb0c595cf76..c2719b97464c 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_base.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_base.py @@ -60,6 +60,7 @@ Optional, Tuple, Iterator, + Type ) from six.moves.http_client import HTTPConnection, HTTPResponse as _HTTPResponse @@ -379,27 +380,30 @@ def set_multipart_mixed(self, *requests, **kwargs): :keyword list[SansIOHTTPPolicy] policies: SansIOPolicy to apply at preparation time :keyword str boundary: Optional boundary - :param requests: HttpRequests object """ self.multipart_mixed_info = ( requests, kwargs.pop("policies", []), - kwargs.pop("boundary", []), + kwargs.pop("boundary", None), kwargs ) - def prepare_multipart_body(self): - # type: () -> None + def prepare_multipart_body(self, content_index=0): + # type: (int) -> int """Will prepare the body of this request according to the multipart information. This call assumes the on_request policies have been applied already in their correct context (sync/async) Does nothing if "set_multipart_mixed" was never called. + + :param int content_index: The current index of parts within the batch message. + :returns: The updated index after all parts in this request have been added. + :rtype: int """ if not self.multipart_mixed_info: - return + return 0 requests = self.multipart_mixed_info[0] # type: List[HttpRequest] boundary = self.multipart_mixed_info[2] # type: Optional[str] @@ -409,12 +413,22 @@ def prepare_multipart_body(self): main_message.add_header("Content-Type", "multipart/mixed") if boundary: main_message.set_boundary(boundary) - for i, req in enumerate(requests): + + for req in requests: part_message = Message() - part_message.add_header("Content-Type", "application/http") - part_message.add_header("Content-Transfer-Encoding", "binary") - part_message.add_header("Content-ID", str(i)) - part_message.set_payload(req.serialize()) + if req.multipart_mixed_info: + content_index = req.prepare_multipart_body(content_index=content_index) + part_message.add_header("Content-Type", req.headers['Content-Type']) + payload = req.serialize() + # We need to remove the ~HTTP/1.1 prefix along with the added content-length + payload = payload[payload.index(b'--'):] + else: + part_message.add_header("Content-Type", "application/http") + part_message.add_header("Content-Transfer-Encoding", "binary") + part_message.add_header("Content-ID", str(content_index)) + payload = req.serialize() + content_index += 1 + part_message.set_payload(payload) main_message.attach(part_message) try: @@ -435,6 +449,7 @@ def prepare_multipart_body(self): self.headers["Content-Type"] = ( "multipart/mixed; boundary=" + main_message.get_boundary() ) + return content_index def serialize(self): # type: () -> bytes @@ -485,6 +500,31 @@ def text(self, encoding=None): encoding = "utf-8-sig" return self.body().decode(encoding) + def _decode_parts(self, message, http_response_type, requests): + # type: (Message, Type[_HttpResponseBase], List[HttpRequest]) -> List[HttpResponse] + """Rebuild an HTTP response from pure string.""" + responses = [] + for index, raw_reponse in enumerate(message.get_payload()): + content_type = raw_reponse.get_content_type() + if content_type == "application/http": + responses.append( + _deserialize_response( + raw_reponse.get_payload(decode=True), + requests[index], + http_response_type=http_response_type, + ) + ) + elif content_type == "multipart/mixed" and requests[index].multipart_mixed_info: + # The message batch contains one or more change sets + changeset_requests = requests[index].multipart_mixed_info[0] # type: ignore + changeset_responses = self._decode_parts(raw_reponse, http_response_type, changeset_requests) + responses.extend(changeset_responses) + else: + raise ValueError( + "Multipart doesn't support part other than application/http for now" + ) + return responses + def _get_raw_parts(self, http_response_type=None): # type (Optional[Type[_HttpResponseBase]]) -> Iterator[HttpResponse] """Assuming this body is multipart, return the iterator or parts. @@ -503,26 +543,9 @@ def _get_raw_parts(self, http_response_type=None): + b"\r\n\r\n" + body_as_bytes ) - message = message_parser(http_body) # type: Message - - # Rebuild an HTTP response from pure string requests = self.request.multipart_mixed_info[0] # type: List[HttpRequest] - responses = [] - for request, raw_reponse in zip(requests, message.get_payload()): - if raw_reponse.get_content_type() == "application/http": - responses.append( - _deserialize_response( - raw_reponse.get_payload(decode=True), - request, - http_response_type=http_response_type, - ) - ) - else: - raise ValueError( - "Multipart doesn't support part other than application/http for now" - ) - return responses + return self._decode_parts(message, http_response_type, requests) class HttpResponse(_HttpResponseBase): # pylint: disable=abstract-method diff --git a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py index 58f6761b4c5a..22810b8afffd 100644 --- a/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/azure_core_asynctests/test_basic_transport.py @@ -18,6 +18,28 @@ import pytest +# transport = mock.MagicMock(spec=AsyncHttpTransport) +# MagicMock support async cxt manager only after 3.8 +# https://github.com/python/cpython/pull/9296 + +class MockAsyncHttpTransport(AsyncHttpTransport): + async def __aenter__(self): return self + async def __aexit__(self, *args): pass + async def open(self): pass + async def close(self): pass + async def send(self, request, **kwargs): pass + + +class MockResponse(AsyncHttpResponse): + def __init__(self, request, body, content_type): + super(MockResponse, self).__init__(request, None) + self._body = body + self.content_type = content_type + + def body(self): + return self._body + + @pytest.mark.asyncio async def test_basic_options_aiohttp(): @@ -31,18 +53,6 @@ async def test_basic_options_aiohttp(): @pytest.mark.asyncio async def test_multipart_send(): - - # transport = mock.MagicMock(spec=AsyncHttpTransport) - # MagicMock support async cxt manager only after 3.8 - # https://github.com/python/cpython/pull/9296 - - class MockAsyncHttpTransport(AsyncHttpTransport): - async def __aenter__(self): return self - async def __aexit__(self, *args): pass - async def open(self): pass - async def close(self): pass - async def send(self, request, **kwargs): pass - transport = MockAsyncHttpTransport() class RequestPolicy(object): @@ -90,17 +100,6 @@ async def on_request(self, request): @pytest.mark.asyncio async def test_multipart_send_with_context(): - # transport = mock.MagicMock(spec=AsyncHttpTransport) - # MagicMock support async cxt manager only after 3.8 - # https://github.com/python/cpython/pull/9296 - - class MockAsyncHttpTransport(AsyncHttpTransport): - async def __aenter__(self): return self - async def __aexit__(self, *args): pass - async def open(self): pass - async def close(self): pass - async def send(self, request, **kwargs): pass - transport = MockAsyncHttpTransport() header_policy = HeadersPolicy() @@ -150,16 +149,288 @@ async def on_request(self, request): @pytest.mark.asyncio -async def test_multipart_receive(): +async def test_multipart_send_with_one_changeset(): + transport = MockAsyncHttpTransport() + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ] + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + *requests, + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_multiple_changesets(): + transport = MockAsyncHttpTransport() + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3"), + boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314" + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset1, + changeset2, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'DELETE /container3/blob3 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_combination_changeset_first(): + transport = MockAsyncHttpTransport() + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset, + HttpRequest("DELETE", "/container2/blob2"), + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_combination_changeset_last(): + transport = MockAsyncHttpTransport() + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2"), + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.asyncio +async def test_multipart_send_with_combination_changeset_middle(): + transport = MockAsyncHttpTransport() + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container1/blob1"), + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2"), + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + async with AsyncPipeline(transport) as pipeline: + await pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) - class MockResponse(AsyncHttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - def body(self): - return self._body +@pytest.mark.asyncio +async def test_multipart_receive(): class ResponsePolicy(object): def on_response(self, request, response): @@ -232,22 +503,349 @@ async def on_response(self, request, response): @pytest.mark.asyncio -async def test_multipart_receive_with_bom(): +async def test_multipart_receive_with_one_changeset(): + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ) - req0 = HttpRequest("DELETE", "/container0/blob0") + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(changeset) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 2 + + res0 = parts[0] + assert res0.status_code == 202 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_multiple_changesets(): + + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3") + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(req0) + request.set_multipart_mixed(changeset1, changeset2) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'HTTP/1.1 409\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) - class MockResponse(AsyncHttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) - def body(self): - return self._body + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 4 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + assert parts[3].status_code == 409 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_combination_changeset_first(): + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(changeset, HttpRequest("DELETE", "/container2/blob2")) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_combination_changeset_middle(): + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed(HttpRequest("DELETE", "/container1/blob1")) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2") + ) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_combination_changeset_last(): + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(HttpRequest("DELETE", "/container0/blob0"), changeset) + + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + async for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +@pytest.mark.asyncio +async def test_multipart_receive_with_bom(): + + req0 = HttpRequest("DELETE", "/container0/blob0") + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(req0) body_as_bytes = ( b"--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\n" b"Content-Type: application/http\n" @@ -288,16 +886,6 @@ async def test_recursive_multipart_receive(): request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed(req0) - - class MockResponse(AsyncHttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - - def body(self): - return self._body - internal_body_as_str = ( "--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n" "Content-Type: application/http\r\n" diff --git a/sdk/core/azure-core/tests/test_basic_transport.py b/sdk/core/azure-core/tests/test_basic_transport.py index 25909791f9fa..d3d13eac2089 100644 --- a/sdk/core/azure-core/tests/test_basic_transport.py +++ b/sdk/core/azure-core/tests/test_basic_transport.py @@ -21,6 +21,16 @@ import pytest +class MockResponse(HttpResponse): + def __init__(self, request, body, content_type): + super(MockResponse, self).__init__(request, None) + self._body = body + self.content_type = content_type + + def body(self): + return self._body + + @pytest.mark.skipif(sys.version_info < (3, 6), reason="Multipart serialization not supported on 2.7 + dict order not deterministic on 3.5") def test_http_request_serialization(): # Method + Url @@ -212,7 +222,6 @@ def test_multipart_send(): @pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") def test_multipart_send_with_context(): - transport = mock.MagicMock(spec=HttpTransport) header_policy = HeadersPolicy({ @@ -259,16 +268,341 @@ def test_multipart_send_with_context(): ) -def test_multipart_receive(): +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_one_changeset(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + requests = [ + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ] + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + *requests, + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset, + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_multiple_changesets(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3"), + policies=[header_policy], + boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314" + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset1, + changeset2, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525", + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'DELETE /container3/blob3 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_combination_changeset_first(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1"), + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + changeset, + HttpRequest("DELETE", "/container2/blob2"), + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_combination_changeset_last(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2"), + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) + +@pytest.mark.skipif(sys.version_info < (3, 0), reason="Multipart serialization not supported on 2.7") +def test_multipart_send_with_combination_changeset_middle(): + + transport = mock.MagicMock(spec=HttpTransport) + + header_policy = HeadersPolicy({ + 'x-ms-date': 'Thu, 14 Jun 2018 16:46:54 GMT' + }) + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container1/blob1"), + policies=[header_policy], + boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2"), + policies=[header_policy], + boundary="batch_357de4f7-6d0b-4e02-8cd2-6361411a9525" + ) + + with Pipeline(transport) as pipeline: + pipeline.run(request) + + assert request.body == ( + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'DELETE /container0/blob0 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: multipart/mixed; boundary=changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'DELETE /container1/blob1 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'DELETE /container2/blob2 HTTP/1.1\r\n' + b'x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT\r\n' + b'\r\n' + b'\r\n' + b'--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + ) - class MockResponse(HttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - def body(self): - return self._body +def test_multipart_receive(): class ResponsePolicy(object): def on_response(self, request, response): @@ -330,22 +664,347 @@ def on_response(self, request, response): assert res1.status_code == 404 assert res1.headers['x-ms-fun'] == 'true' -def test_multipart_receive_with_bom(): - req0 = HttpRequest("DELETE", "/container0/blob0") +def test_multipart_receive_with_one_changeset(): + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ) request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") - request.set_multipart_mixed(req0) + request.set_multipart_mixed(changeset) + + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202 Accepted\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 2 + + res0 = parts[0] + assert res0.status_code == 202 + + +def test_multipart_receive_with_multiple_changesets(): + + changeset1 = HttpRequest(None, None) + changeset1.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ) + changeset2 = HttpRequest(None, None) + changeset2.set_multipart_mixed( + HttpRequest("DELETE", "/container2/blob2"), + HttpRequest("DELETE", "/container3/blob3") + ) - class MockResponse(HttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(changeset1, changeset2) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314"\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 3\r\n' + b'\r\n' + b'HTTP/1.1 409\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_8b9e487e-a353-4dcb-a6f4-0688191e0314--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 4 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + assert parts[3].status_code == 409 - def body(self): - return self._body +def test_multipart_receive_with_combination_changeset_first(): + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + HttpRequest("DELETE", "/container1/blob1") + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(changeset, HttpRequest("DELETE", "/container2/blob2")) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +def test_multipart_receive_with_combination_changeset_middle(): + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed(HttpRequest("DELETE", "/container1/blob1")) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed( + HttpRequest("DELETE", "/container0/blob0"), + changeset, + HttpRequest("DELETE", "/container2/blob2") + ) + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +def test_multipart_receive_with_combination_changeset_last(): + + changeset = HttpRequest(None, None) + changeset.set_multipart_mixed( + HttpRequest("DELETE", "/container1/blob1"), + HttpRequest("DELETE", "/container2/blob2") + ) + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(HttpRequest("DELETE", "/container0/blob0"), changeset) + + body_as_bytes = ( + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 2\r\n' + b'\r\n' + b'HTTP/1.1 200\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n' + b'Content-Type: multipart/mixed; boundary="changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525"\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 0\r\n' + b'\r\n' + b'HTTP/1.1 202\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525\r\n' + b'Content-Type: application/http\r\n' + b'Content-Transfer-Encoding: binary\r\n' + b'Content-ID: 1\r\n' + b'\r\n' + b'HTTP/1.1 404\r\n' + b'x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r\n' + b'x-ms-version: 2018-11-09\r\n' + b'\r\n' + b'\r\n' + b'--changeset_357de4f7-6d0b-4e02-8cd2-6361411a9525--\r\n' + b'\r\n' + b'--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r\n' + ) + + response = MockResponse( + request, + body_as_bytes, + "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" + ) + + parts = [] + for part in response.parts(): + parts.append(part) + assert len(parts) == 3 + assert parts[0].status_code == 200 + assert parts[1].status_code == 202 + assert parts[2].status_code == 404 + + +def test_multipart_receive_with_bom(): + + req0 = HttpRequest("DELETE", "/container0/blob0") + + request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") + request.set_multipart_mixed(req0) body_as_bytes = ( b"--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\n" b"Content-Type: application/http\n" @@ -383,16 +1042,6 @@ def test_recursive_multipart_receive(): request = HttpRequest("POST", "http://account.blob.core.windows.net/?comp=batch") request.set_multipart_mixed(req0) - - class MockResponse(HttpResponse): - def __init__(self, request, body, content_type): - super(MockResponse, self).__init__(request, None) - self._body = body - self.content_type = content_type - - def body(self): - return self._body - internal_body_as_str = ( "--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r\n" "Content-Type: application/http\r\n" @@ -435,6 +1084,7 @@ def body(self): internal_response0 = internal_response[0] assert internal_response0.status_code == 400 + def test_close_unopened_transport(): transport = RequestsTransport() transport.close()