Initial release: - Complete async API client (45+ endpoints) - Pydantic models for all responses - Two-step authentication - Comprehensive error handling - 92% test coverage - Gitea Actions CI/CD - Semantic release configured
333 lines
10 KiB
Python
333 lines
10 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, ValidationResponse, ValidationIssue
|
|
|
|
|
|
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
|
|
|
|
|
|
class TestValidationAPI:
|
|
"""Test text validation endpoint."""
|
|
|
|
@respx.mock
|
|
async def test_validate_text_success(self):
|
|
"""Test successful text validation."""
|
|
client = KworkClient(token="test")
|
|
|
|
mock_data = {
|
|
"success": True,
|
|
"is_valid": True,
|
|
"issues": [],
|
|
"score": 95,
|
|
}
|
|
|
|
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
|
return_value=Response(200, json=mock_data)
|
|
)
|
|
|
|
result = await client.other.validate_text("Хороший текст для кворка")
|
|
|
|
assert isinstance(result, ValidationResponse)
|
|
assert result.success is True
|
|
assert result.is_valid is True
|
|
assert len(result.issues) == 0
|
|
assert result.score == 95
|
|
|
|
@respx.mock
|
|
async def test_validate_text_with_issues(self):
|
|
"""Test text validation with found issues."""
|
|
client = KworkClient(token="test")
|
|
|
|
mock_data = {
|
|
"success": True,
|
|
"is_valid": False,
|
|
"issues": [
|
|
{
|
|
"type": "error",
|
|
"code": "CONTACT_INFO",
|
|
"message": "Текст содержит контактную информацию",
|
|
"position": 25,
|
|
"suggestion": "Удалите номер телефона",
|
|
},
|
|
{
|
|
"type": "warning",
|
|
"code": "LENGTH",
|
|
"message": "Текст слишком короткий",
|
|
},
|
|
],
|
|
"score": 45,
|
|
}
|
|
|
|
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
|
return_value=Response(200, json=mock_data)
|
|
)
|
|
|
|
result = await client.other.validate_text(
|
|
"Звоните +7-999-000-00-00",
|
|
context="kwork_description",
|
|
)
|
|
|
|
assert result.is_valid is False
|
|
assert len(result.issues) == 2
|
|
assert result.issues[0].code == "CONTACT_INFO"
|
|
assert result.issues[0].type == "error"
|
|
assert result.issues[1].type == "warning"
|
|
assert result.score == 45
|
|
|
|
@respx.mock
|
|
async def test_validate_text_empty(self):
|
|
"""Test validation of empty text."""
|
|
client = KworkClient(token="test")
|
|
|
|
mock_data = {
|
|
"success": False,
|
|
"is_valid": False,
|
|
"message": "Текст не может быть пустым",
|
|
"issues": [],
|
|
}
|
|
|
|
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
|
return_value=Response(200, json=mock_data)
|
|
)
|
|
|
|
result = await client.other.validate_text("")
|
|
|
|
assert result.success is False
|
|
assert result.message is not None
|