"""Tests for abacus_file_upload.connectors.s3 module.""" from datetime import datetime, timezone from unittest.mock import Mock, patch import pytest from botocore.exceptions import ClientError from abacus_file_upload.connectors.s3 import ( S3Connector, get_s3_connector, ) class TestS3Connector: """Tests for S3Connector class.""" @pytest.fixture def mock_s3_client(self): """Create a mock S3 client.""" return Mock() @pytest.fixture def s3_connector(self, mock_s3_client): """Create S3Connector instance with mock client.""" return S3Connector(s3_client=mock_s3_client) def test_generate_put_presigned_url(self, s3_connector, mock_s3_client): """Test generating PUT presigned URL.""" mock_s3_client.generate_presigned_url.return_value = ( 'https://s3.amazonaws.com/...' ) url = s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', metadata={'fileName': 'test.csv'}, content_md5='XUFAKrxLKna5cZ2REBfFkg==', content_type='text/csv', ) assert url == 'https://s3.amazonaws.com/...' mock_s3_client.generate_presigned_url.assert_called_once() call_args = mock_s3_client.generate_presigned_url.call_args assert call_args[1]['ClientMethod'] == 'put_object' assert 'Metadata' in call_args[1]['Params'] assert 'ContentMD5' in call_args[1]['Params'] assert 'ContentType' in call_args[1]['Params'] def test_generate_put_presigned_url_minimal(self, s3_connector, mock_s3_client): """Test generating PUT presigned URL with minimal parameters.""" mock_s3_client.generate_presigned_url.return_value = ( 'https://s3.amazonaws.com/...' ) url = s3_connector.generate_put_presigned_url( bucket='test-bucket', key='test-key', ) assert url == 'https://s3.amazonaws.com/...' call_args = mock_s3_client.generate_presigned_url.call_args assert 'Metadata' not in call_args[1]['Params'] assert 'ContentMD5' not in call_args[1]['Params'] def test_generate_get_presigned_url(self, s3_connector, mock_s3_client): """Test generating GET presigned URL.""" mock_s3_client.generate_presigned_url.return_value = ( 'https://s3.amazonaws.com/download' ) url = s3_connector.generate_get_presigned_url( bucket='test-bucket', key='test-key', expires_in=7200, ) assert url == 'https://s3.amazonaws.com/download' mock_s3_client.generate_presigned_url.assert_called_once() call_args = mock_s3_client.generate_presigned_url.call_args assert call_args[1]['ClientMethod'] == 'get_object' assert call_args[1]['ExpiresIn'] == 7200 def test_initiate_multipart_upload(self, s3_connector, mock_s3_client): """Test initiating multipart upload.""" mock_s3_client.create_multipart_upload.return_value = { 'UploadId': 'test-upload-id' } upload_id = s3_connector.initiate_multipart_upload( bucket='test-bucket', key='test-key', metadata={'fileName': 'test.csv'}, content_type='text/csv', ) assert upload_id == 'test-upload-id' mock_s3_client.create_multipart_upload.assert_called_once_with( Bucket='test-bucket', Key='test-key', Metadata={'fileName': 'test.csv'}, ContentType='text/csv', ) def test_initiate_multipart_upload_no_upload_id(self, s3_connector, mock_s3_client): """Test initiating multipart upload with missing UploadId.""" mock_s3_client.create_multipart_upload.return_value = {} with pytest.raises(ValueError) as exc_info: s3_connector.initiate_multipart_upload( bucket='test-bucket', key='test-key', ) assert 'No upload ID returned' in str(exc_info.value) def test_generate_multipart_presigned_urls(self, s3_connector, mock_s3_client): """Test generating multipart presigned URLs.""" mock_s3_client.generate_presigned_url.side_effect = [ 'https://s3.amazonaws.com/part1', 'https://s3.amazonaws.com/part2', ] presigned_parts = s3_connector.generate_multipart_presigned_urls( bucket='test-bucket', key='test-key', upload_id='test-upload-id', total_parts=2, ) assert len(presigned_parts) == 2 assert presigned_parts[0]['part_number'] == 1 assert presigned_parts[0]['url'] == 'https://s3.amazonaws.com/part1' assert presigned_parts[1]['part_number'] == 2 assert presigned_parts[1]['url'] == 'https://s3.amazonaws.com/part2' assert mock_s3_client.generate_presigned_url.call_count == 2 def test_generate_complete_multipart_presigned_url( self, s3_connector, mock_s3_client ): """Test generating complete multipart presigned URL.""" mock_s3_client.generate_presigned_url.return_value = ( 'https://s3.amazonaws.com/complete' ) url = s3_connector.generate_complete_multipart_presigned_url( bucket='test-bucket', key='test-key', upload_id='test-upload-id', ) assert url == 'https://s3.amazonaws.com/complete' call_args = mock_s3_client.generate_presigned_url.call_args assert call_args[1]['ClientMethod'] == 'complete_multipart_upload' assert call_args[1]['Params']['UploadId'] == 'test-upload-id' def test_abort_multipart_upload(self, s3_connector, mock_s3_client): """Test aborting multipart upload.""" s3_connector.abort_multipart_upload( bucket='test-bucket', key='test-key', upload_id='test-upload-id', ) mock_s3_client.abort_multipart_upload.assert_called_once_with( Bucket='test-bucket', Key='test-key', UploadId='test-upload-id', ) def test_copy_object(self, s3_connector, mock_s3_client): """Test coping object.""" s3_connector.copy_object( source_bucket='source-bucket', target_bucket='target-bucket', key='test-key', ) mock_s3_client.copy_object.assert_called_once_with( Bucket='target-bucket', Key='test-key', CopySource={'Bucket': 'source-bucket', 'Key': 'test-key'}, MetadataDirective='COPY', ) def test_delete_object(self, s3_connector, mock_s3_client): """Test deleting object.""" s3_connector.delete_object( bucket='test-bucket', key='test-key', ) mock_s3_client.delete_object.assert_called_once_with( Bucket='test-bucket', Key='test-key', ) def test_object_exists_true(self, s3_connector, mock_s3_client): """Test checking if object exists (exists).""" mock_s3_client.head_object.return_value = {'ContentLength': 1024} exists = s3_connector.object_exists(bucket='test-bucket', key='test-key') assert exists is True mock_s3_client.head_object.assert_called_once() def test_object_exists_false(self, s3_connector, mock_s3_client): """Test checking if object exists (doesn't exist).""" mock_s3_client.head_object.side_effect = ClientError( {'Error': {'Code': '404'}}, 'head_object' ) exists = s3_connector.object_exists(bucket='test-bucket', key='test-key') assert exists is False def test_object_exists_other_error(self, s3_connector, mock_s3_client): """Test checking if object exists with non-404 error.""" mock_s3_client.head_object.side_effect = ClientError( {'Error': {'Code': 'AccessDenied'}}, 'head_object' ) with pytest.raises(ClientError): s3_connector.object_exists(bucket='test-bucket', key='test-key') def test_get_object_metadata(self, s3_connector, mock_s3_client): """Test getting object metadata.""" mock_s3_client.head_object.return_value = { 'ContentLength': 1024000, 'LastModified': datetime(2025, 1, 15, tzinfo=timezone.utc), 'ETag': '"5d41402abc4b2a76b9719d911017c592"', 'Metadata': {'fileName': 'test.csv'}, } metadata = s3_connector.get_object_metadata( bucket='test-bucket', key='test-key' ) assert metadata['size'] == 1024000 assert metadata['etag'] == '"5d41402abc4b2a76b9719d911017c592"' assert metadata['metadata']['fileName'] == 'test.csv' class TestGetS3Connector: """Tests for get_s3_connector function.""" @patch('abacus_file_upload.connectors.s3.get_s3_client') def test_get_connector(self, mock_get_client): """Test getting S3 connector instance.""" mock_client = Mock() mock_get_client.return_value = mock_client connector = get_s3_connector() assert isinstance(connector, S3Connector) assert connector.s3_client == mock_client mock_get_client.assert_called_once()