"""Unit tests for RoyaltyAccountingClient.""" from unittest.mock import MagicMock, Mock import pymysql import pytest from src.connectors.mysql import MySQLConnection from src.enums import BatchErrorCode, BatchStatus from src.errors import TransientError from src.repositories.royalty_accounting import RoyaltyAccountingClient @pytest.fixture def mock_adapter_with_transient_error(): """Mock adapter that raises transient connection error.""" mock_adapter = Mock(spec=MySQLConnection) mock_adapter.cursor.side_effect = pymysql.err.OperationalError( 2003, "Can't connect to MySQL server" ) return RoyaltyAccountingClient(mock_adapter) @pytest.fixture def mock_adapter_with_non_transient_error(): """Mock adapter that raises non-transient error.""" mock_cursor = MagicMock() mock_cursor.execute.side_effect = pymysql.err.InternalError( 1000, 'Internal DB error' ) mock_adapter = MagicMock(spec=MySQLConnection) mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor return RoyaltyAccountingClient(mock_adapter) class TestRepositoryInit: """Tests for RoyaltyAccountingClient initialization.""" def test_init_stores_connection(self): """Test repository stores database connection adapter.""" mock_adapter = Mock(spec=MySQLConnection) repo = RoyaltyAccountingClient(mock_adapter) assert repo.conn is mock_adapter class TestRepositoryErrorHandling: """Tests for RoyaltyAccountingClient error handling across all methods.""" @pytest.mark.parametrize( 'method_name,args', [ ('update_batch_status', (123, BatchStatus.VALIDATING)), ('delete_batch_from_staging', (123,)), ], ) def test_repository_methods_handle_transient_errors( self, mock_adapter_with_transient_error, method_name, args ): """Test all repository methods handle transient errors consistently.""" method = getattr(mock_adapter_with_transient_error, method_name) with pytest.raises(TransientError) as exc_info: method(*args) assert 'Database operation failed' in str(exc_info.value) @pytest.mark.parametrize( 'method_name,args', [ ('update_batch_status', (123, BatchStatus.VALIDATING)), ('delete_batch_from_staging', (123,)), ], ) def test_repository_methods_propagate_non_transient_errors( self, mock_adapter_with_non_transient_error, method_name, args ): """Test all repository methods propagate non-transient errors.""" method = getattr(mock_adapter_with_non_transient_error, method_name) with pytest.raises(pymysql.err.InternalError): method(*args) @pytest.mark.parametrize( 'method_name,args,expected_count', [ ('delete_batch_from_staging', (999,), 0), ], ) def test_repository_methods_handle_zero_rows( self, method_name, args, expected_count ): """Test methods correctly return zero when no rows affected.""" mock_cursor = MagicMock() mock_cursor.rowcount = 0 mock_adapter = MagicMock(spec=MySQLConnection) mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor repo = RoyaltyAccountingClient(mock_adapter) method = getattr(repo, method_name) result = method(*args) assert result == expected_count class TestRepositoryUpdateBatchStatus: """Tests for RoyaltyAccountingClient.update_batch_status method.""" def test_update_batch_status_success(self): """Test successful batch status update.""" mock_cursor = MagicMock() mock_cursor.execute.return_value = 1 mock_adapter = MagicMock(spec=MySQLConnection) mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor repo = RoyaltyAccountingClient(mock_adapter) batch_id = 123 status = BatchStatus.VALIDATING rows_affected = repo.update_batch_status(batch_id, status) mock_cursor.execute.assert_called_once() sql = mock_cursor.execute.call_args[0][0] params = mock_cursor.execute.call_args[0][1] assert 'UPDATE worksheet_flowthrough_batch' in sql assert 'SET batch_status = %s' in sql assert 'WHERE worksheet_flowthrough_batch_id = %s' in sql assert params == [status, batch_id] assert rows_affected == 1 def test_update_batch_status_with_expected_status(self): """Test batch status update with expected status check.""" mock_cursor = MagicMock() mock_cursor.execute.return_value = 1 mock_adapter = MagicMock(spec=MySQLConnection) mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor repo = RoyaltyAccountingClient(mock_adapter) batch_id = 123 status = BatchStatus.VALIDATING expected_status = BatchStatus.PENDING rows_affected = repo.update_batch_status( batch_id, status, expected_status=expected_status ) mock_cursor.execute.assert_called_once() sql = mock_cursor.execute.call_args[0][0] params = mock_cursor.execute.call_args[0][1] assert 'UPDATE worksheet_flowthrough_batch' in sql assert 'SET batch_status = %s' in sql assert 'WHERE worksheet_flowthrough_batch_id = %s' in sql assert 'AND batch_status = %s' in sql assert params == [status, batch_id, expected_status] assert rows_affected == 1 def test_update_batch_status_with_errors(self): """Test batch status update with errors.""" mock_cursor = MagicMock() mock_cursor.execute.return_value = 1 mock_adapter = MagicMock(spec=MySQLConnection) mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor repo = RoyaltyAccountingClient(mock_adapter) batch_id = 123 status = BatchStatus.ERROR errors = [BatchErrorCode.UNKNOWN_ERROR, BatchErrorCode.BATCH_STATE_ERROR] rows_affected = repo.update_batch_status(batch_id, status, errors=errors) mock_cursor.execute.assert_called_once() sql = mock_cursor.execute.call_args[0][0] params = mock_cursor.execute.call_args[0][1] assert 'UPDATE worksheet_flowthrough_batch' in sql assert 'SET batch_status = %s' in sql assert 'errors = %s' in sql assert 'WHERE worksheet_flowthrough_batch_id = %s' in sql # params should contain status, json-serialized errors, and batch_id assert params[0] == status assert batch_id in params assert rows_affected == 1 class TestRepositoryDeleteBatchFromStaging: """Tests for RoyaltyAccountingClient.delete_batch_from_staging method.""" def test_delete_batch_from_staging_success(self): """Test successful deletion of batch data from staging.""" mock_cursor = MagicMock() mock_cursor.rowcount = 50 mock_adapter = MagicMock(spec=MySQLConnection) mock_adapter.cursor.return_value.__enter__.return_value = mock_cursor repo = RoyaltyAccountingClient(mock_adapter) batch_id = 123 deleted_rows = repo.delete_batch_from_staging(batch_id) mock_cursor.execute.assert_called_once() sql = mock_cursor.execute.call_args[0][0] params = mock_cursor.execute.call_args[0][1] assert 'DELETE FROM staging_adjustment' in sql assert 'WHERE worksheet_flowthrough_batch_id = %s' in sql assert params == [batch_id] assert deleted_rows == 50