Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 179 additions & 2 deletions tests/providers/s3compatsigv4/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import datetime
import aiohttpretty
from http import client
from http import HTTPStatus
from urllib import parse
from unittest import mock

Expand Down Expand Up @@ -1188,15 +1189,191 @@ async def test_chunked_upload_aborted_success(self, provider, upload_parts_heade

with pytest.raises(exceptions.UploadError) as exc:
await provider._chunked_upload(file_stream, path)
# The abort has SUCCEEDED (return value True), so the "manual clean-up"
# warning must NOT be appended to the error message.
msg = 'An unexpected error has occurred during the multi-part upload.'
msg += ' The abort action failed to clean up the temporary file parts generated ' \
'during the upload process. Please manually remove them.'
assert str(exc.value) == ', '.join(['500', msg])

provider._create_upload_session.assert_called_with(path)
provider._upload_parts.assert_called_with(file_stream, path, upload_id)
provider._abort_chunked_upload.assert_called_with(path, upload_id)

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_chunked_upload_abort_failure_appends_warning(self, provider, file_stream,
mock_time):
assert file_stream.size == 6
provider.CONTIGUOUS_UPLOAD_SIZE_LIMIT = 5
provider.CHUNK_SIZE = 2

path = WaterButlerPath('/foobah', prepend=provider.prefix)
upload_id = 'EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u' \
'8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-'

provider._create_upload_session = MockCoroutine()
provider._create_upload_session.return_value = upload_id
provider._upload_parts = MockCoroutine()
provider._upload_parts.side_effect = Exception('error')
provider._abort_chunked_upload = MockCoroutine()
provider._abort_chunked_upload.return_value = False

with pytest.raises(exceptions.UploadError) as exc:
await provider._chunked_upload(file_stream, path)
# The abort has FAILED (return value False), so the "manual clean-up"
# warning must be appended to the error message.
msg = 'An unexpected error has occurred during the multi-part upload.'
msg += ' The abort action failed to clean up the temporary file parts generated ' \
'during the upload process. Please manually remove them.'
assert str(exc.value) == ', '.join(['500', msg])

provider._abort_chunked_upload.assert_called_with(path, upload_id)

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_chunked_upload_storage_quota_exceeded(self, provider, file_stream, mock_time):
assert file_stream.size == 6
provider.CONTIGUOUS_UPLOAD_SIZE_LIMIT = 5
provider.CHUNK_SIZE = 2

path = WaterButlerPath('/foobah', prepend=provider.prefix)
upload_id = 'EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u' \
'8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-'
error_xml = ('<?xml version="1.0" encoding="UTF-8"?>'
'<Error><Code>QuotaExceeded</Code>'
'<Message>The bucket quota has been exceeded</Message></Error>')

provider._create_upload_session = MockCoroutine()
provider._create_upload_session.return_value = upload_id
provider._upload_parts = MockCoroutine()
provider._upload_parts.side_effect = exceptions.UploadError({'response': error_xml},
code=403)
provider._abort_chunked_upload = MockCoroutine()
provider._abort_chunked_upload.return_value = True

with pytest.raises(exceptions.UploadError) as exc:
await provider._chunked_upload(file_stream, path)

# A storage-side quota error must surface as HTTP 507 with an explicit,
# user-readable message, and the multipart session must be aborted.
assert exc.value.code == HTTPStatus.INSUFFICIENT_STORAGE
assert provider.QUOTA_EXCEEDED_MESSAGE in exc.value.message
assert 'QuotaExceeded' in exc.value.message
provider._abort_chunked_upload.assert_called_with(path, upload_id)

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_contiguous_upload_storage_quota_exceeded(self, provider, file_stream,
mock_time):
path = WaterButlerPath('/foobah', prepend=provider.prefix)
error_xml = ('<?xml version="1.0" encoding="UTF-8"?>'
'<Error><Code>QuotaExceeded</Code>'
'<Message>The bucket quota has been exceeded</Message></Error>')

# ``make_request`` raises ``UploadError`` built by
# ``exception_from_response`` when the storage rejects the PUT.
provider.make_request = MockCoroutine()
provider.make_request.side_effect = exceptions.UploadError({'response': error_xml},
code=403)

with pytest.raises(exceptions.UploadError) as exc:
await provider._contiguous_upload(file_stream, path)

# A storage-side quota error must surface as HTTP 507 with an explicit,
# user-readable message.
assert exc.value.code == HTTPStatus.INSUFFICIENT_STORAGE
assert provider.QUOTA_EXCEEDED_MESSAGE in exc.value.message
assert 'QuotaExceeded' in exc.value.message

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_contiguous_upload_other_storage_error(self, provider, file_stream,
mock_time):
path = WaterButlerPath('/foobah', prepend=provider.prefix)
error_xml = ('<?xml version="1.0" encoding="UTF-8"?>'
'<Error><Code>AccessDenied</Code>'
'<Message>Access Denied</Message></Error>')

provider.make_request = MockCoroutine()
provider.make_request.side_effect = exceptions.UploadError({'response': error_xml},
code=403)

with pytest.raises(exceptions.UploadError) as exc:
await provider._contiguous_upload(file_stream, path)

# Non-quota errors keep the storage's status code but get a readable
# message (not the raw XML body).
assert exc.value.code == 403
assert 'AccessDenied' in exc.value.message
assert '<Error' not in exc.value.message

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_contiguous_upload_connection_interrupted(self, provider, file_stream,
mock_time):
import aiohttp

path = WaterButlerPath('/foobah', prepend=provider.prefix)

# Some storages close the connection mid-upload when the quota has been
# exceeded; the raw client error must not propagate as an HTTP 500.
provider.make_request = MockCoroutine()
provider.make_request.side_effect = aiohttp.ClientOSError('Connection reset by peer')

with pytest.raises(exceptions.UploadError) as exc:
await provider._contiguous_upload(file_stream, path)

assert exc.value.code == HTTPStatus.BAD_GATEWAY
assert provider.CONNECTION_INTERRUPTED_MESSAGE in exc.value.message

def test_parse_s3_error_body_non_xml(self, provider):
err = exceptions.UploadError({'response': 'not xml at all'}, code=500)
assert provider._parse_s3_error_body(err) == (None, None)
# A non-parsable error must be returned unchanged by the translator.
assert provider._translate_upload_error(err) is err

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_chunked_upload_create_session_quota_exceeded(self, provider, file_stream,
mock_time):
assert file_stream.size == 6
provider.CONTIGUOUS_UPLOAD_SIZE_LIMIT = 5
provider.CHUNK_SIZE = 2

path = WaterButlerPath('/foobah', prepend=provider.prefix)
error_xml = ('<?xml version="1.0" encoding="UTF-8"?>'
'<Error><Code>QuotaExceeded</Code>'
'<Message>The bucket quota has been exceeded</Message></Error>')

provider._create_upload_session = MockCoroutine()
provider._create_upload_session.side_effect = exceptions.UploadError(
{'response': error_xml}, code=403)
provider._abort_chunked_upload = MockCoroutine()

with pytest.raises(exceptions.UploadError) as exc:
await provider._chunked_upload(file_stream, path)

assert exc.value.code == HTTPStatus.INSUFFICIENT_STORAGE
assert provider.QUOTA_EXCEEDED_MESSAGE in exc.value.message
# No session was created, so nothing must be aborted.
provider._abort_chunked_upload.assert_not_called()

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_create_upload_session_invalid_response(self, provider, mock_time):
path = WaterButlerPath('/foobah', prepend=provider.prefix)

resp = mock.Mock()
resp.read = MockCoroutine(return_value=b'this is not the expected xml')
provider.make_request = MockCoroutine(return_value=resp)

with pytest.raises(exceptions.UploadError) as exc:
await provider._create_upload_session(path)

# A malformed 200-range response must become a controlled error, not a
# raw ExpatError/KeyError propagating as HTTP 500.
assert exc.value.code == HTTPStatus.BAD_GATEWAY
assert 'unexpected response' in exc.value.message

@pytest.mark.asyncio
@pytest.mark.aiohttpretty
async def test_chunked_upload_limit_contiguous(self, provider, file_stream, mock_time):
Expand Down
Loading
Loading