""" 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