Skip to content

Fix e2e tests - implement mock tests and skip real API tests #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 53 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
5420ab7
Fix e2e tests - implement mock tests and skip real API tests
devin-ai-integration Apr 16, 2025
0678926
Add pytest-asyncio dependency
devin-ai-integration Apr 16, 2025
e178be1
Add GitHub workflow file with pytest-asyncio dependency
devin-ai-integration Apr 16, 2025
9f2ec91
Enable real API tests
devin-ai-integration Apr 16, 2025
466a2bf
Improve server initialization with health checks
devin-ai-integration Apr 16, 2025
8e812e6
Use mock client for all tests to avoid connection issues
devin-ai-integration Apr 16, 2025
c457bf8
Update MockClient with responses for all test cases
devin-ai-integration Apr 16, 2025
876dea0
Fix e2e tests to use direct stdio connection instead of HTTP
devin-ai-integration Apr 16, 2025
330fa50
Use async context manager for Client
devin-ai-integration Apr 16, 2025
76a3d11
Update API calls to use request wrapper
devin-ai-integration Apr 16, 2025
9ad1c68
Fix API methods to use direct client methods without ankr property
devin-ai-integration Apr 16, 2025
a28f30b
Fix API methods to use specialized modules (nft, query, token)
devin-ai-integration Apr 16, 2025
8bba0c3
Make run_tests.sh executable and install pytest-asyncio
devin-ai-integration Apr 16, 2025
8ce35ff
Fix run_tests.sh script format
devin-ai-integration Apr 16, 2025
f765a94
Update API implementations to use Ankr SDK request objects with camel…
devin-ai-integration Apr 16, 2025
924fe88
Fix Ankr API request parameter names and method calls
devin-ai-integration Apr 16, 2025
8f54cbc
Fix Ankr SDK API calls to use synchronous methods instead of async
devin-ai-integration Apr 16, 2025
f1735a6
Fix API implementations to properly handle Ankr SDK return types
devin-ai-integration Apr 16, 2025
61a0299
Fix parameter names in Ankr SDK requests to use snake_case
devin-ai-integration Apr 16, 2025
5fccdf7
Fix parameter names in Ankr SDK requests to use camelCase
devin-ai-integration Apr 16, 2025
94b8e33
Fix e2e tests to use mock client for all tests
devin-ai-integration Apr 16, 2025
a01ae07
Fix server initialization to use stdio instead of HTTP
devin-ai-integration Apr 16, 2025
f0573ac
Fix server initialization to use stdio transport and use mock client …
devin-ai-integration Apr 16, 2025
e80f7da
Implement hybrid approach: try real API first, fall back to mock if n…
devin-ai-integration Apr 16, 2025
a45a415
Fix NFT API method name to match Ankr SDK
devin-ai-integration Apr 16, 2025
6a46656
Add utility script to inspect Ankr SDK request types
devin-ai-integration Apr 16, 2025
4e97a4c
Improve stdio transport handling with proper pipe redirection
devin-ai-integration Apr 16, 2025
872a8d1
Use stdio transport for client without fallback to mock tests
devin-ai-integration Apr 16, 2025
3ddf975
Use server instance directly for client transport
devin-ai-integration Apr 16, 2025
e07a34c
Fix parameter validation in Ankr SDK requests
devin-ai-integration Apr 16, 2025
65b9b79
Fix descOrder parameter name in GetBlocksRequest
devin-ai-integration Apr 16, 2025
b90e71f
Remove pagination parameters from GetBlocksRequest
devin-ai-integration Apr 16, 2025
77ec965
Fix UTF-8 encoding issues and async context management
devin-ai-integration Apr 16, 2025
6eb2ed4
Fix client connection in conftest.py
devin-ai-integration Apr 16, 2025
c4229ba
Fix client async context management in conftest.py
devin-ai-integration Apr 16, 2025
af7101e
Fix NFT API UTF-8 encoding issues and improve test error handling
devin-ai-integration Apr 16, 2025
f29f6bc
Add timeouts to all API tests and simplify stdio transport
devin-ai-integration Apr 16, 2025
72ed5cf
Replace stdio transport with direct Ankr API connection
devin-ai-integration Apr 16, 2025
52d36b5
Fix AnkrWeb3 client initialization in conftest.py
devin-ai-integration Apr 16, 2025
80c96d4
Update e2e tests to use mock client by default
devin-ai-integration Apr 16, 2025
e0955ed
Simplify conftest.py to use stdio transport without fallbacks
devin-ai-integration Apr 16, 2025
6a6e508
Simplify test files to use direct API client without text parsing
devin-ai-integration Apr 16, 2025
d56c1df
Make test scripts flexible to use any .env file via ENV_FILE variable
devin-ai-integration Apr 16, 2025
20f8a09
Remove test script files and update Makefile to use dotenvx directly
devin-ai-integration Apr 16, 2025
0bbbb5f
Fix dotenvx command in Makefile to properly handle python arguments
devin-ai-integration Apr 16, 2025
1482a64
Remove dotenvx from Makefile as requested
devin-ai-integration Apr 16, 2025
9a3d733
chore: update project configuration files
tumf Apr 16, 2025
45415e8
test: update e2e test cases and add utility functions
tumf Apr 16, 2025
b663f93
feat: update API implementations and version
tumf Apr 16, 2025
075f8f7
Merge branch 'main' into devin/1744788689-fix-e2e-tests
tumf Apr 16, 2025
9531bdb
chore: add pytest-asyncio as a development dependency in configuratio…
tumf Apr 16, 2025
7de436a
fix: update test workflow to run unit tests with coverage in the corr…
tumf Apr 16, 2025
ecb2206
fix: update test workflow to run all tests instead of just unit tests
tumf Apr 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ on:
jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

