mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 17:56:25 +02:00
test(backend): Add pytest testing infrastructure
- Add pytest, pytest-asyncio, pytest-mock to dev dependencies - Configure pytest in pyproject.toml with async support - Create tests directory structure with conftest.py - Set up unit test framework for connector testing - Enable automated testing for backend components
This commit is contained in:
parent
9e27081862
commit
8dd41adae4
10 changed files with 1139 additions and 1 deletions
0
surfsense_backend/tests/__init__.py
Normal file
0
surfsense_backend/tests/__init__.py
Normal file
104
surfsense_backend/tests/conftest.py
Normal file
104
surfsense_backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Shared pytest fixtures for SurfSense backend tests."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.db import Base
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_session():
|
||||
"""Create an async database session for testing."""
|
||||
from sqlalchemy import JSON, ARRAY, event
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
# Use in-memory SQLite for tests
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
echo=False,
|
||||
)
|
||||
|
||||
# Replace JSONB and ARRAY with JSON for SQLite compatibility
|
||||
@event.listens_for(Base.metadata, "before_create")
|
||||
def _set_json_type(target, connection, **kw):
|
||||
for table in Base.metadata.tables.values():
|
||||
for column in table.columns:
|
||||
# Convert JSONB to JSON
|
||||
if isinstance(column.type, type(JSONB())):
|
||||
column.type = JSON()
|
||||
# Convert ARRAY to JSON (SQLite doesn't support ARRAY)
|
||||
elif isinstance(column.type, ARRAY):
|
||||
column.type = JSON()
|
||||
|
||||
# Create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# Create session
|
||||
async_session_maker = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
# Cleanup
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_connector_config():
|
||||
"""Mock connector configuration."""
|
||||
return {
|
||||
"tokens": [
|
||||
{
|
||||
"chain": "ethereum",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"name": "WETH",
|
||||
},
|
||||
{
|
||||
"chain": "solana",
|
||||
"address": "So11111111111111111111111111111111111111112",
|
||||
"name": "SOL",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pair_data():
|
||||
"""Mock DexScreener API response data."""
|
||||
return {
|
||||
"pairs": [
|
||||
{
|
||||
"chainId": "ethereum",
|
||||
"dexId": "uniswap",
|
||||
"url": "https://dexscreener.com/ethereum/0x123",
|
||||
"pairAddress": "0x123",
|
||||
"baseToken": {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
"quoteToken": {
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"name": "USD Coin",
|
||||
"symbol": "USDC",
|
||||
},
|
||||
"priceNative": "1.0",
|
||||
"priceUsd": "2500.00",
|
||||
"txns": {
|
||||
"m5": {"buys": 10, "sells": 5},
|
||||
"h1": {"buys": 100, "sells": 50},
|
||||
"h6": {"buys": 500, "sells": 250},
|
||||
"h24": {"buys": 2000, "sells": 1000},
|
||||
},
|
||||
"volume": {"h24": 1000000.0, "h6": 250000.0, "h1": 50000.0, "m5": 5000.0},
|
||||
"priceChange": {"m5": 0.5, "h1": 1.2, "h6": 2.5, "h24": 5.0},
|
||||
"liquidity": {"usd": 5000000.0, "base": 2000.0, "quote": 5000000.0},
|
||||
"fdv": 10000000.0,
|
||||
"pairCreatedAt": 1609459200000,
|
||||
}
|
||||
]
|
||||
}
|
||||
0
surfsense_backend/tests/unit/__init__.py
Normal file
0
surfsense_backend/tests/unit/__init__.py
Normal file
0
surfsense_backend/tests/unit/connectors/__init__.py
Normal file
0
surfsense_backend/tests/unit/connectors/__init__.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"""Unit tests for DexScreener connector."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import httpx
|
||||
|
||||
from app.connectors.dexscreener_connector import DexScreenerConnector
|
||||
|
||||
|
||||
class TestDexScreenerConnector:
|
||||
"""Test cases for DexScreenerConnector class."""
|
||||
|
||||
def test_init_creates_connector(self):
|
||||
"""Test that connector initializes correctly."""
|
||||
connector = DexScreenerConnector()
|
||||
assert connector.base_url == "https://api.dexscreener.com/latest/dex"
|
||||
assert connector.rate_limit_delay == 0.2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_success(self, mock_pair_data):
|
||||
"""Test successful API request."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
with patch("httpx.AsyncClient.get") as mock_get:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_pair_data
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = await connector.make_request("/dex/tokens/ethereum/0x123")
|
||||
|
||||
assert result == mock_pair_data
|
||||
mock_get.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_with_retry_on_429(self, mock_pair_data):
|
||||
"""Test that connector retries on rate limit (429)."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
with patch("httpx.AsyncClient.get") as mock_get:
|
||||
# First call returns 429, second call succeeds
|
||||
mock_response_429 = MagicMock()
|
||||
mock_response_429.status_code = 429
|
||||
|
||||
mock_response_200 = MagicMock()
|
||||
mock_response_200.status_code = 200
|
||||
mock_response_200.json.return_value = mock_pair_data
|
||||
|
||||
# Mock the responses
|
||||
mock_get.side_effect = [mock_response_429, mock_response_200]
|
||||
|
||||
with patch("asyncio.sleep", return_value=None): # Skip actual sleep
|
||||
result = await connector.make_request("/dex/tokens/ethereum/0x123")
|
||||
|
||||
assert result == mock_pair_data
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_timeout(self):
|
||||
"""Test that connector handles timeout errors."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
with patch("httpx.AsyncClient.get") as mock_get:
|
||||
mock_get.side_effect = httpx.TimeoutException("Request timeout")
|
||||
|
||||
with pytest.raises(Exception, match="Request timeout"):
|
||||
await connector.make_request("/dex/tokens/ethereum/0x123")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_network_error(self):
|
||||
"""Test that connector handles network errors."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
with patch("httpx.AsyncClient.get") as mock_get:
|
||||
mock_get.side_effect = httpx.NetworkError("Network error")
|
||||
|
||||
with pytest.raises(Exception, match="Network error"):
|
||||
await connector.make_request("/dex/tokens/ethereum/0x123")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token_pairs_success(self, mock_pair_data):
|
||||
"""Test successful token pairs retrieval."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
# get_token_pairs returns a tuple (pairs, error)
|
||||
with patch.object(
|
||||
connector, "make_request", return_value=mock_pair_data
|
||||
) as mock_request:
|
||||
pairs, error = await connector.get_token_pairs("ethereum", "0x123")
|
||||
|
||||
assert pairs == mock_pair_data["pairs"]
|
||||
assert error is None
|
||||
mock_request.assert_called_once_with("tokens/ethereum/0x123")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token_pairs_no_data(self):
|
||||
"""Test handling of empty response."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
with patch.object(connector, "make_request", return_value={"pairs": None}):
|
||||
pairs, error = await connector.get_token_pairs("ethereum", "0x123")
|
||||
|
||||
assert pairs == []
|
||||
assert error is not None
|
||||
|
||||
def test_format_pair_to_markdown(self, mock_pair_data):
|
||||
"""Test markdown formatting of pair data."""
|
||||
connector = DexScreenerConnector()
|
||||
pair = mock_pair_data["pairs"][0]
|
||||
|
||||
markdown = connector.format_pair_to_markdown(pair, "WETH")
|
||||
|
||||
# Verify key sections are present (actual format is "# WETH/USDC Trading Pair")
|
||||
assert "# WETH/USDC Trading Pair" in markdown
|
||||
assert "## Price Information" in markdown
|
||||
assert "## Trading Volume" in markdown
|
||||
assert "## Market Metrics" in markdown # New section
|
||||
assert "## Liquidity" in markdown
|
||||
assert "## Transactions (24h)" in markdown
|
||||
assert "WETH" in markdown
|
||||
assert "USD Coin" in markdown or "USDC" in markdown
|
||||
assert "$2500.00" in markdown
|
||||
# Verify new metrics are present
|
||||
assert "6h Volume" in markdown
|
||||
assert "1h Volume" in markdown
|
||||
assert "Market Cap" in markdown
|
||||
assert "FDV (Fully Diluted Valuation)" in markdown
|
||||
|
||||
def test_format_pair_to_markdown_missing_fields(self):
|
||||
"""Test markdown formatting with missing optional fields."""
|
||||
connector = DexScreenerConnector()
|
||||
minimal_pair = {
|
||||
"chainId": "ethereum",
|
||||
"dexId": "uniswap",
|
||||
"pairAddress": "0x123",
|
||||
"baseToken": {"symbol": "WETH", "name": "Wrapped Ether"},
|
||||
"quoteToken": {"symbol": "USDC", "name": "USD Coin"},
|
||||
}
|
||||
|
||||
markdown = connector.format_pair_to_markdown(minimal_pair, "WETH")
|
||||
|
||||
# Should handle missing fields gracefully (actual format is "# WETH/USDC Trading Pair")
|
||||
assert "# WETH/USDC Trading Pair" in markdown
|
||||
assert "WETH" in markdown
|
||||
assert "N/A" in markdown
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_delay(self):
|
||||
"""Test rate limiting delay calculation."""
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
# Simulate rapid requests
|
||||
import time
|
||||
|
||||
connector.last_request_time = time.time()
|
||||
|
||||
with patch("asyncio.sleep") as mock_sleep:
|
||||
await connector._rate_limit_delay()
|
||||
# Should call sleep since last request was recent
|
||||
assert mock_sleep.called
|
||||
0
surfsense_backend/tests/unit/routes/__init__.py
Normal file
0
surfsense_backend/tests/unit/routes/__init__.py
Normal file
298
surfsense_backend/tests/unit/routes/test_dexscreener_routes.py
Normal file
298
surfsense_backend/tests/unit/routes/test_dexscreener_routes.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
"""Unit tests for DexScreener API routes."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.app import app
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType, User, get_async_session
|
||||
from app.users import current_active_user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user():
|
||||
"""Create a mock user for testing."""
|
||||
user = MagicMock(spec=User)
|
||||
user.id = "test-user-id"
|
||||
user.email = "test@example.com"
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
"""Create a mock async database session."""
|
||||
session = AsyncMock(spec=AsyncSession)
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_connector():
|
||||
"""Create a mock connector for testing."""
|
||||
connector = MagicMock(spec=SearchSourceConnector)
|
||||
connector.id = 1
|
||||
connector.name = "DexScreener Connector"
|
||||
connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
connector.config = {
|
||||
"tokens": [
|
||||
{
|
||||
"chain": "ethereum",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"name": "WETH",
|
||||
}
|
||||
]
|
||||
}
|
||||
connector.search_space_id = 1
|
||||
connector.user_id = "test-user-id"
|
||||
connector.is_indexable = True
|
||||
return connector
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_with_overrides(mock_user, mock_session):
|
||||
"""Create a test client with dependency overrides."""
|
||||
# Override dependencies
|
||||
app.dependency_overrides[current_active_user] = lambda: mock_user
|
||||
app.dependency_overrides[get_async_session] = lambda: mock_session
|
||||
|
||||
client = TestClient(app)
|
||||
yield client
|
||||
|
||||
# Clean up overrides after test
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
class TestDexScreenerRoutes:
|
||||
"""Test cases for DexScreener API routes."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_connector_success_new(self, client_with_overrides, mock_session):
|
||||
"""Test successful creation of a new connector."""
|
||||
request_data = {
|
||||
"tokens": [
|
||||
{
|
||||
"chain": "ethereum",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"name": "WETH",
|
||||
}
|
||||
],
|
||||
"space_id": 1,
|
||||
}
|
||||
|
||||
# Mock the database query to return no existing connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = None
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
response = client_with_overrides.post(
|
||||
"/api/v1/connectors/dexscreener/add", json=request_data
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "DexScreener connector added successfully"
|
||||
assert data["connector_type"] == "DEXSCREENER_CONNECTOR"
|
||||
assert data["tokens_count"] == 1
|
||||
assert "connector_id" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_connector_success_update_existing(
|
||||
self, client_with_overrides, mock_session, mock_connector
|
||||
):
|
||||
"""Test successful update of an existing connector."""
|
||||
request_data = {
|
||||
"tokens": [
|
||||
{
|
||||
"chain": "ethereum",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"name": "WETH",
|
||||
},
|
||||
{
|
||||
"chain": "bsc",
|
||||
"address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
||||
"name": "WBNB",
|
||||
},
|
||||
],
|
||||
"space_id": 1,
|
||||
}
|
||||
|
||||
# Mock the database query to return an existing connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = mock_connector
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
response = client_with_overrides.post(
|
||||
"/api/v1/connectors/dexscreener/add", json=request_data
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "DexScreener connector updated successfully"
|
||||
assert data["connector_type"] == "DEXSCREENER_CONNECTOR"
|
||||
assert data["tokens_count"] == 2
|
||||
assert data["connector_id"] == 1
|
||||
|
||||
def test_add_connector_invalid_tokens_missing_address(self, client_with_overrides):
|
||||
"""Test connector addition with missing address field."""
|
||||
request_data = {
|
||||
"tokens": [{"chain": "ethereum"}], # Missing address
|
||||
"space_id": 1,
|
||||
}
|
||||
|
||||
response = client_with_overrides.post(
|
||||
"/api/v1/connectors/dexscreener/add", json=request_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_add_connector_invalid_tokens_missing_chain(self, client_with_overrides):
|
||||
"""Test connector addition with missing chain field."""
|
||||
request_data = {
|
||||
"tokens": [
|
||||
{"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"}
|
||||
], # Missing chain
|
||||
"space_id": 1,
|
||||
}
|
||||
|
||||
response = client_with_overrides.post(
|
||||
"/api/v1/connectors/dexscreener/add", json=request_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_add_connector_empty_tokens_list(self, client_with_overrides):
|
||||
"""Test connector addition with empty tokens list."""
|
||||
request_data = {
|
||||
"tokens": [], # Empty list
|
||||
"space_id": 1,
|
||||
}
|
||||
|
||||
response = client_with_overrides.post(
|
||||
"/api/v1/connectors/dexscreener/add", json=request_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_connector_success(
|
||||
self, client_with_overrides, mock_session, mock_connector
|
||||
):
|
||||
"""Test successful connector deletion."""
|
||||
# Mock the database query to return an existing connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = mock_connector
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
response = client_with_overrides.delete(
|
||||
"/api/v1/connectors/dexscreener",
|
||||
params={"space_id": 1},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "DexScreener connector deleted successfully"
|
||||
mock_session.delete.assert_called_once_with(mock_connector)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_connector_not_found(self, client_with_overrides, mock_session):
|
||||
"""Test deletion of non-existent connector."""
|
||||
# Mock the database query to return no connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = None
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
response = client_with_overrides.delete(
|
||||
"/api/v1/connectors/dexscreener",
|
||||
params={"space_id": 999},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_test_connector_success(
|
||||
self, client_with_overrides, mock_session, mock_connector, mock_pair_data
|
||||
):
|
||||
"""Test successful connector test."""
|
||||
# Mock the database query to return a connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = mock_connector
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
# Mock the DexScreenerConnector.get_token_pairs method
|
||||
with patch(
|
||||
"app.connectors.dexscreener_connector.DexScreenerConnector.get_token_pairs"
|
||||
) as mock_get_pairs:
|
||||
# Return tuple (pairs, None) for success
|
||||
mock_get_pairs.return_value = (mock_pair_data["pairs"], None)
|
||||
|
||||
response = client_with_overrides.get(
|
||||
"/api/v1/connectors/dexscreener/test", params={"space_id": 1}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "DexScreener connector is working correctly"
|
||||
assert data["tokens_configured"] == 1
|
||||
assert data["pairs_found"] == len(mock_pair_data["pairs"])
|
||||
assert "sample_pair" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_test_connector_not_found(self, client_with_overrides, mock_session):
|
||||
"""Test connector test when connector doesn't exist."""
|
||||
# Mock the database query to return no connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = None
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
response = client_with_overrides.get(
|
||||
"/api/v1/connectors/dexscreener/test", params={"space_id": 999}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_test_connector_no_tokens(self, client_with_overrides, mock_session):
|
||||
"""Test connector test with no tokens configured."""
|
||||
# Create a connector with empty tokens
|
||||
empty_connector = MagicMock(spec=SearchSourceConnector)
|
||||
empty_connector.config = {"tokens": []}
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = empty_connector
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
response = client_with_overrides.get(
|
||||
"/api/v1/connectors/dexscreener/test", params={"space_id": 1}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert "no tokens" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_test_connector_api_error(
|
||||
self, client_with_overrides, mock_session, mock_connector
|
||||
):
|
||||
"""Test connector test with API error."""
|
||||
# Mock the database query to return a connector
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.first.return_value = mock_connector
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
with patch(
|
||||
"app.connectors.dexscreener_connector.DexScreenerConnector.get_token_pairs"
|
||||
) as mock_get_pairs:
|
||||
# Return tuple ([], error_message) for error
|
||||
mock_get_pairs.return_value = ([], "API Error: Connection failed")
|
||||
|
||||
response = client_with_overrides.get(
|
||||
"/api/v1/connectors/dexscreener/test", params={"space_id": 1}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert "failed to connect" in data["detail"].lower()
|
||||
504
surfsense_backend/tests/unit/tasks/test_dexscreener_indexer.py
Normal file
504
surfsense_backend/tests/unit/tasks/test_dexscreener_indexer.py
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
"""Unit tests for DexScreener indexer."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
from app.tasks.connector_indexers.dexscreener_indexer import index_dexscreener_pairs
|
||||
from app.db import SearchSourceConnectorType, DocumentType
|
||||
|
||||
|
||||
class TestDexScreenerIndexer:
|
||||
"""Test cases for DexScreener indexer function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_success(self, async_session, mock_connector_config, mock_pair_data):
|
||||
"""Test successful indexing of DexScreener pairs."""
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
# Mock dependencies
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.get_user_long_context_llm") as mock_get_llm, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.generate_document_summary") as mock_gen_summary, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.create_document_chunks") as mock_create_chunks, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed", new_callable=AsyncMock) as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client - use side_effect to return unique markdown for each token
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=(mock_pair_data["pairs"], None))
|
||||
mock_client_instance.format_pair_to_markdown.side_effect = [
|
||||
"# Mock Markdown Content 1",
|
||||
"# Mock Markdown Content 2",
|
||||
]
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock LLM service
|
||||
mock_llm = MagicMock()
|
||||
mock_get_llm.return_value = mock_llm
|
||||
|
||||
# Mock summary generation - use side_effect to return unique summaries for each token
|
||||
mock_gen_summary.side_effect = [
|
||||
(f"Mock summary 1", [0.1] * 384),
|
||||
(f"Mock summary 2", [0.2] * 384),
|
||||
]
|
||||
|
||||
# Mock chunk creation
|
||||
mock_create_chunks.return_value = []
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert error is None
|
||||
assert documents_indexed == 2 # 2 tokens in mock config
|
||||
mock_get_connector.assert_called_once()
|
||||
assert mock_client_instance.get_token_pairs.call_count == 2 # Called for each token
|
||||
mock_update_indexed.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_connector_not_found(self, async_session):
|
||||
"""Test indexer when connector is not found."""
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = None
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_failure = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=999,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert documents_indexed == 0
|
||||
assert error is not None
|
||||
assert "not found" in error.lower()
|
||||
mock_logger_instance.log_task_failure.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_no_tokens_configured(self, async_session):
|
||||
"""Test indexer when no tokens are configured."""
|
||||
# Mock connector with empty tokens
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = {"tokens": []}
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_failure = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert documents_indexed == 0
|
||||
assert error == "No tokens configured for connector"
|
||||
mock_logger_instance.log_task_failure.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_api_error(self, async_session, mock_connector_config):
|
||||
"""Test indexer when API returns an error."""
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed") as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client with API error
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=(None, "API Error: Rate limit exceeded"))
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions - should complete successfully but with 0 documents
|
||||
assert error is None
|
||||
assert documents_indexed == 0
|
||||
mock_update_indexed.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_no_pairs_found(self, async_session, mock_connector_config):
|
||||
"""Test indexer when API returns no pairs."""
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed") as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client with empty pairs
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=([], None))
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert error is None
|
||||
assert documents_indexed == 0
|
||||
mock_update_indexed.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_skips_invalid_tokens(self, async_session):
|
||||
"""Test indexer skips tokens with missing chain or address."""
|
||||
# Mock connector with invalid tokens
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = {
|
||||
"tokens": [
|
||||
{"chain": "ethereum"}, # Missing address
|
||||
{"address": "0x123"}, # Missing chain
|
||||
{"chain": "solana", "address": "So11111111111111111111111111111111111111112", "name": "SOL"},
|
||||
]
|
||||
}
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.get_user_long_context_llm") as mock_get_llm, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.generate_document_summary") as mock_gen_summary, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.create_document_chunks") as mock_create_chunks, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed") as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client
|
||||
mock_client_instance = MagicMock()
|
||||
mock_pair = {
|
||||
"pairAddress": "0xabc",
|
||||
"baseToken": {"symbol": "SOL"},
|
||||
"quoteToken": {"symbol": "USDC"},
|
||||
"dexId": "raydium",
|
||||
"priceUsd": "100.0",
|
||||
"liquidity": {"usd": 1000000},
|
||||
"volume": {"h24": 500000},
|
||||
"priceChange": {"h24": 2.5},
|
||||
}
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=([mock_pair], None))
|
||||
mock_client_instance.format_pair_to_markdown.return_value = "# Mock Markdown"
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock LLM and summary
|
||||
mock_get_llm.return_value = MagicMock()
|
||||
mock_gen_summary.return_value = ("Mock summary", [0.1] * 384)
|
||||
mock_create_chunks.return_value = []
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions - should only process the valid token
|
||||
assert error is None
|
||||
assert documents_indexed == 1
|
||||
assert mock_client_instance.get_token_pairs.call_count == 1 # Only called for valid token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_skips_pairs_without_address(self, async_session, mock_connector_config, mock_pair_data):
|
||||
"""Test indexer skips pairs without pairAddress."""
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
# Create pair without pairAddress
|
||||
invalid_pair = mock_pair_data["pairs"][0].copy()
|
||||
del invalid_pair["pairAddress"]
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed") as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client with invalid pair
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=([invalid_pair], None))
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions - should skip invalid pairs
|
||||
assert error is None
|
||||
assert documents_indexed == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_without_llm(self, async_session, mock_connector_config, mock_pair_data):
|
||||
"""Test indexer works without LLM service (fallback to basic summary)."""
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.get_user_long_context_llm") as mock_get_llm, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.create_document_chunks") as mock_create_chunks, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed", new_callable=AsyncMock) as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.config") as mock_config, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client - use side_effect to return unique markdown for each token
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=(mock_pair_data["pairs"], None))
|
||||
mock_client_instance.format_pair_to_markdown.side_effect = [
|
||||
"# Mock Markdown 1",
|
||||
"# Mock Markdown 2",
|
||||
]
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock LLM service returns None (fallback mode)
|
||||
mock_get_llm.return_value = None
|
||||
|
||||
# Mock embedding model - use side_effect to return unique embeddings
|
||||
mock_embedding_instance = MagicMock()
|
||||
mock_embedding_instance.embed.side_effect = [
|
||||
[0.1] * 384,
|
||||
[0.2] * 384,
|
||||
]
|
||||
mock_config.embedding_model_instance = mock_embedding_instance
|
||||
|
||||
# Mock chunk creation
|
||||
mock_create_chunks.return_value = []
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions - should use fallback summary
|
||||
assert error is None
|
||||
assert documents_indexed == 2
|
||||
mock_embedding_instance.embed.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_update_last_indexed_false(self, async_session, mock_connector_config, mock_pair_data):
|
||||
"""Test indexer respects update_last_indexed=False parameter."""
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
mock_connector.last_indexed_at = None
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.DexScreenerConnector") as mock_dex_client, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.get_user_long_context_llm") as mock_get_llm, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.generate_document_summary") as mock_gen_summary, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.create_document_chunks") as mock_create_chunks, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.update_connector_last_indexed", new_callable=AsyncMock) as mock_update_indexed, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.return_value = mock_connector
|
||||
|
||||
# Mock DexScreener client - use side_effect to return unique markdown for each token
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.get_token_pairs = AsyncMock(return_value=(mock_pair_data["pairs"], None))
|
||||
mock_client_instance.format_pair_to_markdown.side_effect = [
|
||||
"# Mock Markdown 1",
|
||||
"# Mock Markdown 2",
|
||||
]
|
||||
mock_dex_client.return_value = mock_client_instance
|
||||
|
||||
# Mock LLM and summary - use side_effect to return unique summaries
|
||||
mock_get_llm.return_value = MagicMock()
|
||||
mock_gen_summary.side_effect = [
|
||||
(f"Mock summary 1", [0.1] * 384),
|
||||
(f"Mock summary 2", [0.2] * 384),
|
||||
]
|
||||
mock_create_chunks.return_value = []
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_success = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer with update_last_indexed=False
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
update_last_indexed=False,
|
||||
)
|
||||
|
||||
# Assertions - should NOT update last_indexed_at
|
||||
assert error is None
|
||||
assert documents_indexed == 2
|
||||
mock_update_indexed.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index_pairs_database_error(self, async_session, mock_connector_config):
|
||||
"""Test indexer handles database errors gracefully."""
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
# Mock connector
|
||||
mock_connector = MagicMock()
|
||||
mock_connector.id = 1
|
||||
mock_connector.connector_type = SearchSourceConnectorType.DEXSCREENER_CONNECTOR
|
||||
mock_connector.config = mock_connector_config
|
||||
|
||||
with patch("app.tasks.connector_indexers.dexscreener_indexer.get_connector_by_id") as mock_get_connector, \
|
||||
patch("app.tasks.connector_indexers.dexscreener_indexer.TaskLoggingService") as mock_task_logger:
|
||||
|
||||
# Setup mocks
|
||||
mock_get_connector.side_effect = SQLAlchemyError("Database connection failed")
|
||||
|
||||
# Mock task logger
|
||||
mock_logger_instance = MagicMock()
|
||||
mock_logger_instance.log_task_start = AsyncMock(return_value=MagicMock(id=1))
|
||||
mock_logger_instance.log_task_progress = AsyncMock()
|
||||
mock_logger_instance.log_task_failure = AsyncMock()
|
||||
mock_task_logger.return_value = mock_logger_instance
|
||||
|
||||
# Execute indexer
|
||||
documents_indexed, error = await index_dexscreener_pairs(
|
||||
session=async_session,
|
||||
connector_id=1,
|
||||
search_space_id=1,
|
||||
user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert documents_indexed == 0
|
||||
assert error is not None
|
||||
assert "Database error" in error
|
||||
mock_logger_instance.log_task_failure.assert_called_once()
|
||||
Loading…
Add table
Add a link
Reference in a new issue