Skip to content

Commit d243c76

Browse files
committed
test(client): Add comprehensive tests for base and trading modules
- Created test_client_base.py with 14 tests for ProjectXBase class - Tests initialization, context managers, authentication methods - Tests factory methods (from_env, from_config_file) - Achieved 100% coverage for base.py - Created test_client_trading.py with 21 tests for TradingMixin - Tests search_open_positions with various scenarios - Tests search_trades with date ranges and filters - Tests deprecated get_positions method - Achieved 98% coverage for trading.py - Overall client module coverage improved from 82% to 93% - auth.py: 83% coverage - base.py: 100% coverage (up from 61%) - cache.py: 95% coverage - http.py: 99% coverage - market_data.py: 90% coverage - trading.py: 98% coverage (up from 31%) All tests pass successfully with proper mocking of async operations.
1 parent 09503f6 commit d243c76

File tree

2 files changed

+924
-0
lines changed

2 files changed

+924
-0
lines changed

tests/test_client_base.py

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
"""Comprehensive tests for the base module of ProjectX client."""
2+
3+
import os
4+
from unittest.mock import AsyncMock, Mock, patch
5+
6+
import pytest
7+
8+
from project_x_py.client.base import ProjectXBase
9+
from project_x_py.exceptions import ProjectXAuthenticationError
10+
from project_x_py.models import Account, ProjectXConfig
11+
12+
13+
class TestProjectXBase:
14+
"""Test suite for ProjectXBase class."""
15+
16+
@pytest.fixture
17+
def mock_config(self):
18+
"""Create a mock configuration."""
19+
return ProjectXConfig(
20+
api_url="https://api.test.com",
21+
realtime_url="wss://realtime.test.com",
22+
user_hub_url="/tradehub-userhub",
23+
market_hub_url="/tradehub-markethub",
24+
timezone="America/Chicago",
25+
timeout_seconds=30,
26+
retry_attempts=3,
27+
retry_delay_seconds=1.0,
28+
requests_per_minute=100,
29+
burst_limit=10,
30+
)
31+
32+
@pytest.fixture
33+
def base_client(self, mock_config):
34+
"""Create a ProjectXBase client for testing."""
35+
return ProjectXBase(
36+
username="testuser",
37+
api_key="test-api-key",
38+
config=mock_config,
39+
account_name="TEST_ACCOUNT",
40+
)
41+
42+
def test_initialization(self, base_client):
43+
"""Test client initialization."""
44+
assert base_client.username == "testuser"
45+
assert base_client.api_key == "test-api-key"
46+
assert base_client.account_name == "TEST_ACCOUNT"
47+
assert base_client.base_url == "https://api.test.com"
48+
assert base_client._client is None
49+
assert base_client._authenticated is False
50+
assert base_client.session_token == "" # Initialized as empty string
51+
assert base_client.account_info is None
52+
assert base_client.api_call_count == 0
53+
assert base_client.cache_hit_count == 0
54+
55+
def test_initialization_with_defaults(self):
56+
"""Test client initialization with default config."""
57+
client = ProjectXBase(
58+
username="user",
59+
api_key="key",
60+
)
61+
assert client.username == "user"
62+
assert client.api_key == "key"
63+
assert client.account_name is None
64+
assert client.base_url == "https://api.topstepx.com/api" # Default URL
65+
66+
@pytest.mark.asyncio
67+
async def test_context_manager(self, base_client):
68+
"""Test async context manager functionality."""
69+
mock_http_client = AsyncMock()
70+
71+
with patch(
72+
"project_x_py.client.base.httpx.AsyncClient", return_value=mock_http_client
73+
):
74+
async with base_client as client:
75+
assert client is base_client
76+
assert base_client._client is not None
77+
78+
# After exiting context, client should be closed
79+
mock_http_client.aclose.assert_called_once()
80+
assert base_client._client is None
81+
82+
@pytest.mark.asyncio
83+
async def test_context_manager_with_exception(self, base_client):
84+
"""Test context manager handles exceptions properly."""
85+
mock_http_client = AsyncMock()
86+
87+
with patch(
88+
"project_x_py.client.base.httpx.AsyncClient", return_value=mock_http_client
89+
):
90+
with pytest.raises(ValueError, match="Test exception"):
91+
async with base_client:
92+
raise ValueError("Test exception")
93+
94+
# Client should still be closed even with exception
95+
mock_http_client.aclose.assert_called_once()
96+
97+
def test_get_session_token_when_authenticated(self, base_client):
98+
"""Test getting session token when authenticated."""
99+
base_client._authenticated = True
100+
base_client.session_token = "test-session-token"
101+
102+
token = base_client.get_session_token()
103+
assert token == "test-session-token"
104+
105+
def test_get_session_token_when_not_authenticated(self, base_client):
106+
"""Test getting session token when not authenticated."""
107+
base_client._authenticated = False
108+
109+
with pytest.raises(ProjectXAuthenticationError, match="Not authenticated"):
110+
base_client.get_session_token()
111+
112+
def test_get_session_token_no_token(self, base_client):
113+
"""Test getting session token when authenticated but no token."""
114+
base_client._authenticated = True
115+
base_client.session_token = "" # Empty string counts as no token
116+
117+
with pytest.raises(ProjectXAuthenticationError, match="Not authenticated"):
118+
base_client.get_session_token()
119+
120+
def test_get_account_info_when_available(self, base_client):
121+
"""Test getting account info when available."""
122+
account = Account(
123+
id=12345,
124+
name="Test Account",
125+
balance=10000.0,
126+
canTrade=True,
127+
isVisible=True,
128+
simulated=False,
129+
)
130+
base_client.account_info = account
131+
132+
result = base_client.get_account_info()
133+
assert result == account
134+
135+
def test_get_account_info_when_not_available(self, base_client):
136+
"""Test getting account info when not available."""
137+
base_client.account_info = None
138+
139+
with pytest.raises(ProjectXAuthenticationError, match="No account selected"):
140+
base_client.get_account_info()
141+
142+
@pytest.mark.asyncio
143+
async def test_from_env_success(self):
144+
"""Test creating client from environment variables."""
145+
with patch.dict(
146+
os.environ,
147+
{
148+
"PROJECT_X_USERNAME": "env_user",
149+
"PROJECT_X_API_KEY": "env_key",
150+
"PROJECT_X_ACCOUNT_NAME": "env_account",
151+
},
152+
):
153+
with patch("project_x_py.client.base.ConfigManager") as mock_config_manager:
154+
mock_manager = Mock()
155+
mock_manager.get_auth_config.return_value = {
156+
"username": "env_user",
157+
"api_key": "env_key",
158+
}
159+
mock_config_manager.return_value = mock_manager
160+
161+
mock_http_client = AsyncMock()
162+
with patch(
163+
"project_x_py.client.base.httpx.AsyncClient",
164+
return_value=mock_http_client,
165+
):
166+
async with ProjectXBase.from_env() as client:
167+
assert client.username == "env_user"
168+
assert client.api_key == "env_key"
169+
assert (
170+
client.account_name == "ENV_ACCOUNT"
171+
) # Should be uppercase
172+
173+
@pytest.mark.asyncio
174+
async def test_from_env_with_custom_account(self):
175+
"""Test creating client from environment with custom account name."""
176+
with patch.dict(
177+
os.environ,
178+
{
179+
"PROJECT_X_USERNAME": "env_user",
180+
"PROJECT_X_API_KEY": "env_key",
181+
},
182+
):
183+
with patch("project_x_py.client.base.ConfigManager") as mock_config_manager:
184+
mock_manager = Mock()
185+
mock_manager.get_auth_config.return_value = {
186+
"username": "env_user",
187+
"api_key": "env_key",
188+
}
189+
mock_config_manager.return_value = mock_manager
190+
191+
mock_http_client = AsyncMock()
192+
with patch(
193+
"project_x_py.client.base.httpx.AsyncClient",
194+
return_value=mock_http_client,
195+
):
196+
async with ProjectXBase.from_env(
197+
account_name="custom_account"
198+
) as client:
199+
assert client.account_name == "CUSTOM_ACCOUNT"
200+
201+
@pytest.mark.asyncio
202+
async def test_from_env_with_custom_config(self):
203+
"""Test creating client from environment with custom config."""
204+
custom_config = ProjectXConfig(
205+
api_url="https://custom.api.com",
206+
realtime_url="wss://custom.realtime.com",
207+
user_hub_url="/custom-userhub",
208+
market_hub_url="/custom-markethub",
209+
timezone="Europe/London",
210+
timeout_seconds=60,
211+
retry_attempts=5,
212+
retry_delay_seconds=2.0,
213+
requests_per_minute=200,
214+
burst_limit=20,
215+
)
216+
217+
with patch.dict(
218+
os.environ,
219+
{
220+
"PROJECT_X_USERNAME": "env_user",
221+
"PROJECT_X_API_KEY": "env_key",
222+
},
223+
):
224+
with patch("project_x_py.client.base.ConfigManager") as mock_config_manager:
225+
mock_manager = Mock()
226+
mock_manager.get_auth_config.return_value = {
227+
"username": "env_user",
228+
"api_key": "env_key",
229+
}
230+
mock_config_manager.return_value = mock_manager
231+
232+
mock_http_client = AsyncMock()
233+
with patch(
234+
"project_x_py.client.base.httpx.AsyncClient",
235+
return_value=mock_http_client,
236+
):
237+
async with ProjectXBase.from_env(config=custom_config) as client:
238+
assert client.config == custom_config
239+
assert client.base_url == "https://custom.api.com"
240+
241+
@pytest.mark.asyncio
242+
async def test_from_config_file(self):
243+
"""Test creating client from config file."""
244+
with patch("project_x_py.client.base.ConfigManager") as mock_config_manager:
245+
mock_manager = Mock()
246+
mock_config = ProjectXConfig(
247+
api_url="https://file.api.com",
248+
realtime_url="wss://file.realtime.com",
249+
user_hub_url="/file-userhub",
250+
market_hub_url="/file-markethub",
251+
timezone="US/Pacific",
252+
timeout_seconds=45,
253+
retry_attempts=4,
254+
retry_delay_seconds=1.5,
255+
requests_per_minute=150,
256+
burst_limit=15,
257+
)
258+
mock_manager.load_config.return_value = mock_config
259+
mock_manager.get_auth_config.return_value = {
260+
"username": "file_user",
261+
"api_key": "file_key",
262+
}
263+
mock_config_manager.return_value = mock_manager
264+
265+
mock_http_client = AsyncMock()
266+
with patch(
267+
"project_x_py.client.base.httpx.AsyncClient",
268+
return_value=mock_http_client,
269+
):
270+
async with ProjectXBase.from_config_file("test_config.json") as client:
271+
assert client.username == "file_user"
272+
assert client.api_key == "file_key"
273+
assert client.base_url == "https://file.api.com"
274+
275+
# Verify ConfigManager was called with the config file
276+
mock_config_manager.assert_called_once_with("test_config.json")
277+
278+
@pytest.mark.asyncio
279+
async def test_from_config_file_with_account_name(self):
280+
"""Test creating client from config file with account name."""
281+
with patch("project_x_py.client.base.ConfigManager") as mock_config_manager:
282+
mock_manager = Mock()
283+
mock_config = ProjectXConfig(
284+
api_url="https://file.api.com",
285+
realtime_url="wss://file.realtime.com",
286+
user_hub_url="/file-userhub",
287+
market_hub_url="/file-markethub",
288+
timezone="US/Pacific",
289+
timeout_seconds=45,
290+
retry_attempts=4,
291+
retry_delay_seconds=1.5,
292+
requests_per_minute=150,
293+
burst_limit=15,
294+
)
295+
mock_manager.load_config.return_value = mock_config
296+
mock_manager.get_auth_config.return_value = {
297+
"username": "file_user",
298+
"api_key": "file_key",
299+
}
300+
mock_config_manager.return_value = mock_manager
301+
302+
mock_http_client = AsyncMock()
303+
with patch(
304+
"project_x_py.client.base.httpx.AsyncClient",
305+
return_value=mock_http_client,
306+
):
307+
async with ProjectXBase.from_config_file(
308+
"test_config.json", account_name="file_account"
309+
) as client:
310+
assert client.account_name == "FILE_ACCOUNT"
311+
312+
def test_headers_property(self, base_client):
313+
"""Test headers property."""
314+
assert base_client.headers == {"Content-Type": "application/json"}
315+
316+
def test_config_property(self, mock_config):
317+
"""Test config property."""
318+
client = ProjectXBase(
319+
username="user",
320+
api_key="key",
321+
config=mock_config,
322+
)
323+
assert client.config == mock_config
324+
assert client.config.timezone == "America/Chicago"
325+
326+
def test_rate_limiter_initialization(self, base_client):
327+
"""Test rate limiter is properly initialized."""
328+
assert base_client.rate_limiter is not None
329+
assert base_client.rate_limiter.max_requests == 100
330+
assert base_client.rate_limiter.window_seconds == 60

0 commit comments

Comments
 (0)