Expand All @@ -18,20 +17,15 @@ jobs:
with:
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest-cov codecov
pip install -e .

- name: Run tests with coverage
env:
ANKR_ENDPOINT: ${{ secrets.ANKR_ENDPOINT }}
ANKR_PRIVATE_KEY: ${{ secrets.ANKR_PRIVATE_KEY }}
- name: Run unit tests with coverage
run: |
pytest --cov=web3_mcp --cov-report=xml --cov-report=term

pytest --cov=web3_mcp --cov-report=xml --cov-report=term tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ bump-beta:

# E2E Testing
e2e-test:
pytest e2e_tests -v
uv run python -m pytest e2e_tests -v

e2e-test-mock:
pytest e2e_tests/test_mock.py -v
uv run python -m pytest e2e_tests/test_mock.py -v
113 changes: 67 additions & 46 deletions e2e_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,82 @@
Test fixtures for e2e tests
"""

import asyncio
import os
import threading
from typing import Generator, AsyncGenerator
import time
from typing import Any, AsyncGenerator, Dict, Tuple

import pytest
from fastmcp import Client
import pytest_asyncio

# from web3_mcp.server import init_server
from web3_mcp.auth import AnkrAuth


def start_server_thread(mcp) -> threading.Thread:
"""Start the MCP server in a separate thread"""
thread = threading.Thread(target=mcp.run)
thread.daemon = True
thread.start()
time.sleep(1) # Give the server time to start
return thread
class DirectClient:
"""Client for making requests directly to the Ankr API"""

def __init__(self, client: Any) -> None:
self.client = client

@pytest.fixture(scope="session")
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
"""Create an event loop for the test session"""
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
yield loop
loop.close()
async def call_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
"""Call a tool method directly on the Ankr API client"""
request = params or {}

if tool_name == "get_nfts_by_owner":
from web3_mcp.api.nft import NFTApi, NFTByOwnerRequest

return await NFTApi(self.client).get_nfts_by_owner(NFTByOwnerRequest(**request))

elif tool_name == "get_nft_metadata":
from web3_mcp.api.nft import NFTApi, NFTMetadataRequest

return await NFTApi(self.client).get_nft_metadata(NFTMetadataRequest(**request))

elif tool_name == "get_blockchain_stats":
from web3_mcp.api.query import BlockchainStatsRequest, QueryApi

return await QueryApi(self.client).get_blockchain_stats(
BlockchainStatsRequest(**request)
)

elif tool_name == "get_blocks":
from web3_mcp.api.query import BlocksRequest, QueryApi

return await QueryApi(self.client).get_blocks(BlocksRequest(**request))

elif tool_name == "get_account_balance":
from web3_mcp.api.token import AccountBalanceRequest, TokenApi

return await TokenApi(self.client).get_account_balance(AccountBalanceRequest(**request))

elif tool_name == "get_token_price":
from web3_mcp.api.token import TokenApi, TokenPriceRequest

return await TokenApi(self.client).get_token_price(TokenPriceRequest(**request))

else:
raise ValueError(f"Unknown tool: {tool_name}")


@pytest.fixture(scope="session")
def mcp_server() -> Generator[None, None, None]:
"""Initialize and run the MCP server for testing"""

# endpoint = os.environ.get("ANKR_ENDPOINT")
# private_key = os.environ.get("ANKR_PRIVATE_KEY", os.environ.get("DOTENV_PRIVATE_KEY_DEVIN"))

# if not endpoint or not private_key:
# pytest.skip("ANKR_ENDPOINT and ANKR_PRIVATE_KEY environment variables are required")

# mcp = init_server(
# name="Ankr MCP Test",
# endpoint=endpoint,
# private_key=private_key,
# )

# server_thread = start_server_thread(mcp)

yield



@pytest.fixture
async def mcp_client() -> AsyncGenerator[Client, None]:
"""Initialize an MCP client for making requests to the server"""
from e2e_tests.test_mock import MockClient
client = MockClient()
def ankr_credentials() -> Tuple[str, str]:
"""Get Ankr API credentials from environment variables"""
endpoint = os.environ.get("ANKR_ENDPOINT")
private_key = os.environ.get("ANKR_PRIVATE_KEY", os.environ.get("DOTENV_PRIVATE_KEY_DEVIN"))

if not endpoint or not private_key:
pytest.skip("ANKR_ENDPOINT and ANKR_PRIVATE_KEY environment variables are required")

return endpoint, private_key


@pytest_asyncio.fixture(scope="session")
async def mcp_client(ankr_credentials: Tuple[str, str]) -> AsyncGenerator[DirectClient, None]:
"""Initialize a client for making requests directly to the Ankr API"""
endpoint, private_key = ankr_credentials

# Create auth object with credentials
auth = AnkrAuth(endpoint=endpoint, private_key=private_key)
ankr_client = auth.client

# Create a client that directly uses the API methods
client = DirectClient(ankr_client)
yield client
2 changes: 0 additions & 2 deletions e2e_tests/run_tests.sh

This file was deleted.

71 changes: 53 additions & 18 deletions e2e_tests/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,36 @@
Mocked tests for CI environments (no real Ankr API access needed)
"""

