Skip to content

Commit fccf7e4

Browse files
author
f.fallah
committed
feat(tests): add tests dir, auth integration tests and the base of search service tests
1 parent eacf274 commit fccf7e4

File tree

6 files changed

+364
-3
lines changed

6 files changed

+364
-3
lines changed

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,3 @@ venv.bak/
5555
# IDE files
5656
.idea/
5757
.DS_Store
58-
59-
# Test files
60-
test*

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file makes the tests directory a Python package

tests/pytest.ini

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[tool:pytest]
2+
testpaths = tests
3+
python_files = test_*.py
4+
python_classes = Test*
5+
python_functions = test_*
6+
addopts =
7+
-v
8+
--tb=short
9+
--strict-markers
10+
--disable-warnings
11+
markers =
12+
asyncio: marks tests as asyncio tests
13+
integration: marks tests as integration tests that make real API calls
14+
unit: marks tests as unit tests with mocked dependencies
15+
slow: marks tests as slow running
16+
asyncio_mode = auto

tests/requirements-test.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pytest>=7.0.0
2+
pytest-mock>=3.10.0
3+
pytest-asyncio>=0.21.0
4+
httpx>=0.24.0

tests/test_auth_integration.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"""
2+
Integration tests for the authentication module using real API calls.
3+
"""
4+
import pytest
5+
import os
6+
7+
from basalam_sdk.auth import (
8+
TokenInfo, ClientCredentials, AuthorizationCode
9+
)
10+
from basalam_sdk.config import BasalamConfig, Environment
11+
from basalam_sdk.errors import BasalamAuthError
12+
13+
14+
# Real test client credentials
15+
TEST_CLIENT_ID = ""
16+
TEST_CLIENT_SECRET = ""
17+
TEST_REDIRECT_URI = ""
18+
19+
20+
@pytest.fixture
21+
def integration_config():
22+
"""Create a real config for integration testing."""
23+
return BasalamConfig(
24+
environment=Environment.PRODUCTION,
25+
timeout=30.0,
26+
user_agent="Integration Test Agent"
27+
)
28+
29+
30+
@pytest.fixture
31+
def client_credentials_auth(integration_config):
32+
"""Create a real ClientCredentials auth instance for integration testing."""
33+
return ClientCredentials(
34+
client_id=TEST_CLIENT_ID,
35+
client_secret=TEST_CLIENT_SECRET,
36+
scopes=["vendor.profile.read", "vendor.profile.write"],
37+
config=integration_config
38+
)
39+
40+
41+
@pytest.fixture
42+
def authorization_code_auth(integration_config):
43+
"""Create a real AuthorizationCode auth instance for integration testing."""
44+
return AuthorizationCode(
45+
client_id=TEST_CLIENT_ID,
46+
client_secret=TEST_CLIENT_SECRET,
47+
redirect_uri=TEST_REDIRECT_URI,
48+
scopes=["vendor.profile.read", "vendor.profile.write"],
49+
config=integration_config
50+
)
51+
52+
53+
@pytest.mark.integration
54+
class TestClientCredentialsIntegration:
55+
"""Integration tests for ClientCredentials authentication with real API calls."""
56+
57+
def test_get_token_sync_success(self, client_credentials_auth):
58+
"""Test successful synchronous token acquisition with real API call."""
59+
token = client_credentials_auth.get_token_sync()
60+
61+
# Verify token structure
62+
assert isinstance(token, TokenInfo)
63+
assert token.access_token is not None
64+
assert len(token.access_token) > 10 # Should be a substantial token
65+
assert token.token_type == "Bearer"
66+
assert token.expires_in > 0
67+
assert token.created_at is not None
68+
69+
# Verify token timing
70+
assert not token.is_expired
71+
assert not token.should_refresh # Fresh token shouldn't need refresh
72+
73+
# Verify scopes (if returned by the API)
74+
if token.scope:
75+
granted_scopes = token.granted_scopes
76+
assert len(granted_scopes) > 0
77+
# Check if at least one expected scope is granted
78+
expected_scopes = {"vendor.profile.read", "vendor.profile.write"}
79+
assert len(granted_scopes.intersection(expected_scopes)) > 0
80+
81+
# Verify the auth instance stores the token
82+
assert client_credentials_auth._token_info == token
83+
84+
@pytest.mark.asyncio
85+
async def test_get_token_async_success(self, client_credentials_auth):
86+
"""Test successful asynchronous token acquisition with real API call."""
87+
# Clear any existing token
88+
client_credentials_auth._token_info = None
89+
90+
token = await client_credentials_auth.get_token()
91+
92+
# Verify token structure
93+
assert isinstance(token, TokenInfo)
94+
assert token.access_token is not None
95+
assert len(token.access_token) > 10
96+
assert token.token_type == "Bearer"
97+
assert token.expires_in > 0
98+
99+
# Verify token is valid and fresh
100+
assert not token.is_expired
101+
assert not token.should_refresh
102+
103+
def test_get_auth_headers_sync(self, client_credentials_auth):
104+
"""Test getting auth headers with a real token."""
105+
# Get a real token first
106+
token = client_credentials_auth.get_token_sync()
107+
108+
# Get auth headers
109+
headers = client_credentials_auth.get_auth_headers_sync()
110+
111+
assert "Authorization" in headers
112+
expected_header = f"{token.token_type} {token.access_token}"
113+
assert headers["Authorization"] == expected_header
114+
115+
@pytest.mark.asyncio
116+
async def test_get_auth_headers_async(self, client_credentials_auth):
117+
"""Test getting auth headers asynchronously with a real token."""
118+
# Clear any existing token
119+
client_credentials_auth._token_info = None
120+
121+
# Get auth headers (should fetch token automatically)
122+
headers = await client_credentials_auth.get_auth_headers()
123+
124+
assert "Authorization" in headers
125+
assert headers["Authorization"].startswith("Bearer ")
126+
127+
# Verify token was stored
128+
assert client_credentials_auth._token_info is not None
129+
130+
def test_refresh_token_sync_success(self, client_credentials_auth):
131+
"""Test successful synchronous token refresh."""
132+
# Get initial token
133+
initial_token = client_credentials_auth.get_token_sync()
134+
initial_access_token = initial_token.access_token
135+
136+
# Refresh token (for client credentials, this gets a new token)
137+
refreshed_token = client_credentials_auth.refresh_token_sync()
138+
139+
# Verify we got a token back
140+
assert isinstance(refreshed_token, TokenInfo)
141+
assert refreshed_token.access_token is not None
142+
143+
# For client credentials flow, refresh might return the same token
144+
# if it's still valid, or a new one
145+
assert refreshed_token.token_type == "Bearer"
146+
assert refreshed_token.expires_in > 0
147+
148+
@pytest.mark.asyncio
149+
async def test_refresh_token_async_success(self, client_credentials_auth):
150+
"""Test successful asynchronous token refresh."""
151+
# Get initial token
152+
initial_token = await client_credentials_auth.get_token()
153+
154+
# Refresh token
155+
refreshed_token = await client_credentials_auth.refresh_token()
156+
157+
# Verify we got a token back
158+
assert isinstance(refreshed_token, TokenInfo)
159+
assert refreshed_token.access_token is not None
160+
assert refreshed_token.token_type == "Bearer"
161+
162+
def test_scope_validation(self, client_credentials_auth):
163+
"""Test scope validation with a real token."""
164+
# Get a real token
165+
token = client_credentials_auth.get_token_sync()
166+
167+
# Test scope checking
168+
if token.scope:
169+
granted_scopes = client_credentials_auth.get_granted_scopes()
170+
assert isinstance(granted_scopes, set)
171+
assert len(granted_scopes) > 0
172+
173+
# Test has_scope method
174+
for scope in granted_scopes:
175+
assert client_credentials_auth.has_scope(scope)
176+
177+
# Test with a scope we definitely don't have
178+
assert not client_credentials_auth.has_scope("non.existent.scope")
179+
180+
def test_multiple_scopes_request(self, integration_config):
181+
"""Test requesting multiple scopes."""
182+
auth = ClientCredentials(
183+
client_id=TEST_CLIENT_ID,
184+
client_secret=TEST_CLIENT_SECRET,
185+
scopes=["vendor.profile.read", "vendor.profile.write", "vendor.product.read"],
186+
config=integration_config
187+
)
188+
189+
token = auth.get_token_sync()
190+
191+
assert isinstance(token, TokenInfo)
192+
assert token.access_token is not None
193+
194+
# Check if multiple scopes were granted (if the API supports it)
195+
if token.scope:
196+
granted_scopes = token.granted_scopes
197+
# We should have at least one of our requested scopes
198+
requested_scopes = {"vendor.profile.read", "vendor.profile.write", "vendor.product.read"}
199+
assert len(granted_scopes.intersection(requested_scopes)) > 0
200+
201+
202+
@pytest.mark.integration
203+
class TestErrorHandlingIntegration:
204+
"""Integration tests for error handling with real API calls."""
205+
206+
def test_invalid_client_credentials(self, integration_config):
207+
"""Test behavior with invalid client credentials."""
208+
auth = ClientCredentials(
209+
client_id="invalid_client_id",
210+
client_secret="invalid_client_secret",
211+
scopes=["vendor.profile.read"],
212+
config=integration_config
213+
)
214+
215+
# Should raise an authentication error
216+
with pytest.raises(BasalamAuthError) as exc_info:
217+
auth.get_token_sync()
218+
219+
assert "Failed to get access token" in str(exc_info.value)
220+
221+
def test_invalid_scope_request(self, integration_config):
222+
"""Test behavior when requesting invalid scopes."""
223+
auth = ClientCredentials(
224+
client_id=TEST_CLIENT_ID,
225+
client_secret=TEST_CLIENT_SECRET,
226+
scopes=["completely.invalid.scope.that.does.not.exist"],
227+
config=integration_config
228+
)
229+
230+
try:
231+
token = auth.get_token_sync()
232+
if token.scope:
233+
assert "completely.invalid.scope.that.does.not.exist" not in token.granted_scopes
234+
except BasalamAuthError:
235+
pass
236+
237+
@pytest.mark.asyncio
238+
async def test_network_timeout_handling(self, integration_config):
239+
"""Test behavior with very short timeout."""
240+
short_timeout_config = BasalamConfig(
241+
environment=Environment.PRODUCTION,
242+
timeout=0.001 # 1ms - should timeout
243+
)
244+
245+
auth = ClientCredentials(
246+
client_id=TEST_CLIENT_ID,
247+
client_secret=TEST_CLIENT_SECRET,
248+
config=short_timeout_config
249+
)
250+
251+
with pytest.raises(BasalamAuthError):
252+
await auth.get_token()

