"""Unit tests for S3Connection.""" from unittest.mock import Mock, patch import pytest from botocore.exceptions import ClientError from adjustments_json_import.connectors.s3 import ( S3Connection, get_s3_client, get_s3_connection, ) from adjustments_json_import.error_handling import S3FileNotFoundError, TransientError class TestS3Connection: """Tests for S3Connection methods.""" @pytest.fixture def mock_s3_client(self): """Mock S3 client.""" return Mock() @pytest.fixture def s3_connector(self, mock_s3_client): """S3Connection instance.""" return S3Connection(mock_s3_client) def test_download_file_success(self, s3_connector, mock_s3_client): """Test successful file download.""" bucket = 'test-bucket' key = 'path/to/file.xlsx' local_path = '/tmp/file.xlsx' s3_connector.download_file(bucket, key, local_path) mock_s3_client.download_file.assert_called_once_with( Bucket=bucket, Key=key, Filename=local_path ) def test_download_file_handles_transient_errors(self, s3_connector, mock_s3_client): """Test download_file converts S3 transient errors to TransientError.""" client_error = ClientError( { 'Error': {'Code': 'SlowDown', 'Message': 'Reduce your request rate'}, 'ResponseMetadata': {'HTTPStatusCode': 503}, }, 'DownloadFile', ) mock_s3_client.download_file.side_effect = client_error bucket = 'test-bucket' key = 'path/to/file.xlsx' local_path = '/tmp/file.xlsx' with pytest.raises(TransientError) as exc_info: s3_connector.download_file(bucket, key, local_path) expected_message = ( f'S3 service error ({client_error.response["Error"]["Code"]}): ' f'{client_error.response["Error"]["Message"]}' ) assert expected_message in str(exc_info.value) def test_download_file_handles_not_found_error(self, s3_connector, mock_s3_client): """Test download_file raises S3ObjectNotFoundError for 404.""" mock_s3_client.download_file.side_effect = ClientError( { 'Error': {'Code': 'NoSuchKey', 'Message': 'File not found'}, 'ResponseMetadata': {'HTTPStatusCode': 404}, }, 'DownloadFile', ) bucket = 'test-bucket' key = 'path/to/file.xlsx' local_path = '/tmp/file.xlsx' with pytest.raises(S3FileNotFoundError) as exc_info: s3_connector.download_file(bucket, key, local_path) assert 'NoSuchKey' in str(exc_info.value) def test_download_file_propagates_other_errors(self, s3_connector, mock_s3_client): """Test download_file propagates other S3 errors (e.g. 403).""" mock_s3_client.download_file.side_effect = ClientError( { 'Error': {'Code': 'AccessDenied', 'Message': 'Access denied'}, 'ResponseMetadata': {'HTTPStatusCode': 403}, }, 'DownloadFile', ) bucket = 'test-bucket' key = 'path/to/file.xlsx' local_path = '/tmp/file.xlsx' with pytest.raises(ClientError) as exc_info: s3_connector.download_file(bucket, key, local_path) assert 'AccessDenied' in str(exc_info.value) class TestS3ClientFactory: """Tests for S3 client factory functions.""" @patch('adjustments_json_import.connectors.s3.boto3.client') def test_get_s3_client_with_default_config(self, mock_boto_client): """Test get_s3_client creates client with default config.""" mock_client = Mock() mock_boto_client.return_value = mock_client result = get_s3_client() assert result == mock_client mock_boto_client.assert_called_once() call_args = mock_boto_client.call_args assert call_args[0][0] == 's3' assert call_args[1]['config'].signature_version == 's3v4' @patch('adjustments_json_import.connectors.s3.boto3.client') def test_get_s3_client_with_custom_config(self, mock_boto_client): """Test get_s3_client uses provided config.""" from botocore.client import Config custom_config = Config(signature_version='s3v2') mock_client = Mock() mock_boto_client.return_value = mock_client result = get_s3_client(custom_config) assert result == mock_client mock_boto_client.assert_called_once_with('s3', config=custom_config) @patch('adjustments_json_import.connectors.s3.get_s3_client') def test_get_s3_connection_with_default_client(self, mock_get_client): """Test get_s3_connection creates connector with default client.""" mock_client = Mock() mock_get_client.return_value = mock_client result = get_s3_connection() assert isinstance(result, S3Connection) assert result.s3_client == mock_client mock_get_client.assert_called_once() def test_get_s3_connection_with_provided_client(self): """Test get_s3_connection uses provided client.""" mock_client = Mock() result = get_s3_connection(mock_client) assert isinstance(result, S3Connection) assert result.s3_client == mock_client