"""Error scenario tests for abacus_file_upload.connectors.s3 module.""" from unittest.mock import Mock, patch import pytest from botocore.exceptions import ( BotoCoreError, ClientError, EndpointConnectionError, ReadTimeoutError, ) from abacus_file_upload.connectors.s3 import S3Connector @pytest.fixture def mock_s3_client(): """Create a mock S3 client.""" return Mock() @pytest.fixture def s3_connector(mock_s3_client): """Create S3Connector instance with mock client.""" return S3Connector(s3_client=mock_s3_client) class TestPresignedURLErrors: """Tests for presigned URL generation error scenarios.""" def test_generate_put_presigned_url_no_credentials( self, s3_connector, mock_s3_client ): """Test generating presigned URL without credentials.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( { 'Error': { 'Code': 'NoCredentialsError', 'Message': 'Unable to locate credentials', } }, 'generate_presigned_url', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', ) assert 'NoCredentialsError' in str(exc_info.value) def test_generate_put_presigned_url_invalid_bucket( self, s3_connector, mock_s3_client ): """Test generating presigned URL with invalid bucket name.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( { 'Error': { 'Code': 'InvalidBucketName', 'Message': 'The specified bucket is not valid', } }, 'generate_presigned_url', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_put_presigned_url( bucket='invalid_bucket_name', key='test-key', ) assert 'InvalidBucketName' in str(exc_info.value) def test_generate_get_presigned_url_bucket_not_found( self, s3_connector, mock_s3_client ): """Test generating download URL for non-existent bucket.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( { 'Error': { 'Code': 'NoSuchBucket', 'Message': 'The specified bucket does not exist', } }, 'generate_presigned_url', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_get_presigned_url( bucket='nonexistent-bucket', key='test-key', ) assert 'NoSuchBucket' in str(exc_info.value) def test_generate_presigned_url_access_denied(self, s3_connector, mock_s3_client): """Test generating presigned URL without permissions.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( {'Error': {'Code': 'AccessDenied', 'Message': 'Access Denied'}}, 'generate_presigned_url', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_put_presigned_url( bucket='restricted-bucket', key='test-key', ) assert 'AccessDenied' in str(exc_info.value) class TestMultipartUploadErrors: """Tests for multipart upload error scenarios.""" def test_initiate_multipart_upload_bucket_not_found( self, s3_connector, mock_s3_client ): """Test initiating multipart upload with non-existent bucket.""" mock_s3_client.create_multipart_upload.side_effect = ClientError( { 'Error': { 'Code': 'NoSuchBucket', 'Message': 'The specified bucket does not exist', } }, 'create_multipart_upload', ) with pytest.raises(ClientError) as exc_info: s3_connector.initiate_multipart_upload( bucket='nonexistent-bucket', key='test-key', ) assert 'NoSuchBucket' in str(exc_info.value) def test_initiate_multipart_upload_access_denied( self, s3_connector, mock_s3_client ): """Test initiating multipart upload without permissions.""" mock_s3_client.create_multipart_upload.side_effect = ClientError( {'Error': {'Code': 'AccessDenied', 'Message': 'Access Denied'}}, 'create_multipart_upload', ) with pytest.raises(ClientError) as exc_info: s3_connector.initiate_multipart_upload( bucket='restricted-bucket', key='test-key', ) assert 'AccessDenied' in str(exc_info.value) def test_generate_multipart_presigned_urls_invalid_upload_id( self, s3_connector, mock_s3_client ): """Test generating multipart URLs with invalid upload ID.""" error = ClientError( { 'Error': { 'Code': 'NoSuchUpload', 'Message': 'The specified upload does not exist', } }, 'upload_part', ) # Mock will be called multiple times (once per part), make first call fail mock_s3_client.generate_presigned_url.side_effect = error with pytest.raises(ClientError) as exc_info: s3_connector.generate_multipart_presigned_urls( bucket='test-bucket', key='test-key', upload_id='invalid-upload-id', total_parts=3, ) assert 'NoSuchUpload' in str(exc_info.value) def test_abort_multipart_upload_no_such_upload(self, s3_connector, mock_s3_client): """Test aborting multipart upload that doesn't exist.""" mock_s3_client.abort_multipart_upload.side_effect = ClientError( { 'Error': { 'Code': 'NoSuchUpload', 'Message': 'The specified upload does not exist', } }, 'abort_multipart_upload', ) with pytest.raises(ClientError) as exc_info: s3_connector.abort_multipart_upload( bucket='test-bucket', key='test-key', upload_id='invalid-upload-id', ) assert 'NoSuchUpload' in str(exc_info.value) def test_complete_multipart_presigned_url_invalid_upload( self, s3_connector, mock_s3_client ): """Test generating complete URL for invalid multipart upload.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( { 'Error': { 'Code': 'NoSuchUpload', 'Message': 'The specified upload does not exist', } }, 'complete_multipart_upload', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_complete_multipart_presigned_url( bucket='test-bucket', key='test-key', upload_id='invalid-upload-id', ) assert 'NoSuchUpload' in str(exc_info.value) class TestObjectOperationErrors: """Tests for object operation error scenarios.""" def test_delete_object_access_denied(self, s3_connector, mock_s3_client): """Test deleting object without permissions.""" mock_s3_client.delete_object.side_effect = ClientError( {'Error': {'Code': 'AccessDenied', 'Message': 'Access Denied'}}, 'delete_object', ) with pytest.raises(ClientError) as exc_info: s3_connector.delete_object(bucket='test-bucket', key='test-key') assert 'AccessDenied' in str(exc_info.value) def test_delete_object_bucket_not_found(self, s3_connector, mock_s3_client): """Test deleting object from non-existent bucket.""" mock_s3_client.delete_object.side_effect = ClientError( { 'Error': { 'Code': 'NoSuchBucket', 'Message': 'The specified bucket does not exist', } }, 'delete_object', ) with pytest.raises(ClientError) as exc_info: s3_connector.delete_object(bucket='nonexistent-bucket', key='test-key') assert 'NoSuchBucket' in str(exc_info.value) def test_get_object_metadata_object_not_found(self, s3_connector, mock_s3_client): """Test getting metadata for non-existent object.""" mock_s3_client.head_object.side_effect = ClientError( {'Error': {'Code': '404', 'Message': 'Not Found'}}, 'head_object' ) with pytest.raises(ClientError) as exc_info: s3_connector.get_object_metadata(bucket='test-bucket', key='missing-key') assert '404' in str(exc_info.value) def test_get_object_metadata_access_denied(self, s3_connector, mock_s3_client): """Test getting metadata without permissions.""" mock_s3_client.head_object.side_effect = ClientError( {'Error': {'Code': 'AccessDenied', 'Message': 'Access Denied'}}, 'head_object', ) with pytest.raises(ClientError) as exc_info: s3_connector.get_object_metadata(bucket='restricted-bucket', key='test-key') assert 'AccessDenied' in str(exc_info.value) class TestConnectionErrors: """Tests for connection and network error scenarios.""" def test_presigned_url_connection_error(self, s3_connector, mock_s3_client): """Test presigned URL generation with connection error.""" mock_s3_client.generate_presigned_url.side_effect = EndpointConnectionError( endpoint_url='https://s3.amazonaws.com' ) with pytest.raises(EndpointConnectionError): s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', ) def test_multipart_upload_timeout(self, s3_connector, mock_s3_client): """Test multipart upload initiation with timeout.""" mock_s3_client.create_multipart_upload.side_effect = ReadTimeoutError( endpoint_url='https://s3.amazonaws.com' ) with pytest.raises(ReadTimeoutError): s3_connector.initiate_multipart_upload( bucket='test-bucket', key='test-key', ) def test_object_exists_connection_error(self, s3_connector, mock_s3_client): """Test object_exists with connection error.""" mock_s3_client.head_object.side_effect = EndpointConnectionError( endpoint_url='https://s3.amazonaws.com' ) with pytest.raises(EndpointConnectionError): s3_connector.object_exists(bucket='test-bucket', key='test-key') def test_abort_multipart_connection_error(self, s3_connector, mock_s3_client): """Test aborting multipart upload with connection error.""" mock_s3_client.abort_multipart_upload.side_effect = EndpointConnectionError( endpoint_url='https://s3.amazonaws.com' ) with pytest.raises(EndpointConnectionError): s3_connector.abort_multipart_upload( bucket='test-bucket', key='test-key', upload_id='test-upload-id', ) class TestBotoCoreErrors: """Tests for generic BotoCore error scenarios.""" def test_generic_botocore_error_on_put_url(self, s3_connector, mock_s3_client): """Test generic BotoCoreError during PUT URL generation.""" mock_s3_client.generate_presigned_url.side_effect = BotoCoreError() with pytest.raises(BotoCoreError): s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', ) def test_generic_botocore_error_on_multipart_init( self, s3_connector, mock_s3_client ): """Test generic BotoCoreError during multipart initiation.""" mock_s3_client.create_multipart_upload.side_effect = BotoCoreError() with pytest.raises(BotoCoreError): s3_connector.initiate_multipart_upload( bucket='test-bucket', key='test-key', ) def test_generic_botocore_error_on_delete(self, s3_connector, mock_s3_client): """Test generic BotoCoreError during object deletion.""" mock_s3_client.delete_object.side_effect = BotoCoreError() with pytest.raises(BotoCoreError): s3_connector.delete_object(bucket='test-bucket', key='test-key') class TestRateLimitingErrors: """Tests for S3 rate limiting and throttling errors.""" def test_slowdown_error_on_presigned_url(self, s3_connector, mock_s3_client): """Test SlowDown error during presigned URL generation.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( { 'Error': { 'Code': 'SlowDown', 'Message': 'Please reduce your request rate', } }, 'generate_presigned_url', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', ) assert 'SlowDown' in str(exc_info.value) def test_throttling_error_on_multipart(self, s3_connector, mock_s3_client): """Test throttling error during multipart upload.""" mock_s3_client.create_multipart_upload.side_effect = ClientError( { 'Error': { 'Code': 'RequestLimitExceeded', 'Message': 'The request rate has been exceeded', } }, 'create_multipart_upload', ) with pytest.raises(ClientError) as exc_info: s3_connector.initiate_multipart_upload( bucket='test-bucket', key='test-key', ) assert 'RequestLimitExceeded' in str(exc_info.value) class TestInvalidParameterErrors: """Tests for invalid parameter error scenarios.""" def test_invalid_part_number(self, s3_connector, mock_s3_client): """Test generating multipart URLs with invalid part numbers.""" error = ClientError( { 'Error': { 'Code': 'InvalidPartNumber', 'Message': 'Part number must be between 1 and 10000', } }, 'upload_part', ) # Mock will be called for each part, make first call fail mock_s3_client.generate_presigned_url.side_effect = error with pytest.raises(ClientError) as exc_info: s3_connector.generate_multipart_presigned_urls( bucket='test-bucket', key='test-key', upload_id='test-upload-id', total_parts=10001, # Very large number of parts ) assert 'InvalidPartNumber' in str(exc_info.value) def test_invalid_object_state(self, s3_connector, mock_s3_client): """Test operation on object in invalid state.""" mock_s3_client.head_object.side_effect = ClientError( { 'Error': { 'Code': 'InvalidObjectState', 'Message': "The operation is not valid for the object's storage class", } }, 'head_object', ) with pytest.raises(ClientError) as exc_info: s3_connector.get_object_metadata(bucket='test-bucket', key='archived-key') assert 'InvalidObjectState' in str(exc_info.value) class TestServerErrors: """Tests for S3 server error scenarios.""" def test_internal_server_error(self, s3_connector, mock_s3_client): """Test handling of S3 internal server error.""" mock_s3_client.generate_presigned_url.side_effect = ClientError( { 'Error': { 'Code': 'InternalError', 'Message': 'We encountered an internal error. Please try again.', } }, 'generate_presigned_url', ) with pytest.raises(ClientError) as exc_info: s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', ) assert 'InternalError' in str(exc_info.value) def test_service_unavailable_error(self, s3_connector, mock_s3_client): """Test handling of S3 service unavailable error.""" mock_s3_client.create_multipart_upload.side_effect = ClientError( { 'Error': { 'Code': 'ServiceUnavailable', 'Message': 'Service is temporarily unavailable', } }, 'create_multipart_upload', ) with pytest.raises(ClientError) as exc_info: s3_connector.initiate_multipart_upload( bucket='test-bucket', key='test-key', ) assert 'ServiceUnavailable' in str(exc_info.value)