Skip to content

Commit 3dcbb23

Browse files
[Storage] Mocking Transport Tests & Fixes (#38948)
1 parent 1b7445c commit 3dcbb23

File tree

13 files changed

+600
-18
lines changed

13 files changed

+600
-18
lines changed

sdk/storage/azure-storage-blob/azure/storage/blob/_shared/policies_async.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ async def is_checksum_retry(response):
4646
# retry if invalid content md5
4747
if response.context.get('validate_content', False) and response.http_response.headers.get('content-md5'):
4848
try:
49-
await response.http_response.read() # Load the body in memory and close the socket
49+
await response.http_response.load_body() # Load the body in memory and close the socket
5050
except (StreamClosedError, StreamConsumedError):
5151
pass
5252
computed_md5 = response.http_request.headers.get('content-md5', None) or \
53-
encode_base64(StorageContentValidation.get_content_md5(response.http_response.content))
53+
encode_base64(StorageContentValidation.get_content_md5(response.http_response.body()))
5454
if response.http_response.headers['content-md5'] != computed_md5:
5555
return True
5656
return False

sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
async def process_content(data: Any, start_offset: int, end_offset: int, encryption: Dict[str, Any]) -> bytes:
4747
if data is None:
4848
raise ValueError("Response cannot be None.")
49-
await data.response.read()
50-
content = cast(bytes, data.response.content)
49+
await data.response.load_body()
50+
content = cast(bytes, data.response.body())
5151
if encryption.get('key') is not None or encryption.get('resolver') is not None:
5252
try:
5353
return decrypt_blob(

sdk/storage/azure-storage-blob/tests/test_common_blob_async.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
from devtools_testutils.aio import recorded_by_proxy_async
5454
from devtools_testutils.storage.aio import AsyncStorageRecordedTestCase
5555
from settings.testcase import BlobPreparer
56-
from test_helpers_async import AsyncStream
56+
from test_helpers_async import AsyncStream, MockStorageTransport
5757

5858
# ------------------------------------------------------------------------------
5959
TEST_CONTAINER_PREFIX = 'container'
@@ -3455,4 +3455,59 @@ async def test_upload_blob_partial_stream_chunked(self, **kwargs):
34553455
# Assert
34563456
result = await (await blob.download_blob()).readall()
34573457
assert result == data[:length]
3458-
# ------------------------------------------------------------------------------
3458+
3459+
@BlobPreparer()
3460+
async def test_mock_transport_no_content_validation(self, **kwargs):
3461+
storage_account_name = kwargs.pop("storage_account_name")
3462+
storage_account_key = kwargs.pop("storage_account_key")
3463+
3464+
transport = MockStorageTransport()
3465+
blob_client = BlobClient(
3466+
self.account_url(storage_account_name, "blob"),
3467+
container_name='test_cont',
3468+
blob_name='test_blob',
3469+
credential=storage_account_key,
3470+
transport=transport,
3471+
retry_total=0
3472+
)
3473+
3474+
content = await blob_client.download_blob()
3475+
assert content is not None
3476+
3477+
props = await blob_client.get_blob_properties()
3478+
assert props is not None
3479+
3480+
data = b"Hello Async World!"
3481+
stream = AsyncStream(data)
3482+
resp = await blob_client.upload_blob(stream, overwrite=True)
3483+
assert resp is not None
3484+
3485+
blob_data = await (await blob_client.download_blob()).read()
3486+
assert blob_data == b"Hello Async World!" # data is fixed by mock transport
3487+
3488+
resp = await blob_client.delete_blob()
3489+
assert resp is None
3490+
3491+
@BlobPreparer()
3492+
async def test_mock_transport_with_content_validation(self, **kwargs):
3493+
storage_account_name = kwargs.pop("storage_account_name")
3494+
storage_account_key = kwargs.pop("storage_account_key")
3495+
3496+
transport = MockStorageTransport()
3497+
blob_client = BlobClient(
3498+
self.account_url(storage_account_name, "blob"),
3499+
container_name='test_cont',
3500+
blob_name='test_blob',
3501+
credential=storage_account_key,
3502+
transport=transport,
3503+
retry_total=0
3504+
)
3505+
3506+
data = b"Hello Async World!"
3507+
stream = AsyncStream(data)
3508+
resp = await blob_client.upload_blob(stream, overwrite=True, validate_content=True)
3509+
assert resp is not None
3510+
3511+
blob_data = await (await blob_client.download_blob(validate_content=True)).read()
3512+
assert blob_data == b"Hello Async World!" # data is fixed by mock transport
3513+
# ------------------------------------------------------------------------------

sdk/storage/azure-storage-blob/tests/test_helpers_async.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
# license information.
55
# --------------------------------------------------------------------------
66
from io import IOBase, UnsupportedOperation
7-
from typing import Optional
7+
from typing import Any, Dict, Optional
8+
9+
from azure.core.pipeline.transport import AsyncHttpTransport
10+
from azure.core.rest import HttpRequest
11+
from azure.core.rest._aiohttp import RestAioHttpTransportResponse
12+
from aiohttp import ClientResponse
813

914

1015
class ProgressTracker:
@@ -60,3 +65,110 @@ async def read(self, size: int = -1) -> bytes:
6065
self._offset += len(data)
6166

6267
return data
68+
69+
70+
class MockAioHttpClientResponse(ClientResponse):
71+
def __init__(
72+
self, url: str,
73+
body_bytes: bytes,
74+
headers: Dict[str, Any],
75+
status: int = 200,
76+
reason: str = "OK"
77+
) -> None:
78+
super(MockAioHttpClientResponse).__init__()
79+
self._url = url
80+
self._body = body_bytes
81+
self._headers = headers
82+
self._cache = {}
83+
self._loop = None
84+
self.status = status
85+
self.reason = reason
86+
87+
88+
class MockStorageTransport(AsyncHttpTransport):
89+
"""
90+
This transport returns legacy http response objects from azure core and is
91+
intended only to test our backwards compatibility support.
92+
"""
93+
async def send(self, request: HttpRequest, **kwargs: Any) -> RestAioHttpTransportResponse:
94+
if request.method == 'GET':
95+
# download_blob
96+
headers = {
97+
"Content-Type": "application/octet-stream",
98+
"Content-Range": "bytes 0-17/18",
99+
"Content-Length": "18",
100+
}
101+
102+
if "x-ms-range-get-content-md5" in request.headers:
103+
headers["Content-MD5"] = "I3pVbaOCUTom+G9F9uKFoA=="
104+
105+
rest_response = RestAioHttpTransportResponse(
106+
request=request,
107+
internal_response=MockAioHttpClientResponse(
108+
request.url,
109+
b"Hello Async World!",
110+
headers,
111+
),
112+
decompress=False
113+
)
114+
elif request.method == 'HEAD':
115+
# get_blob_properties
116+
rest_response = RestAioHttpTransportResponse(
117+
request=request,
118+
internal_response=MockAioHttpClientResponse(
119+
request.url,
120+
b"",
121+
{
122+
"Content-Type": "application/octet-stream",
123+
"Content-Length": "1024",
124+
},
125+
),
126+
decompress=False
127+
)
128+
elif request.method == 'PUT':
129+
# upload_blob
130+
rest_response = RestAioHttpTransportResponse(
131+
request=request,
132+
internal_response=MockAioHttpClientResponse(
133+
request.url,
134+
b"",
135+
{
136+
"Content-Length": "0",
137+
},
138+
201,
139+
"Created"
140+
),
141+
decompress=False
142+
)
143+
elif request.method == 'DELETE':
144+
# delete_blob
145+
rest_response = RestAioHttpTransportResponse(
146+
request=request,
147+
internal_response=MockAioHttpClientResponse(
148+
request.url,
149+
b"",
150+
{
151+
"Content-Length": "0",
152+
},
153+
202,
154+
"Accepted"
155+
),
156+
decompress=False
157+
)
158+
else:
159+
raise ValueError("The request is not accepted as part of MockStorageTransport.")
160+
161+
await rest_response.read()
162+
return rest_response
163+
164+
async def __aenter__(self):
165+
return self
166+
167+
async def __aexit__(self, *args):
168+
pass
169+
170+
async def open(self):
171+
pass
172+
173+
async def close(self):
174+
pass

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_deserialize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def process_storage_error(storage_error) -> NoReturn: # pylint:disable=too-many
164164
error_dict = error_body.get('error', {})
165165
elif not error_code:
166166
_LOGGER.warning(
167-
'Unexpected return type % from ContentDecodePolicy.deserialize_from_http_generics.', type(error_body))
167+
'Unexpected return type %s from ContentDecodePolicy.deserialize_from_http_generics.', type(error_body))
168168
error_dict = {'message': str(error_body)}
169169

170170
# If we extracted from a Json or XML response

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/policies_async.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ async def is_checksum_retry(response):
4646
# retry if invalid content md5
4747
if response.context.get('validate_content', False) and response.http_response.headers.get('content-md5'):
4848
try:
49-
await response.http_response.read() # Load the body in memory and close the socket
49+
await response.http_response.load_body() # Load the body in memory and close the socket
5050
except (StreamClosedError, StreamConsumedError):
5151
pass
5252
computed_md5 = response.http_request.headers.get('content-md5', None) or \
53-
encode_base64(StorageContentValidation.get_content_md5(response.http_response.content))
53+
encode_base64(StorageContentValidation.get_content_md5(response.http_response.body()))
5454
if response.http_response.headers['content-md5'] != computed_md5:
5555
return True
5656
return False

sdk/storage/azure-storage-file-datalake/tests/test_file_async.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from devtools_testutils.aio import recorded_by_proxy_async
3636
from devtools_testutils.storage.aio import AsyncStorageRecordedTestCase
3737
from settings.testcase import DataLakePreparer
38+
from test_helpers_async import AsyncStream, MockStorageTransport
3839
# ------------------------------------------------------------------------------
3940

4041
TEST_DIRECTORY_PREFIX = 'directory'
@@ -1536,6 +1537,62 @@ async def test_bad_audience_file_client(self, **kwargs):
15361537
await fc.get_file_properties()
15371538
await fc.upload_data(data, overwrite=True)
15381539

1540+
@DataLakePreparer()
1541+
async def test_mock_transport_no_content_validation(self, **kwargs):
1542+
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
1543+
datalake_storage_account_key = kwargs.pop("datalake_storage_account_key")
1544+
1545+
transport = MockStorageTransport()
1546+
file_client = DataLakeFileClient(
1547+
self.account_url(datalake_storage_account_name, 'dfs'),
1548+
"filesystem/",
1549+
"dir/file.txt",
1550+
credential=datalake_storage_account_key,
1551+
transport=transport,
1552+
retry_total=0
1553+
)
1554+
1555+
data = await file_client.download_file()
1556+
assert data is not None
1557+
1558+
props = await file_client.get_file_properties()
1559+
assert props is not None
1560+
1561+
data = b"Hello Async World!"
1562+
stream = AsyncStream(data)
1563+
resp = await file_client.upload_data(stream, overwrite=True)
1564+
assert resp is not None
1565+
1566+
file_data = await (await file_client.download_file()).read()
1567+
assert file_data == b"Hello Async World!" # data is fixed by mock transport
1568+
1569+
resp = await file_client.delete_file()
1570+
assert resp is not None
1571+
1572+
@DataLakePreparer()
1573+
async def test_mock_transport_with_content_validation(self, **kwargs):
1574+
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
1575+
datalake_storage_account_key = kwargs.pop("datalake_storage_account_key")
1576+
1577+
await self._setUp(datalake_storage_account_name, datalake_storage_account_key)
1578+
1579+
transport = MockStorageTransport()
1580+
file_client = DataLakeFileClient(
1581+
self.account_url(datalake_storage_account_name, 'dfs'),
1582+
"filesystem/",
1583+
"dir/file.txt",
1584+
credential=datalake_storage_account_key,
1585+
transport=transport,
1586+
retry_total=0
1587+
)
1588+
1589+
data = b"Hello Async World!"
1590+
stream = AsyncStream(data)
1591+
resp = await file_client.upload_data(stream, overwrite=True, validate_content=True)
1592+
assert resp is not None
1593+
1594+
file_data = await (await file_client.download_file(validate_content=True)).read()
1595+
assert file_data == b"Hello Async World!" # data is fixed by mock transport
15391596

15401597
# ------------------------------------------------------------------------------
15411598
if __name__ == '__main__':

0 commit comments

Comments
 (0)