tests/test_search_client.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
Integration tests for the Search service client.
3+
"""
4+
import pytest
5+
from typing import Dict, Any
6+
7+
from basalam_sdk import BasalamClient
8+
from basalam_sdk.search import SearchService
9+
from basalam_sdk.search.models import ProductSearchModel
10+
from basalam_sdk.config import BasalamConfig, Environment
11+
from basalam_sdk.auth import ClientCredentials
12+
13+
14+
# Test client credentials
15+
TEST_CLIENT_ID = ""
16+
TEST_CLIENT_SECRET = ""
17+
18+
19+
@pytest.fixture
20+
def basalam_config():
21+
"""Create a real config for search integration testing."""
22+
return BasalamConfig(
23+
environment=Environment.PRODUCTION,
24+
timeout=30.0,
25+
user_agent="SearchService Integration Test Agent"
26+
)
27+
28+
29+
@pytest.fixture
30+
def basalam_auth(basalam_config):
31+
"""Create a real auth instance for search integration testing."""
32+
return ClientCredentials(
33+
client_id=TEST_CLIENT_ID,
34+
client_secret=TEST_CLIENT_SECRET,
35+
scopes=["vendor.profile.read", "vendor.profile.write", "search.read"],
36+
config=basalam_config
37+
)
38+
39+
40+
@pytest.fixture
41+
def basalam_client(basalam_auth, basalam_config):
42+
"""Create a real SearchService client for integration testing."""
43+
return BasalamClient(auth=basalam_auth, config=basalam_config)
44+
45+
46+
@pytest.mark.integration
47+
class TestSearchServiceIntegration:
48+
"""Integration tests for SearchService."""
49+
50+
@pytest.mark.asyncio
51+
async def test_search_products_async(self, basalam_client):
52+
"""Test searching products asynchronously."""
53+
search_request = ProductSearchModel(
54+
query="laptop",
55+
limit=5,
56+
offset=0
57+
)
58+
59+
try:
60+
response = await basalam_client.search_products(search_request)
61+
assert isinstance(response, dict)
62+
# Search results should have data
63+
if 'data' in response:
64+
assert isinstance(response['data'], list)
65+
elif 'results' in response:
66+
assert isinstance(response['results'], list)
67+
except Exception as e:
68+
# May fail due to permissions or API limitations
69+
print(f"Expected API call may fail: {e}")
70+
assert "401" in str(e) or "403" in str(e) or "not found" in str(e).lower()
71+
72+
def test_search_products_sync(self, basalam_client):
73+
"""Test searching products synchronously."""
74+
search_request = ProductSearchModel(
75+
query="laptop",
76+
limit=5,
77+
offset=0
78+
)
79+
80+
try:
81+
response = basalam_client.search_products_sync(search_request)
82+
assert isinstance(response, dict)
83+
# Search results should have data
84+
if 'data' in response:
85+
assert isinstance(response['data'], list)
86+
elif 'results' in response:
87+
assert isinstance(response['results'], list)
88+
except Exception as e:
89+
# May fail due to permissions or API limitations
90+
print(f"Expected API call may fail: {e}")
91+
assert "401" in str(e) or "403" in str(e) or "not found" in str(e).lower()

0 commit comments

Comments
 (0)