diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..cd12bbf --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[run] +omit = + # omit __init__.py + */*/*/__init__.py + # omit generate abi + src/story_protocol_python_sdk/abi/* + # omit tests + tests/* +branch = true + diff --git a/README.md b/README.md index a78c136..4202e7c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,13 @@ pytest - Unit Tests ``` -pytest tests/unit/resources -v -ra -q +coverage run -m pytest tests/unit -v -ra -q +``` + +- Generating a Coverage Report + +``` +coverage report ``` ## Release diff --git a/setup.py b/setup.py index a809667..33387e6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='story_protocol_python_sdk', - version='0.3.12', + version='0.3.14', packages=find_packages(where='src', exclude=["tests"]), package_dir={'': 'src'}, install_requires=[ diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index ac50432..6449ae4 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.3.12" +__version__ = "0.3.14" from .story_client import StoryClient from .resources.IPAsset import IPAsset 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 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