Skip to content

Commit 09503f6

Browse files
TexasCodingclaude
andcommitted
test: Improve client module test coverage from 30% to 82%
- Add comprehensive test suite for auth.py (83% coverage) - 11 test cases covering authentication, token refresh, and account selection - Tests for error handling and edge cases - Add comprehensive test suite for market_data.py (90% coverage) - 18 test cases covering instrument search, bar data retrieval, and caching - Tests for contract selection logic and API error handling - Add comprehensive test suite for http.py (99% coverage) - 21 test cases covering HTTP client, request handling, and error scenarios - Tests for rate limiting, authentication refresh, and connection errors - Add comprehensive test suite for cache.py (95% coverage) - 21 test cases covering instrument and market data caching - Tests for serialization, compression, TTL expiration, and cache stats - Fix bug in market_data.py _select_best_contract method - Method was incorrectly looking for 'symbol' field instead of 'name' - Fixed to use correct field names from API response - Add pyjwt dependency for authentication testing Overall client module coverage improved from 30% to 82% 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ffd8a2e commit 09503f6

File tree

7 files changed

+1439
-5
lines changed

7 files changed

+1439
-5
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ dev = [
297297
"types-pytz>=2025.2.0.20250516",
298298
"types-pyyaml>=6.0.12.20250516",
299299
"psutil>=7.0.0",
300+
"pyjwt>=2.10.1",
300301
]
301302
test = [
302303
"pytest>=8.4.1",

src/project_x_py/client/market_data.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def _select_best_contract(
239239

240240
# First try exact match
241241
for inst in instruments:
242-
if inst.get("symbol", "").upper() == search_upper:
242+
if inst.get("name", "").upper() == search_upper:
243243
return inst
244244

245245
# For futures, try to find the front month
@@ -248,8 +248,8 @@ def _select_best_contract(
248248
base_symbols: dict[str, list[dict[str, Any]]] = {}
249249

250250
for inst in instruments:
251-
symbol = inst.get("symbol", "").upper()
252-
match = futures_pattern.match(symbol)
251+
name = inst.get("name", "").upper()
252+
match = futures_pattern.match(name)
253253
if match:
254254
base = match.group(1)
255255
if base not in base_symbols:
@@ -264,9 +264,9 @@ def _select_best_contract(
264264
break
265265

266266
if matching_base and base_symbols[matching_base]:
267-
# Sort by symbol to get front month (alphabetical = chronological for futures)
267+
# Sort by name to get front month (alphabetical = chronological for futures)
268268
sorted_contracts = sorted(
269-
base_symbols[matching_base], key=lambda x: x.get("symbol", "")
269+
base_symbols[matching_base], key=lambda x: x.get("name", "")
270270
)
271271
return sorted_contracts[0]
272272

tests/test_client_auth_simple.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
"""Simplified tests for the authentication module of ProjectX client."""
2+
3+
import asyncio
4+
from datetime import datetime, timedelta, timezone
5+
from unittest.mock import AsyncMock, patch
6+
7+
import jwt
8+
import pytest
9+
10+
from project_x_py.client.auth import AuthenticationMixin
11+
from project_x_py.exceptions import ProjectXAuthenticationError
12+
from project_x_py.models import Account
13+
14+
15+
class MockAuthClient(AuthenticationMixin):
16+
"""Mock client that includes AuthenticationMixin for testing."""
17+
18+
def __init__(self):
19+
super().__init__()
20+
self.username = "test_user"
21+
self.api_key = "test_api_key"
22+
self.account_name = None
23+
self.base_url = "https://api.test.com"
24+
self.headers = {}
25+
self._http_client = AsyncMock()
26+
self._make_request = AsyncMock()
27+
self._auth_lock = asyncio.Lock()
28+
self._authenticated = False
29+
self.jwt_token = None
30+
self.session_token = None
31+
self.account_info = None
32+
33+
34+
class TestAuthenticationMixin:
35+
"""Test suite for AuthenticationMixin class."""
36+
37+
@pytest.fixture
38+
def auth_client(self):
39+
"""Create a mock client with AuthenticationMixin for testing."""
40+
return MockAuthClient()
41+
42+
@pytest.mark.asyncio
43+
async def test_authenticate_success(self, auth_client):
44+
"""Test successful authentication flow."""
45+
# Mock responses for the two API calls
46+
auth_response = {"token": "test_jwt_token"}
47+
accounts_response = {
48+
"success": True,
49+
"accounts": [
50+
{
51+
"id": 1,
52+
"name": "Test Account",
53+
"balance": 10000.0,
54+
"canTrade": True,
55+
"isVisible": True,
56+
"simulated": False,
57+
}
58+
],
59+
}
60+
61+
auth_client._make_request.side_effect = [auth_response, accounts_response]
62+
63+
await auth_client.authenticate()
64+
65+
assert auth_client.session_token == "test_jwt_token"
66+
assert auth_client.account_info.name == "Test Account"
67+
assert auth_client._authenticated is True
68+
69+
@pytest.mark.asyncio
70+
async def test_authenticate_with_specific_account(self, auth_client):
71+
"""Test authentication with specific account selection."""
72+
auth_client.account_name = "Second Account"
73+
74+
auth_response = {"token": "test_jwt_token"}
75+
accounts_response = {
76+
"success": True,
77+
"accounts": [
78+
{
79+
"id": 1,
80+
"name": "First Account",
81+
"balance": 5000.0,
82+
"canTrade": True,
83+
"isVisible": True,
84+
"simulated": False,
85+
},
86+
{
87+
"id": 2,
88+
"name": "Second Account",
89+
"balance": 10000.0,
90+
"canTrade": True,
91+
"isVisible": True,
92+
"simulated": True,
93+
},
94+
],
95+
}
96+
97+
auth_client._make_request.side_effect = [auth_response, accounts_response]
98+
99+
await auth_client.authenticate()
100+
101+
assert auth_client.account_info.name == "Second Account"
102+
assert auth_client.account_info.id == 2
103+
assert auth_client.account_info.simulated is True
104+
105+
@pytest.mark.asyncio
106+
async def test_authenticate_no_matching_account(self, auth_client):
107+
"""Test authentication fails when specified account not found."""
108+
auth_client.account_name = "Nonexistent Account"
109+
110+
auth_response = {"token": "test_jwt_token"}
111+
accounts_response = {
112+
"success": True,
113+
"accounts": [
114+
{
115+
"id": 1,
116+
"name": "Only Account",
117+
"balance": 5000.0,
118+
"canTrade": True,
119+
"isVisible": True,
120+
"simulated": False,
121+
}
122+
],
123+
}
124+
125+
auth_client._make_request.side_effect = [auth_response, accounts_response]
126+
127+
from project_x_py.exceptions import ProjectXError
128+
129+
with pytest.raises(ProjectXError, match="not found"):
130+
await auth_client.authenticate()
131+
132+
@pytest.mark.asyncio
133+
async def test_authenticate_no_accounts(self, auth_client):
134+
"""Test authentication fails when no accounts returned."""
135+
auth_response = {"token": "test_jwt_token"}
136+
accounts_response = {"success": True, "accounts": []}
137+
138+
auth_client._make_request.side_effect = [auth_response, accounts_response]
139+
140+
with pytest.raises(ProjectXAuthenticationError, match="No accounts found"):
141+
await auth_client.authenticate()
142+
143+
@pytest.mark.asyncio
144+
async def test_ensure_authenticated_when_not_authenticated(self, auth_client):
145+
"""Test _ensure_authenticated triggers authentication."""
146+
auth_client._authenticated = False
147+
auth_client.authenticate = AsyncMock()
148+
149+
await auth_client._ensure_authenticated()
150+
151+
auth_client.authenticate.assert_called_once()
152+
153+
@pytest.mark.asyncio
154+
async def test_ensure_authenticated_when_authenticated(self, auth_client):
155+
"""Test _ensure_authenticated skips when already authenticated."""
156+
auth_client._authenticated = True
157+
auth_client.jwt_token = "valid_token"
158+
auth_client.account_info = Account(
159+
id=1,
160+
name="Test",
161+
balance=10000.0,
162+
canTrade=True,
163+
isVisible=True,
164+
simulated=False,
165+
)
166+
auth_client.authenticate = AsyncMock()
167+
auth_client._should_refresh_token = lambda: False # Mock to return False
168+
169+
await auth_client._ensure_authenticated()
170+
171+
auth_client.authenticate.assert_not_called()
172+
173+
def test_should_refresh_token_near_expiry(self, auth_client):
174+
"""Test _should_refresh_token returns True when token is near expiry."""
175+
import pytz
176+
177+
auth_client.token_expiry = datetime.now(pytz.UTC) + timedelta(minutes=4)
178+
assert auth_client._should_refresh_token() is True
179+
180+
def test_should_refresh_token_plenty_time(self, auth_client):
181+
"""Test _should_refresh_token returns False when token has time."""
182+
import pytz
183+
184+
auth_client.token_expiry = datetime.now(pytz.UTC) + timedelta(hours=2)
185+
assert auth_client._should_refresh_token() is False
186+
187+
def test_should_refresh_token_no_expiry(self, auth_client):
188+
"""Test _should_refresh_token returns True when no expiry set."""
189+
auth_client.token_expiry = None
190+
assert auth_client._should_refresh_token() is True
191+
192+
@pytest.mark.asyncio
193+
async def test_list_accounts(self, auth_client):
194+
"""Test listing all available accounts."""
195+
accounts_response = {
196+
"success": True,
197+
"accounts": [
198+
{
199+
"id": 1,
200+
"name": "Account 1",
201+
"balance": 5000.0,
202+
"canTrade": True,
203+
"isVisible": True,
204+
"simulated": False,
205+
},
206+
{
207+
"id": 2,
208+
"name": "Account 2",
209+
"balance": 10000.0,
210+
"canTrade": True,
211+
"isVisible": True,
212+
"simulated": True,
213+
},
214+
],
215+
}
216+
217+
auth_client._make_request.return_value = accounts_response
218+
auth_client._ensure_authenticated = AsyncMock() # Mock authentication check
219+
220+
accounts = await auth_client.list_accounts()
221+
222+
assert len(accounts) == 2
223+
assert accounts[0].name == "Account 1"
224+
assert accounts[1].name == "Account 2"
225+
226+
@pytest.mark.asyncio
227+
async def test_authentication_error_handling(self, auth_client):
228+
"""Test proper error handling during authentication."""
229+
auth_client._make_request.side_effect = Exception("Connection failed")
230+
231+
with pytest.raises(Exception, match="Connection failed"):
232+
await auth_client.authenticate()
233+
234+
assert auth_client._authenticated is False
235+
assert auth_client.jwt_token is None

0 commit comments

Comments
 (0)