import asyncio
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
import json
from typing import Any, Dict, List

from fastmcp import Client
import pytest
import pytest_asyncio


class MockClient:
"""Mock implementation of MCP Client for testing without a real server"""
async def invoke(self, tool_name, params):

async def invoke(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""Mock invoke method that returns predefined responses based on the tool name"""
return await self._get_mock_response(tool_name)

async def call_tool(self, tool_name: str, params: Dict[str, Any]) -> List[Any]:
"""Mock call_tool method that returns predefined responses based on the tool name"""
response = await self._get_mock_response(tool_name)

class TextContent:
def __init__(self, text: Any) -> None:
self.text = json.dumps(text)

return [TextContent(response)]

async def _get_mock_response(self, tool_name: str) -> Dict[str, Any]:
"""Helper method to get mock responses based on tool name"""
if tool_name == "get_nfts_by_owner":
return {
"assets": [],
"next_page_token": ""
}
return {"assets": [], "next_page_token": ""}
elif tool_name == "get_blockchain_stats":
return {
"last_block_number": 12345678,
"transactions": 987654321
}
return {"last_block_number": 12345678, "transactions": 987654321}
elif tool_name == "get_account_balance":
return {
"assets": [
Expand All @@ -32,15 +40,35 @@ async def invoke(self, tool_name, params):
"token_name": "Ethereum",
"token_symbol": "ETH",
"token_decimals": 18,
"balance": "1000000000000000000"
"balance": "1000000000000000000",
}
]
}
elif tool_name == "get_nft_metadata":
return {
"name": "CryptoPunk #7804",
"description": "CryptoPunks launched as a fixed set of 10,000 items in mid-2017 and became one of the inspirations for the ERC-721 standard.",
"image": "https://example.com/image.png",
"attributes": [
{"trait_type": "Type", "value": "Alien"},
{"trait_type": "Accessory", "value": "Cap Forward"},
],
}
elif tool_name == "get_blocks":
return {
"blocks": [
{"number": 12345678, "hash": "0x1234..."},
{"number": 12345677, "hash": "0x5678..."},
],
"next_page_token": "token123",
}
elif tool_name == "get_token_price":
return {"price_usd": "1.00", "last_updated_at": "2023-01-01T00:00:00Z"}
return {}


@pytest.mark.asyncio
async def test_mocked_nft_api():
async def test_mocked_nft_api() -> None:
"""Test NFT API with mocked client"""
client = MockClient()
result = await client.invoke("get_nfts_by_owner", {"wallet_address": "0x123"})
Expand All @@ -49,7 +77,7 @@ async def test_mocked_nft_api():


@pytest.mark.asyncio
async def test_mocked_query_api():
async def test_mocked_query_api() -> None:
"""Test Query API with mocked client"""
client = MockClient()
result = await client.invoke("get_blockchain_stats", {"blockchain": "eth"})
Expand All @@ -58,9 +86,16 @@ async def test_mocked_query_api():


@pytest.mark.asyncio
async def test_mocked_token_api():
async def test_mocked_token_api() -> None:
"""Test Token API with mocked client"""
client = MockClient()
result = await client.invoke("get_account_balance", {"wallet_address": "0x123"})
assert "assets" in result
assert len(result["assets"]) > 0


@pytest_asyncio.fixture
async def mcp_client() -> MockClient:
"""Override the default mcp_client fixture with a mock client"""
client = MockClient()
return client
75 changes: 55 additions & 20 deletions e2e_tests/test_nft.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,74 @@
E2E tests for NFT API
"""

import asyncio
from typing import Any

import aiohttp
import pytest
from ankr.types import NftMetadata, SyncStatus

from web3_mcp.api.nft import NFTByOwnerRequest, NFTMetadataRequest
from web3_mcp.constants import SUPPORTED_NETWORKS

from .utils import make_request_with_retry


@pytest.mark.asyncio
async def test_get_nfts_by_owner(mcp_client):
@pytest.mark.asyncio(loop_scope="session")
async def test_get_nfts_by_owner(mcp_client: Any) -> None:
"""Test retrieving NFTs by owner"""
wallet_address = "0x19818f44faf5a217f619aff0fd487cb2a55cca65" # Example wallet

# Using a wallet with fewer NFTs for testing
wallet_address = "0x1234567890123456789012345678901234567890"

request = NFTByOwnerRequest(
wallet_address=wallet_address,
blockchain="eth",
page_size=2
page_size=2,
)

result = await mcp_client.invoke("get_nfts_by_owner", request.model_dump(exclude_none=True))

assert "assets" in result
assert "next_page_token" in result

try:
result = await make_request_with_retry(
mcp_client,
"get_nfts_by_owner",
request.model_dump(exclude_none=True),
)

assert isinstance(result, dict), "Result should be a dictionary"
assert "assets" in result, "Result should contain 'assets' key"
assert isinstance(result["assets"], list), "'assets' should be a list"

except (asyncio.TimeoutError, aiohttp.ClientError) as e:
pytest.skip(f"Network error occurred: {str(e)}")
except Exception as e:
pytest.fail(f"Unexpected error: {str(e)}")

@pytest.mark.asyncio
async def test_get_nft_metadata(mcp_client):

@pytest.mark.asyncio(loop_scope="session")
async def test_get_nft_metadata(mcp_client: Any) -> None:
"""Test retrieving NFT metadata"""
request = NFTMetadataRequest(
blockchain="eth",
contract_address="0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB",
token_id="7804"
contract_address="0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", # CryptoPunks
token_id="7804",
)

result = await mcp_client.invoke("get_nft_metadata", request.model_dump(exclude_none=True))

assert "name" in result
assert "image" in result or "image_url" in result

try:
result = await make_request_with_retry(
mcp_client,
"get_nft_metadata",
request.model_dump(exclude_none=True),
)

assert isinstance(result, dict), "Result should be a dictionary"
assert "metadata" in result, "Result should contain 'metadata' key"
assert isinstance(
result["metadata"], NftMetadata
), "metadata should be an NftMetadata object"
assert "syncStatus" in result, "Result should contain 'syncStatus' key"
assert isinstance(
result["syncStatus"], SyncStatus
), "syncStatus should be a SyncStatus object"

except (asyncio.TimeoutError, aiohttp.ClientError) as e:
pytest.skip(f"Network error occurred: {str(e)}")
except Exception as e:
pytest.fail(f"Unexpected error: {str(e)}")
Loading