Features: - Full API coverage (45 endpoints from HAR analysis) - Async/await support with httpx - Pydantic models for all responses - Clear error handling (KworkAuthError, KworkApiError, etc.) - Session management (cookies + web_auth_token) - Unit tests with respx mocks - Integration tests template - JSON logging support via structlog Endpoints implemented: - Authentication: signIn, getWebAuthToken - Catalog: catalogMainv2, getKworkDetails, getKworkDetailsExtra - Projects: projects, payerOrders, workerOrders - User: user, userReviews, favoriteKworks - Reference: cities, countries, timezones, features, badges - Notifications: notifications, notificationsFetch, dialogs - Other: 30+ additional endpoints Tests: 13 passed, 79% coverage
243 lines
7.3 KiB
Python
243 lines
7.3 KiB
Python
"""
|
|
Unit tests for KworkClient with mocks.
|
|
|
|
These tests use respx for HTTP mocking and don't require real API access.
|
|
"""
|
|
|
|
import pytest
|
|
import respx
|
|
from httpx import Response
|
|
|
|
from kwork_api import KworkClient, KworkAuthError, KworkApiError
|
|
from kwork_api.models import CatalogResponse, Kwork
|
|
|
|
|
|
class TestAuthentication:
|
|
"""Test authentication flows."""
|
|
|
|
@respx.mock
|
|
async def test_login_success(self):
|
|
"""Test successful login."""
|
|
import httpx
|
|
|
|
# Mock login endpoint
|
|
login_route = respx.post("https://kwork.ru/signIn")
|
|
login_route.mock(return_value=httpx.Response(
|
|
200,
|
|
headers={"Set-Cookie": "userId=12345; slrememberme=token123"},
|
|
))
|
|
|
|
# Mock token endpoint
|
|
token_route = respx.post("https://kwork.ru/getWebAuthToken").mock(
|
|
return_value=Response(
|
|
200,
|
|
json={"web_auth_token": "test_token_abc123"},
|
|
)
|
|
)
|
|
|
|
# Login
|
|
client = await KworkClient.login("testuser", "testpass")
|
|
|
|
# Verify
|
|
assert login_route.called
|
|
assert token_route.called
|
|
assert client._token == "test_token_abc123"
|
|
|
|
@respx.mock
|
|
async def test_login_invalid_credentials(self):
|
|
"""Test login with invalid credentials."""
|
|
respx.post("https://kwork.ru/signIn").mock(
|
|
return_value=Response(401, json={"error": "Invalid credentials"})
|
|
)
|
|
|
|
with pytest.raises(KworkAuthError):
|
|
await KworkClient.login("wrong", "wrong")
|
|
|
|
@respx.mock
|
|
async def test_login_no_userid(self):
|
|
"""Test login without userId in cookies."""
|
|
import httpx
|
|
|
|
respx.post("https://kwork.ru/signIn").mock(
|
|
return_value=httpx.Response(200, headers={"Set-Cookie": "other=value"})
|
|
)
|
|
|
|
with pytest.raises(KworkAuthError, match="no userId"):
|
|
await KworkClient.login("test", "test")
|
|
|
|
@respx.mock
|
|
async def test_login_no_token(self):
|
|
"""Test login without web_auth_token in response."""
|
|
import httpx
|
|
|
|
respx.post("https://kwork.ru/signIn").mock(
|
|
return_value=httpx.Response(200, headers={"Set-Cookie": "userId=123"})
|
|
)
|
|
|
|
respx.post("https://kwork.ru/getWebAuthToken").mock(
|
|
return_value=Response(200, json={"other": "data"})
|
|
)
|
|
|
|
with pytest.raises(KworkAuthError, match="No web_auth_token"):
|
|
await KworkClient.login("test", "test")
|
|
|
|
def test_init_with_token(self):
|
|
"""Test client initialization with token."""
|
|
client = KworkClient(token="test_token")
|
|
assert client._token == "test_token"
|
|
|
|
|
|
class TestCatalogAPI:
|
|
"""Test catalog endpoints."""
|
|
|
|
@respx.mock
|
|
async def test_get_catalog(self):
|
|
"""Test getting catalog list."""
|
|
client = KworkClient(token="test")
|
|
|
|
mock_data = {
|
|
"kworks": [
|
|
{"id": 1, "title": "Test Kwork", "price": 1000.0},
|
|
{"id": 2, "title": "Another Kwork", "price": 2000.0},
|
|
],
|
|
"pagination": {
|
|
"current_page": 1,
|
|
"total_pages": 5,
|
|
"total_items": 100,
|
|
},
|
|
}
|
|
|
|
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
|
return_value=Response(200, json=mock_data)
|
|
)
|
|
|
|
result = await client.catalog.get_list(page=1)
|
|
|
|
assert isinstance(result, CatalogResponse)
|
|
assert len(result.kworks) == 2
|
|
assert result.kworks[0].id == 1
|
|
assert result.pagination.total_pages == 5
|
|
|
|
@respx.mock
|
|
async def test_get_kwork_details(self):
|
|
"""Test getting kwork details."""
|
|
client = KworkClient(token="test")
|
|
|
|
mock_data = {
|
|
"id": 123,
|
|
"title": "Detailed Kwork",
|
|
"price": 5000.0,
|
|
"full_description": "Full description here",
|
|
"delivery_time": 3,
|
|
}
|
|
|
|
respx.post(f"{client.base_url}/getKworkDetails").mock(
|
|
return_value=Response(200, json=mock_data)
|
|
)
|
|
|
|
result = await client.catalog.get_details(123)
|
|
|
|
assert result.id == 123
|
|
assert result.full_description == "Full description here"
|
|
assert result.delivery_time == 3
|
|
|
|
@respx.mock
|
|
async def test_catalog_error(self):
|
|
"""Test catalog API error handling."""
|
|
client = KworkClient(token="test")
|
|
|
|
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
|
return_value=Response(400, json={"message": "Invalid category"})
|
|
)
|
|
|
|
with pytest.raises(KworkApiError):
|
|
await client.catalog.get_list(category_id=99999)
|
|
|
|
|
|
class TestProjectsAPI:
|
|
"""Test projects endpoints."""
|
|
|
|
@respx.mock
|
|
async def test_get_projects(self):
|
|
"""Test getting projects list."""
|
|
client = KworkClient(token="test")
|
|
|
|
mock_data = {
|
|
"projects": [
|
|
{
|
|
"id": 1,
|
|
"title": "Test Project",
|
|
"description": "Test description",
|
|
"budget": 10000.0,
|
|
"status": "open",
|
|
}
|
|
],
|
|
"pagination": {"current_page": 1},
|
|
}
|
|
|
|
respx.post(f"{client.base_url}/projects").mock(
|
|
return_value=Response(200, json=mock_data)
|
|
)
|
|
|
|
result = await client.projects.get_list()
|
|
|
|
assert len(result.projects) == 1
|
|
assert result.projects[0].budget == 10000.0
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Test error handling."""
|
|
|
|
@respx.mock
|
|
async def test_404_error(self):
|
|
"""Test 404 error handling."""
|
|
client = KworkClient(token="test")
|
|
|
|
respx.post(f"{client.base_url}/getKworkDetails").mock(
|
|
return_value=Response(404)
|
|
)
|
|
|
|
with pytest.raises(KworkApiError) as exc_info:
|
|
await client.catalog.get_details(999)
|
|
|
|
assert exc_info.value.status_code == 404
|
|
|
|
@respx.mock
|
|
async def test_401_error(self):
|
|
"""Test 401 error handling."""
|
|
client = KworkClient(token="invalid")
|
|
|
|
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
|
return_value=Response(401)
|
|
)
|
|
|
|
with pytest.raises(KworkAuthError):
|
|
await client.catalog.get_list()
|
|
|
|
@respx.mock
|
|
async def test_network_error(self):
|
|
"""Test network error handling."""
|
|
client = KworkClient(token="test")
|
|
|
|
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
|
side_effect=Exception("Connection refused")
|
|
)
|
|
|
|
with pytest.raises(Exception):
|
|
await client.catalog.get_list()
|
|
|
|
|
|
class TestContextManager:
|
|
"""Test async context manager."""
|
|
|
|
async def test_context_manager(self):
|
|
"""Test using client as context manager."""
|
|
async with KworkClient(token="test") as client:
|
|
assert client._client is None # Not created yet
|
|
|
|
# Client should be created on first request
|
|
# (but we don't make actual requests in this test)
|
|
|
|
# Client should be closed after context
|
|
assert client._client is None or client._client.is_closed
|