From 9569d37670a4fc18ef561ab78d136f9f69f1d395 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 8 Jul 2025 17:40:27 +0800 Subject: [PATCH 1/2] Refactor and expand unit tests for Permission module - Updated test cases for `set_permission` and `set_all_permissions` methods to improve coverage. - Added tests for handling unregistered IP accounts, invalid signer addresses, and invalid transaction addresses. - Introduced tests for creating permission signatures with validation for deadlines. - Enhanced error handling in tests to ensure proper exception messages are raised. --- tests/__init__.py | 1 + tests/unit/__init__.py | 1 + tests/unit/fixtures/__init__.py | 0 tests/unit/fixtures/data.py | 5 + tests/unit/fixtures/web3.py | 25 ++++ tests/unit/resources/__init__.py | 1 + tests/unit/resources/test_permission.py | 167 ++++++++++++++---------- 7 files changed, 133 insertions(+), 67 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/fixtures/__init__.py create mode 100644 tests/unit/fixtures/data.py create mode 100644 tests/unit/fixtures/web3.py create mode 100644 tests/unit/resources/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c5a6e4c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# This file makes the tests/unit directory a Python package \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..c5a6e4c --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +# This file makes the tests/unit directory a Python package \ No newline at end of file diff --git a/tests/unit/fixtures/__init__.py b/tests/unit/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/fixtures/data.py b/tests/unit/fixtures/data.py new file mode 100644 index 0000000..df5ad3f --- /dev/null +++ b/tests/unit/fixtures/data.py @@ -0,0 +1,5 @@ +CHAIN_ID = 1315 +ADDRESS = "0x1234567890123456789012345678901234567890" +TX_HASH = "0x0c0cce07beb64ccfbdd59da111f23084ab7c9e96a951f7381af49e792d014c04" +# STATE as bytes32 (32 bytes = 64 hex characters) +STATE = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" diff --git a/tests/unit/fixtures/web3.py b/tests/unit/fixtures/web3.py new file mode 100644 index 0000000..aa33b40 --- /dev/null +++ b/tests/unit/fixtures/web3.py @@ -0,0 +1,25 @@ +from web3 import Web3 +from unittest.mock import MagicMock, Mock + +from tests.unit.fixtures.data import ADDRESS + + +mock_web3 = Mock(spec=Web3) +mock_web3.to_checksum_address = MagicMock(return_value=ADDRESS) + +# Add eth attribute with contract method +mock_eth = Mock() + + +# Create a function that returns a new mock contract each time +def create_mock_contract(*args, **kwargs): + """Create a new mock contract instance with address""" + mock_contract = Mock() + mock_contract.address = ADDRESS + mock_contract.encode_abi = MagicMock(return_value="0x00") + return mock_contract + + +# Set up the contract method to return new mock contracts +mock_eth.contract = create_mock_contract +mock_web3.eth = mock_eth diff --git a/tests/unit/resources/__init__.py b/tests/unit/resources/__init__.py new file mode 100644 index 0000000..3922977 --- /dev/null +++ b/tests/unit/resources/__init__.py @@ -0,0 +1 @@ +# This file makes the tests/unit/resources directory a Python package \ No newline at end of file diff --git a/tests/unit/resources/test_permission.py b/tests/unit/resources/test_permission.py index 7bf3818..eb7b9c5 100644 --- a/tests/unit/resources/test_permission.py +++ b/tests/unit/resources/test_permission.py @@ -1,75 +1,108 @@ -import os -import sys import pytest -from unittest.mock import patch, MagicMock -from web3 import Web3 -from dotenv import load_dotenv +from unittest.mock import Mock, patch -# Ensure the src directory is in the Python path -current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) -if src_path not in sys.path: - sys.path.append(src_path) +from story_protocol_python_sdk.resources.Permission import Permission +from tests.unit.fixtures.data import CHAIN_ID, ADDRESS, CHAIN_ID, STATE, TX_HASH +from tests.unit.fixtures.web3 import mock_web3 -from src.story_protocol_python_sdk.resources.Permission import Permission -# Load environment variables from .env file -load_dotenv() -private_key = os.getenv('WALLET_PRIVATE_KEY') -rpc_url = os.getenv('RPC_PROVIDER_URL') +@pytest.fixture +def permission(): + return Permission(mock_web3, ADDRESS, CHAIN_ID) -# Initialize Web3 -web3 = Web3(Web3.HTTPProvider(rpc_url)) -# Check if connected -if not web3.is_connected(): - raise Exception("Failed to connect to Web3 provider") +class TestSetPermission: + def test_unregistered_ip_account(self, permission: Permission): + with patch.object( + permission.ip_asset_registry_client, "isRegistered", return_value=False + ): + with pytest.raises( + Exception, + match="IP id with 0x1234567890123456789012345678901234567890 is not registered.", + ): + permission.set_permission(ADDRESS, ADDRESS, ADDRESS, 1) -# Set up the account with the private key -account = web3.eth.account.from_key(private_key) -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + def test_invalid_signer_address(self, permission: Permission): + with patch.object( + permission.ip_asset_registry_client, "isRegistered", return_value=True + ): + with pytest.raises(Exception, match="Invalid address: 0xInvalidAddress."): + permission.set_permission(ADDRESS, "0xInvalidAddress", ADDRESS, 1) -@pytest.fixture -def permission(): - chain_id = 11155111 # Sepolia chain ID - return Permission(web3, account, chain_id) - - -def test_unregistered_ip_account(permission): - with patch.object(permission, '_is_registered', return_value=False): - with pytest.raises(ValueError, match="The IP account with id 0x0000000000000000000000000000000000000000 is not registered."): - permission.setPermission(ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 1) - -def test_invalid_signer_address(permission): - with pytest.raises(ValueError, match="The address 0xInvalidAddress that can call 'to' on behalf of the 'ip_asset' is not a valid address."): - permission.setPermission(ZERO_ADDRESS, "0xInvalidAddress", ZERO_ADDRESS, "0x11111111111111111111111111111") - -def test_invalid_to_address(permission): - with pytest.raises(ValueError, match="The recipient of the transaction 0xInvalidAddress is not a valid address."): - permission.setPermission(ZERO_ADDRESS, ZERO_ADDRESS, "0xInvalidAddress", "0x11111111111111111111111111111") - -def test_successful_transaction(permission): - ip_asset = "0x587AE719cACC8cC34188D9648d67CF885bE10558" - signer = "0x8059F63663576bE3605B3CcD30aaEb858C345640" - to = "0x2ac240293f12032E103458451dE8A8096c5A72E8" - func = "0x00000000" - permission_level = 1 - tx_hash = "0x0c0cce07beb64ccfbdd59da111f23084ab7c9e96a951f7381af49e792d014c04" - - with patch.object(permission, '_is_registered', return_value=True), \ - patch('story_protocol_python_sdk.resources.IPAccount.IPAccount.execute', return_value={'txHash': tx_hash}): - response = permission.setPermission(ip_asset, signer, to, permission_level, func) - assert response['txHash'] == tx_hash - -def test_transaction_request_fails(permission): - ip_asset = "0x587AE719cACC8cC34188D9648d67CF885bE10558" - signer = "0x8059F63663576bE3605B3CcD30aaEb858C345640" - to = "0x2ac240293f12032E103458451dE8A8096c5A72E8" - func = "0x00000000" - permission_level = 1 - - with patch.object(permission, '_is_registered', return_value=True), \ - patch('story_protocol_python_sdk.resources.IPAccount.IPAccount.execute', side_effect=Exception("Transaction failed")): - with pytest.raises(Exception) as excinfo: - permission.setPermission(ip_asset, signer, to, permission_level, func) - assert str(excinfo.value) == "Transaction failed" \ No newline at end of file + def test_invalid_to_address(self, permission: Permission): + with pytest.raises(Exception, match="Invalid address: 0xInvalidAddress."): + permission.set_permission(ADDRESS, ADDRESS, "0xInvalidAddress", 1) + + def test_successful_transaction(self, permission: Permission): + with patch.object( + permission.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + permission.ip_account, "execute", return_value={"tx_hash": TX_HASH} + ): + response = permission.set_permission(ADDRESS, ADDRESS, ADDRESS, 1) + assert response["tx_hash"] == TX_HASH + + def test_transaction_request_fails(self, permission: Permission): + with patch.object( + permission.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + permission.ip_account, + "execute", + side_effect=Exception("Transaction failed"), + ): + with pytest.raises(Exception, match="Transaction failed"): + permission.set_permission(ADDRESS, ADDRESS, ADDRESS, 1) + + +class TestSetAllPermissions: + def test_successful_transaction(self, permission: Permission): + with patch.object( + permission.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + permission.ip_account, "execute", return_value={"tx_hash": TX_HASH} + ): + response = permission.set_all_permissions(ADDRESS, ADDRESS, 1) + assert response["tx_hash"] == TX_HASH + + def test_transaction_request_fails(self, permission: Permission): + with patch.object( + permission.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + permission.ip_account, + "execute", + side_effect=Exception("Transaction failed"), + ): + with pytest.raises(Exception, match="Transaction failed"): + permission.set_all_permissions(ADDRESS, ADDRESS, 1) + + +class TestCreateSetPermissionSignature: + + def test_invalid_deadline(self, permission: Permission): + with pytest.raises(Exception, match="Invalid deadline value."): + permission.create_set_permission_signature( + ADDRESS, ADDRESS, ADDRESS, 1, deadline=-1 + ) + + def test_successful_signature(self, permission: Permission): + mock_client = patch( + "story_protocol_python_sdk.resources.Permission.IPAccountImplClient" + ).start() + mock_client.return_value.state.return_value = STATE + with patch.multiple( + permission, + ip_account=Mock( + execute_with_sig=Mock(return_value={"tx_hash": TX_HASH}), + ), + sign_util=Mock( + get_permission_signature=Mock( + return_value={ + "signature": "0x1234567890123456789012345678901234567890" + } + ) + ), + ): + response = permission.create_set_permission_signature( + ADDRESS, ADDRESS, ADDRESS, 1 + ) + assert response["tx_hash"] == TX_HASH From b20ee0149c9832b875419ac2e4ccb726e1c84a52 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 9 Jul 2025 11:10:02 +0800 Subject: [PATCH 2/2] Refactor unit tests in test_story_client.py to use CHAIN_ID constant - Removed hardcoded chain ID values and replaced them with the CHAIN_ID constant for better maintainability. - Improved formatting of error messages for environment variable checks. - Cleaned up unnecessary code related to Python path adjustments. --- tests/unit/test_story_client.py | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/unit/test_story_client.py b/tests/unit/test_story_client.py index 131c86b..afb0c23 100644 --- a/tests/unit/test_story_client.py +++ b/tests/unit/test_story_client.py @@ -4,27 +4,24 @@ from web3 import Web3 from dotenv import load_dotenv -# Ensure the src/story_protocol_python_sdk directory is in the Python path -current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..', 'src')) -if src_path not in sys.path: - sys.path.append(src_path) - from story_protocol_python_sdk.story_client import StoryClient from story_protocol_python_sdk.resources.IPAsset import IPAsset from story_protocol_python_sdk.resources.License import License from story_protocol_python_sdk.resources.Royalty import Royalty from story_protocol_python_sdk.resources.IPAccount import IPAccount from story_protocol_python_sdk.resources.Permission import Permission +from tests.unit.fixtures.data import CHAIN_ID # Load environment variables from .env file load_dotenv() -private_key = os.getenv('WALLET_PRIVATE_KEY') -rpc_url = os.getenv('RPC_PROVIDER_URL') +private_key = os.getenv("WALLET_PRIVATE_KEY") +rpc_url = os.getenv("RPC_PROVIDER_URL") # Ensure the environment variables are set if not private_key or not rpc_url: - raise ValueError("Please set WALLET_PRIVATE_KEY and RPC_PROVIDER_URL in the .env file") + raise ValueError( + "Please set WALLET_PRIVATE_KEY and RPC_PROVIDER_URL in the .env file" + ) # Initialize Web3 web3 = Web3(Web3.HTTPProvider(rpc_url)) @@ -36,48 +33,57 @@ # Set up the account with the private key account = web3.eth.account.from_key(private_key) + @pytest.fixture def story_client(): - chain_id = 11155111 # Sepolia chain ID - return StoryClient(web3, account, chain_id) + return StoryClient(web3, account, CHAIN_ID) + def test_story_client_constructor(story_client): assert story_client is not None assert isinstance(story_client, StoryClient) + def test_story_client_transport_error(): with pytest.raises(ValueError): - StoryClient(None, account, chain_id=11155111) + StoryClient(None, account, chain_id=CHAIN_ID) + def test_story_client_account_missing(): with pytest.raises(ValueError): - StoryClient(web3, None, chain_id=11155111) + StoryClient(web3, None, chain_id=CHAIN_ID) + def test_story_client_wallet_initialization(): - client = StoryClient(web3, account, chain_id=11155111) + client = StoryClient(web3, account, chain_id=CHAIN_ID) assert client is not None assert isinstance(client, StoryClient) + def test_ip_asset_client_getter(story_client): ip_asset = story_client.IPAsset assert ip_asset is not None assert isinstance(ip_asset, IPAsset) + def test_license_client_getter(story_client): license = story_client.License assert license is not None assert isinstance(license, License) + def test_royalty_client_getter(story_client): royalty = story_client.Royalty assert royalty is not None assert isinstance(royalty, Royalty) + def test_ip_account_client_getter(story_client): ip_account = story_client.IPAccount assert ip_account is not None assert isinstance(ip_account, IPAccount) + def test_permission_getter(story_client): permission = story_client.Permission assert permission is not None