diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..551eae6 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,44 @@ +name: Python Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install uv + uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5 + + - name: Install Python + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" # Update cache if uv.lock changes + + - name: Install the project + run: | + cd python/thirdweb-ai + uv sync --all-extras --dev + + - name: Test with pytest + env: + __THIRDWEB_SECRET_KEY_DEV: ${{ secrets.__THIRDWEB_SECRET_KEY_DEV }} + run: | + cd python/thirdweb-ai + uv run pytest tests --cov=thirdweb_ai --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + fail_ci_if_error: true diff --git a/python/thirdweb-ai/pytest.ini b/python/thirdweb-ai/pytest.ini new file mode 100644 index 0000000..127f7a0 --- /dev/null +++ b/python/thirdweb-ai/pytest.ini @@ -0,0 +1,13 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Add markers if needed +markers = + unit: Unit tests + integration: Integration tests + +# Configure verbosity and coverage +addopts = -v --cov=src/thirdweb_ai --cov-report=term --cov-report=html \ No newline at end of file diff --git a/python/thirdweb-ai/src/thirdweb_ai/adapters/coinbase_agentkit/__init__.py b/python/thirdweb-ai/src/thirdweb_ai/adapters/coinbase_agentkit/__init__.py index d2e86f8..969aa86 100644 --- a/python/thirdweb-ai/src/thirdweb_ai/adapters/coinbase_agentkit/__init__.py +++ b/python/thirdweb-ai/src/thirdweb_ai/adapters/coinbase_agentkit/__init__.py @@ -1,3 +1,3 @@ -from .coinbase_agentkit import thirdweb_action_provider +from .coinbase_agentkit import ThirdwebActionProvider, thirdweb_action_provider -__all__ = ["thirdweb_action_provider"] +__all__ = ["ThirdwebActionProvider", "thirdweb_action_provider"] diff --git a/python/thirdweb-ai/src/thirdweb_ai/adapters/openai/__init__.py b/python/thirdweb-ai/src/thirdweb_ai/adapters/openai/__init__.py index c08a5c1..3d8035f 100644 --- a/python/thirdweb-ai/src/thirdweb_ai/adapters/openai/__init__.py +++ b/python/thirdweb-ai/src/thirdweb_ai/adapters/openai/__init__.py @@ -1,3 +1,3 @@ -from .agents import get_agents_tools +from .agents import get_agents_tools as get_openai_tools -__all__ = ["get_agents_tools"] +__all__ = ["get_openai_tools"] diff --git a/python/thirdweb-ai/src/thirdweb_ai/common/utils.py b/python/thirdweb-ai/src/thirdweb_ai/common/utils.py index a664415..0b04170 100644 --- a/python/thirdweb-ai/src/thirdweb_ai/common/utils.py +++ b/python/thirdweb-ai/src/thirdweb_ai/common/utils.py @@ -1,7 +1,13 @@ +import importlib.util import re from typing import Any +def has_module(module_name: str) -> bool: + """Check if module is available.""" + return importlib.util.find_spec(module_name) is not None + + def extract_digits(value: int | str) -> int: value_str = str(value).strip("\"'") digit_match = re.search(r"\d+", value_str) diff --git a/python/thirdweb-ai/tests/adapters/__init__.py b/python/thirdweb-ai/tests/adapters/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/__init__.py @@ -0,0 +1 @@ + diff --git a/python/thirdweb-ai/tests/adapters/test_autogen.py b/python/thirdweb-ai/tests/adapters/test_autogen.py new file mode 100644 index 0000000..c9d0087 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_autogen.py @@ -0,0 +1,27 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_autogen_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to AutoGen tools.""" + pytest.importorskip("autogen_core") + from autogen_core.tools import BaseTool as AutogenBaseTool # type: ignore[import] + + from thirdweb_ai.adapters.autogen import get_autogen_tools + + # Convert tools to AutoGen tools + autogen_tools = get_autogen_tools(test_tools) + + # Assert we got the correct number of tools + assert len(autogen_tools) == len(test_tools) + + # Check all tools were properly converted + assert all(isinstance(tool, AutogenBaseTool) for tool in autogen_tools) + + # Check properties were preserved + assert [tool.name for tool in autogen_tools] == [tool.name for tool in test_tools] + assert [tool.description for tool in autogen_tools] == [tool.description for tool in test_tools] + + # Check all tools have a run method + assert all(callable(getattr(tool, "run", None)) for tool in autogen_tools) diff --git a/python/thirdweb-ai/tests/adapters/test_coinbase_agentkit.py b/python/thirdweb-ai/tests/adapters/test_coinbase_agentkit.py new file mode 100644 index 0000000..a3cef08 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_coinbase_agentkit.py @@ -0,0 +1,49 @@ +import pytest +from coinbase_agentkit import ( + EthAccountWalletProvider, + EthAccountWalletProviderConfig, +) +from eth_account import Account + +from thirdweb_ai.tools.tool import Tool + + +def test_get_coinbase_agentkit_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to Coinbase AgentKit tools.""" + pytest.importorskip("coinbase_agentkit") + from coinbase_agentkit import ActionProvider # type: ignore[import] + + from thirdweb_ai.adapters.coinbase_agentkit import ThirdwebActionProvider, thirdweb_action_provider + + # Convert tools to Coinbase AgentKit provider + provider = thirdweb_action_provider(test_tools) + + # Check provider was created with the right type + assert isinstance(provider, ThirdwebActionProvider) + assert isinstance(provider, ActionProvider) + + # Check provider name + assert provider.name == "thirdweb" + + account = Account.create() + # Initialize Ethereum Account Wallet Provider + wallet_provider = EthAccountWalletProvider( + config=EthAccountWalletProviderConfig( + account=account, + chain_id="8453", # Base mainnet + rpc_url="https://8453.rpc.thirdweb.com", + ) + ) + actions = provider.get_actions(wallet_provider=wallet_provider) + # Check provider has the expected number of actions + assert len(actions) == len(test_tools) + + # Check properties were preserved by getting actions and checking names/descriptions + assert [action.name for action in actions] == [tool.name for tool in test_tools] + assert [action.description for action in actions] == [tool.description for tool in test_tools] + + # Verify that args_schema is set correctly + assert [action.args_schema for action in actions] == [tool.args_type() for tool in test_tools] + + # Check all actions have callable invoke functions + assert all(callable(action.invoke) for action in actions) diff --git a/python/thirdweb-ai/tests/adapters/test_goat.py b/python/thirdweb-ai/tests/adapters/test_goat.py new file mode 100644 index 0000000..6e8e302 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_goat.py @@ -0,0 +1,29 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_goat_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to GOAT tools.""" + # Skip this test if module not fully installed + pytest.importorskip("goat.tools") + + from goat.tools import BaseTool as GoatBaseTool # type: ignore[import] + + from thirdweb_ai.adapters.goat import get_goat_tools + + # Convert tools to GOAT tools + goat_tools = get_goat_tools(test_tools) + + # Assert we got the correct number of tools + assert len(goat_tools) == len(test_tools) + + # Check all tools were properly converted + assert all(isinstance(tool, GoatBaseTool) for tool in goat_tools) + + # Check properties were preserved + assert [tool.name for tool in goat_tools] == [tool.name for tool in test_tools] + assert [tool.description for tool in goat_tools] == [tool.description for tool in test_tools] + + # Check all tools have a callable run method + assert all(callable(getattr(tool, "run", None)) for tool in goat_tools) diff --git a/python/thirdweb-ai/tests/adapters/test_langchain.py b/python/thirdweb-ai/tests/adapters/test_langchain.py new file mode 100644 index 0000000..a1ad3a6 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_langchain.py @@ -0,0 +1,30 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_langchain_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to LangChain tools.""" + pytest.importorskip("langchain_core") + from langchain_core.tools.structured import StructuredTool # type: ignore[import] + + from thirdweb_ai.adapters.langchain import get_langchain_tools + + # Convert tools to LangChain tools + langchain_tools = get_langchain_tools(test_tools) + + # Assert we got the correct number of tools + assert len(langchain_tools) == len(test_tools) + + # Check all tools were properly converted + assert all(isinstance(tool, StructuredTool) for tool in langchain_tools) + + # Check properties were preserved + assert [tool.name for tool in langchain_tools] == [tool.name for tool in test_tools] + assert [tool.description for tool in langchain_tools] == [tool.description for tool in test_tools] + + # Check schemas were preserved + assert [tool.args_schema for tool in langchain_tools] == [tool.args_type() for tool in test_tools] + + # Check all tools have callable run methods + assert all(callable(getattr(tool, "func", None)) for tool in langchain_tools) diff --git a/python/thirdweb-ai/tests/adapters/test_llama_index.py b/python/thirdweb-ai/tests/adapters/test_llama_index.py new file mode 100644 index 0000000..fb8ef75 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_llama_index.py @@ -0,0 +1,28 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_llama_index_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to LlamaIndex tools.""" + pytest.importorskip("llama_index") + from llama_index.core.tools import FunctionTool # type: ignore[import] + + from thirdweb_ai.adapters.llama_index import get_llama_index_tools + + # Convert tools to LlamaIndex tools + llama_tools = get_llama_index_tools(test_tools) + + # Assert we got the correct number of tools + assert len(llama_tools) == len(test_tools) + + # Check all tools were properly converted + assert all(isinstance(tool, FunctionTool) for tool in llama_tools) + + # Check properties were preserved + assert [tool.metadata.name for tool in llama_tools] == [tool.name for tool in test_tools] + assert [tool.metadata.description for tool in llama_tools] == [tool.description for tool in test_tools] + assert [tool.metadata.fn_schema for tool in llama_tools] == [tool.args_type() for tool in test_tools] + + # Check all tools are callable + assert all(callable(tool) for tool in llama_tools) diff --git a/python/thirdweb-ai/tests/adapters/test_mcp.py b/python/thirdweb-ai/tests/adapters/test_mcp.py new file mode 100644 index 0000000..9e15003 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_mcp.py @@ -0,0 +1,29 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_mcp_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to MCP tools.""" + pytest.importorskip("mcp.types") + + import mcp.types as mcp_types # type: ignore[import] + + from thirdweb_ai.adapters.mcp import get_mcp_tools + + # Convert tools to MCP tools + mcp_tools = get_mcp_tools(test_tools) + + # Assert we got the correct number of tools + assert len(mcp_tools) == len(test_tools) + + # Check all tools were properly converted + assert all(isinstance(tool, mcp_types.Tool) for tool in mcp_tools) + + # Check properties were preserved + assert [tool.name for tool in mcp_tools] == [tool.name for tool in test_tools] + assert [tool.description for tool in mcp_tools] == [tool.description for tool in test_tools] + + # Check that input schemas were set correctly + for i, tool in enumerate(mcp_tools): + assert tool.inputSchema == test_tools[i].schema.get("parameters") diff --git a/python/thirdweb-ai/tests/adapters/test_openai.py b/python/thirdweb-ai/tests/adapters/test_openai.py new file mode 100644 index 0000000..de51fa7 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_openai.py @@ -0,0 +1,28 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_openai_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to OpenAI tools.""" + pytest.importorskip("openai") + from agents import FunctionTool + + from thirdweb_ai.adapters.openai import get_openai_tools + + # Convert tools to OpenAI tools + openai_tools = get_openai_tools(test_tools) + + # Assert we got the correct number of tools + assert len(openai_tools) == len(test_tools) + + # Check all required properties exist in the tools + for i, tool in enumerate(openai_tools): + assert isinstance(tool, FunctionTool) + assert hasattr(tool, "name") + assert hasattr(tool, "description") + assert hasattr(tool, "params_json_schema") + + # Check name and description match + assert tool.name == test_tools[i].name + assert tool.description == test_tools[i].description diff --git a/python/thirdweb-ai/tests/adapters/test_pydantic_ai.py b/python/thirdweb-ai/tests/adapters/test_pydantic_ai.py new file mode 100644 index 0000000..ecec063 --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_pydantic_ai.py @@ -0,0 +1,28 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_pydantic_ai_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to Pydantic AI tools.""" + pytest.importorskip("pydantic_ai") + from pydantic_ai import Tool as PydanticTool # type: ignore[import] + + from thirdweb_ai.adapters.pydantic_ai import get_pydantic_ai_tools + + # Convert tools to Pydantic AI tools + pydantic_ai_tools = get_pydantic_ai_tools(test_tools) + + # Assert we got the correct number of tools + assert len(pydantic_ai_tools) == len(test_tools) + + # Check all tools were properly converted + assert all(isinstance(tool, PydanticTool) for tool in pydantic_ai_tools) + + # Check properties were preserved + assert [tool.name for tool in pydantic_ai_tools] == [tool.name for tool in test_tools] + assert [tool.description for tool in pydantic_ai_tools] == [tool.description for tool in test_tools] + + # Check all tools have function and prepare methods + assert all(callable(getattr(tool, "function", None)) for tool in pydantic_ai_tools) + assert all(callable(getattr(tool, "prepare", None)) for tool in pydantic_ai_tools) diff --git a/python/thirdweb-ai/tests/adapters/test_smolagents.py b/python/thirdweb-ai/tests/adapters/test_smolagents.py new file mode 100644 index 0000000..ff5206c --- /dev/null +++ b/python/thirdweb-ai/tests/adapters/test_smolagents.py @@ -0,0 +1,28 @@ +import pytest + +from thirdweb_ai.tools.tool import Tool + + +def test_get_smolagents_tools(test_tools: list[Tool]): + """Test converting thirdweb tools to SmolaGents tools.""" + pytest.importorskip("smolagents") + + from smolagents import Tool as SmolagentTool # type: ignore[import] + + from thirdweb_ai.adapters.smolagents import get_smolagents_tools + + # Convert tools to SmolaGents tools + smolagents_tools = get_smolagents_tools(test_tools) + + # Assert we got the correct number of tools + assert len(smolagents_tools) == len(test_tools) + + # Check all tools were properly converted (using duck typing with SmolagentTool) + assert all(isinstance(tool, SmolagentTool) for tool in smolagents_tools) + + # Check properties were preserved + assert [tool.name for tool in smolagents_tools] == [tool.name for tool in test_tools] + assert [tool.description for tool in smolagents_tools] == [tool.description for tool in test_tools] + + # Check all tools have a callable forward method + assert all(callable(getattr(tool, "forward", None)) for tool in smolagents_tools) diff --git a/python/thirdweb-ai/tests/common/test_utils.py b/python/thirdweb-ai/tests/common/test_utils.py index 09a20e6..7f67afb 100644 --- a/python/thirdweb-ai/tests/common/test_utils.py +++ b/python/thirdweb-ai/tests/common/test_utils.py @@ -35,10 +35,3 @@ def test_no_digits(self): with pytest.raises(ValueError, match="does not contain any digits"): normalize_chain_id(["ethereum", "polygon"]) - - def test_invalid_digit_string(self): - # This test is for completeness, but the current implementation - # doesn't trigger this error case since re.search('\d+') always - # returns a valid digit string if it matches - pass - diff --git a/python/thirdweb-ai/tests/conftest.py b/python/thirdweb-ai/tests/conftest.py new file mode 100644 index 0000000..3f14260 --- /dev/null +++ b/python/thirdweb-ai/tests/conftest.py @@ -0,0 +1,62 @@ +import pytest +from pydantic import BaseModel, Field + +from thirdweb_ai.tools.tool import BaseTool, FunctionTool, Tool + + +class TestArgsModel(BaseModel): + """Test arguments model.""" + + param1: str = Field(description="Test parameter 1") + param2: int = Field(description="Test parameter 2") + + +class TestReturnModel(BaseModel): + """Test return model.""" + + result: str + + +class TestBaseTool(BaseTool[TestArgsModel, TestReturnModel]): + """A simple test tool for testing adapters.""" + + def __init__(self): + super().__init__( + args_type=TestArgsModel, + return_type=TestReturnModel, + name="test_tool", + description="A test tool for testing", + ) + + def run(self, args: TestArgsModel | None = None) -> TestReturnModel: + if args is None: + raise ValueError("Arguments are required") + return TestReturnModel(result=f"Executed with {args.param1} and {args.param2}") + + +@pytest.fixture +def test_tool() -> TestBaseTool: + """Fixture that returns a test tool.""" + return TestBaseTool() + + +@pytest.fixture +def test_function_tool() -> FunctionTool: + """Fixture that returns a test function tool.""" + + def test_func(param1: str, param2: int = 42) -> str: + """A test function for the function tool.""" + return f"Function called with {param1} and {param2}" + + return FunctionTool( + func_definition=test_func, + func_execute=test_func, + description="A test function tool", + name="test_function_tool", + ) + + +@pytest.fixture +def test_tools(test_tool: TestBaseTool, test_function_tool: FunctionTool) -> list[Tool]: + """Fixture that returns a list of test tools.""" + return [test_tool, test_function_tool] diff --git a/python/thirdweb-ai/tests/services/__init__.py b/python/thirdweb-ai/tests/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/thirdweb-ai/tests/services/test_engine.py b/python/thirdweb-ai/tests/services/test_engine.py new file mode 100644 index 0000000..42bc5e6 --- /dev/null +++ b/python/thirdweb-ai/tests/services/test_engine.py @@ -0,0 +1,47 @@ +import os + +import pytest + +from thirdweb_ai.services.engine import Engine + + +class MockEngine(Engine): + def __init__( + self, + engine_url: str, + engine_auth_jwt: str, + chain_id: int | str | None = None, + backend_wallet_address: str | None = None, + secret_key: str = "", + ): + super().__init__( + engine_url=engine_url, + engine_auth_jwt=engine_auth_jwt, + chain_id=chain_id, + backend_wallet_address=backend_wallet_address, + secret_key=secret_key, + ) + + +@pytest.fixture +def engine(): + return MockEngine( + engine_url="https://engine.thirdweb-dev.com", + engine_auth_jwt="test_jwt", + chain_id=84532, + backend_wallet_address="0xC22166664e820cdA6bf4cedBdbb4fa1E6A84C440", + secret_key=os.getenv("__THIRDWEB_SECRET_KEY_DEV") or "", + ) + + +class TestEngine: + # Constants + CHAIN_ID = "84532" + TEST_ADDRESS = "0xC22166664e820cdA6bf4cedBdbb4fa1E6A84C440" + TEST_QUEUE_ID = "9eb88b00-f04f-409b-9df7-7dcc9003bc35" + + # def test_create_backend_wallet(self, engine: Engine): + # create_backend_wallet = engine.create_backend_wallet.__wrapped__ + # result = create_backend_wallet(engine, wallet_type="local", label="Test Wallet") + # + # assert isinstance(result, dict) diff --git a/python/thirdweb-ai/tests/services/test_insight.py b/python/thirdweb-ai/tests/services/test_insight.py new file mode 100644 index 0000000..29f59f6 --- /dev/null +++ b/python/thirdweb-ai/tests/services/test_insight.py @@ -0,0 +1,31 @@ +import os + +import pytest + +from thirdweb_ai.services.insight import Insight + + +class MockInsight(Insight): + def __init__(self, secret_key: str, chain_id: int | str | list[int | str]): + super().__init__(secret_key=secret_key, chain_id=chain_id) + self.base_url = "https://insight.thirdweb-dev.com/v1" + + +@pytest.fixture +def insight(): + return MockInsight(secret_key=os.getenv("__THIRDWEB_SECRET_KEY_DEV") or "", chain_id=84532) + + +class TestInsight: + # Constants + CHAIN_ID = 1 + TEST_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + TEST_DOMAIN = "thirdweb.eth" + DEFAULT_LIMIT = 5 + + def test_get_erc20_tokens(self, insight: Insight): + get_erc20_tokens = insight.get_erc20_tokens.__wrapped__ + result = get_erc20_tokens(insight, chain=self.CHAIN_ID, owner_address=self.TEST_ADDRESS) + + assert isinstance(result, dict) + assert "data" in result diff --git a/python/thirdweb-ai/tests/services/test_nebula.py b/python/thirdweb-ai/tests/services/test_nebula.py new file mode 100644 index 0000000..c168f86 --- /dev/null +++ b/python/thirdweb-ai/tests/services/test_nebula.py @@ -0,0 +1,29 @@ +import os +import typing + +import pytest + +from thirdweb_ai.services.nebula import Nebula + + +class MockNebula(Nebula): + def __init__(self, secret_key: str): + super().__init__(secret_key=secret_key) + self.base_url = "https://nebula-api.thirdweb-dev.com" + + +@pytest.fixture +def nebula(): + return MockNebula(secret_key=os.getenv("__THIRDWEB_SECRET_KEY_DEV") or "") + + +class TestNebula: + TEST_MESSAGE = "What is thirdweb?" + TEST_SESSION_ID = "test-session-id" + TEST_CONTEXT: typing.ClassVar = {"chainIds": ["1", "137"], "walletAddress": "0x123456789abcdef"} + + def test_chat(self, nebula: Nebula): + chat = nebula.chat.__wrapped__ + result = chat(nebula, message=self.TEST_MESSAGE, session_id=self.TEST_SESSION_ID, context=self.TEST_CONTEXT) + + assert isinstance(result, dict) diff --git a/python/thirdweb-ai/tests/services/test_storage.py b/python/thirdweb-ai/tests/services/test_storage.py new file mode 100644 index 0000000..005345a --- /dev/null +++ b/python/thirdweb-ai/tests/services/test_storage.py @@ -0,0 +1,67 @@ +import os +from typing import ClassVar +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from pydantic import BaseModel + +from thirdweb_ai.services.storage import Storage + + +class MockStorage(Storage): + def __init__(self, secret_key: str): + super().__init__(secret_key=secret_key) + self.base_url = "https://storage.thirdweb.com" + + +@pytest.fixture +def storage(): + return MockStorage(secret_key=os.getenv("__THIRDWEB_SECRET_KEY_DEV") or "test-key") + + +class TestStorage: + # Constants + TEST_IPFS_HASH: ClassVar[str] = "ipfs://QmTcHZQ5QEjjbBMJrz7Xaz9AQyVBqsKCS4YQQ71B3gDQ4f" + TEST_CONTENT: ClassVar[dict[str, str]] = {"name": "test", "description": "test description"} + + def test_fetch_ipfs_content(self, storage: Storage): + fetch_ipfs_content = storage.fetch_ipfs_content.__wrapped__ + + # Test invalid IPFS hash + result = fetch_ipfs_content(storage, ipfs_hash="invalid-hash") + assert "error" in result + + # Mock the _get method to return test content + storage._get = MagicMock(return_value=self.TEST_CONTENT) # type:ignore[assignment] # noqa: SLF001 + + # Test valid IPFS hash + result = fetch_ipfs_content(storage, ipfs_hash=self.TEST_IPFS_HASH) + assert result == self.TEST_CONTENT + storage._get.assert_called_once() # noqa: SLF001 # type:ignore[union-attr] + + @pytest.mark.asyncio + async def test_upload_to_ipfs_json(self, storage: Storage): + upload_to_ipfs = storage.upload_to_ipfs.__wrapped__ + + # Create test data + class TestModel(BaseModel): + name: str + value: int + + test_model = TestModel(name="test", value=123) + + # Mock the _async_post_file method + with patch.object(storage, "_async_post_file", new_callable=AsyncMock) as mock_post: + mock_post.return_value = {"IpfsHash": "QmTest123"} + + # Test with dict + result = await upload_to_ipfs(storage, data={"test": "value"}) + assert result == "ipfs://QmTest123" + + # Test with Pydantic model + result = await upload_to_ipfs(storage, data=test_model) + assert result == "ipfs://QmTest123" + + # Verify post was called + assert mock_post.call_count == 2 +