diff --git a/.ipynb_checkpoints/Untitled-checkpoint.ipynb b/.ipynb_checkpoints/Untitled-checkpoint.ipynb new file mode 100644 index 0000000..363fcab --- /dev/null +++ b/.ipynb_checkpoints/Untitled-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Untitled.ipynb b/Untitled.ipynb new file mode 100644 index 0000000..c841c93 --- /dev/null +++ b/Untitled.ipynb @@ -0,0 +1,97 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "f28552f1-618c-4853-92e2-566554a2de2c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import asyncio\n", + "from kwork_api import KworkClient\n", + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "load_dotenv('tests/e2e/.env')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "953d142e-a575-41b7-927d-8cd1546d2747", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KworkAuthError: Login failed: 404\n" + ] + } + ], + "source": [ + "try:\n", + " client = await KworkClient.login(\n", + " username=os.getenv('KWORK_USERNAME'),\n", + " password=os.getenv('KWORK_PASSWORD')\n", + " )\n", + " print(f\"✅ Logged in as: {client.token[:20]}...\")\n", + "except Exception as e:\n", + " print(e)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "655aa71e-5645-4c7a-aadd-5b044a0713c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'8AQhyzQRcTJ6v81maCNa'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "os.getenv('KWORK_PASSWORD')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/kwork_api/__pycache__/__init__.cpython-312.pyc b/src/kwork_api/__pycache__/__init__.cpython-312.pyc index 0e5b60d..e761467 100644 Binary files a/src/kwork_api/__pycache__/__init__.cpython-312.pyc and b/src/kwork_api/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/kwork_api/__pycache__/client.cpython-312.pyc b/src/kwork_api/__pycache__/client.cpython-312.pyc index 455e83c..43a1a1d 100644 Binary files a/src/kwork_api/__pycache__/client.cpython-312.pyc and b/src/kwork_api/__pycache__/client.cpython-312.pyc differ diff --git a/src/kwork_api/__pycache__/errors.cpython-312.pyc b/src/kwork_api/__pycache__/errors.cpython-312.pyc index adda591..d940f65 100644 Binary files a/src/kwork_api/__pycache__/errors.cpython-312.pyc and b/src/kwork_api/__pycache__/errors.cpython-312.pyc differ diff --git a/src/kwork_api/__pycache__/models.cpython-312.pyc b/src/kwork_api/__pycache__/models.cpython-312.pyc index 624181c..49bfc6c 100644 Binary files a/src/kwork_api/__pycache__/models.cpython-312.pyc and b/src/kwork_api/__pycache__/models.cpython-312.pyc differ diff --git a/src/kwork_api/client.py b/src/kwork_api/client.py index e08064d..16b5c37 100644 --- a/src/kwork_api/client.py +++ b/src/kwork_api/client.py @@ -81,8 +81,6 @@ class KworkClient: """ BASE_URL = "https://api.kwork.ru" - LOGIN_URL = "https://kwork.ru/signIn" - TOKEN_URL = "https://kwork.ru/getWebAuthToken" def __init__( self, @@ -241,38 +239,41 @@ class KworkClient: try: async with client._get_httpx_client() as http_client: - # Step 1: Login to get session cookies + # Step 1: Login to get session cookies and token + # Kwork uses /api/user/login with JSON body login_data = { - "login_or_email": username, - "password": password, + "l_username": username, + "l_password": password, + "jlog": 1, + "recaptcha_pass_token": "", + "track_client_id": False, + "smart-token": "", + "l_remember_me": "1", } response = await http_client.post( - cls.LOGIN_URL, - data=login_data, - headers={"Referer": "https://kwork.ru/"}, + "https://kwork.ru/api/user/login", + json=login_data, + headers={ + "Accept": "application/json, text/plain, */*", + "X-Requested-With": "XMLHttpRequest", + }, ) if response.status_code != 200: raise KworkAuthError(f"Login failed: {response.status_code}") - # Extract cookies + response_data = response.json() + + # Extract userId from response or cookies cookies = dict(response.cookies) + user_id = response_data.get("user_id") or cookies.get("userId") - if "userId" not in cookies: - raise KworkAuthError("Login failed: no userId in cookies") + if not user_id: + raise KworkAuthError("Login failed: no userId in response") - # Step 2: Get web auth token - token_response = await http_client.post( - cls.TOKEN_URL, - json={}, - ) - - if token_response.status_code != 200: - raise KworkAuthError(f"Token request failed: {token_response.status_code}") - - token_data = token_response.json() - web_token = token_data.get("web_auth_token") + # Extract web_auth_token from response + web_token = response_data.get("web_auth_token") or cookies.get("web_auth_token") if not web_token: raise KworkAuthError("No web_auth_token in response") @@ -1174,9 +1175,7 @@ class KworkClient: """ return await self.client._request("POST", "/actor") - async def validate_text( - self, text: str, context: str | None = None - ) -> ValidationResponse: + async def validate_text(self, text: str, context: str | None = None) -> ValidationResponse: """ Проверить текст на соответствие требованиям Kwork. diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index ab0a7bc..980f2fb 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -8,7 +8,7 @@ import pytest import respx from httpx import Response -from kwork_api import KworkApiError, KworkAuthError, KworkClient +from kwork_api import KworkApiError, KworkAuthError, KworkClient, KworkNetworkError from kwork_api.models import CatalogResponse, ValidationResponse @@ -18,22 +18,11 @@ class TestAuthentication: @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( + login_route = respx.post("https://kwork.ru/api/user/login").mock( return_value=Response( 200, - json={"web_auth_token": "test_token_abc123"}, + json={"user_id": 12345, "web_auth_token": "test_token_abc123"}, ) ) @@ -42,13 +31,12 @@ class TestAuthentication: # 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( + respx.post("https://kwork.ru/api/user/login").mock( return_value=Response(401, json={"error": "Invalid credentials"}) ) @@ -57,11 +45,9 @@ class TestAuthentication: @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"}) + """Test login without userId in response.""" + respx.post("https://kwork.ru/api/user/login").mock( + return_value=Response(200, json={"error": "No user_id"}) ) with pytest.raises(KworkAuthError, match="no userId"): @@ -70,14 +56,8 @@ class TestAuthentication: @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"}) + respx.post("https://kwork.ru/api/user/login").mock( + return_value=Response(200, json={"user_id": 123}) ) with pytest.raises(KworkAuthError, match="No web_auth_token"): @@ -177,7 +157,9 @@ class TestProjectsAPI: "pagination": {"current_page": 1}, } - respx.post(f"{client.base_url}/projects").mock(return_value=Response(200, json=mock_data)) + respx.post(f"{client.base_url}/projects").mock( + return_value=Response(200, json=mock_data) + ) result = await client.projects.get_list() @@ -193,7 +175,9 @@ class TestErrorHandling: """Test 404 error handling.""" client = KworkClient(token="test") - respx.post(f"{client.base_url}/getKworkDetails").mock(return_value=Response(404)) + 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) @@ -205,7 +189,9 @@ class TestErrorHandling: """Test 401 error handling.""" client = KworkClient(token="invalid") - respx.post(f"{client.base_url}/catalogMainv2").mock(return_value=Response(401)) + respx.post(f"{client.base_url}/catalogMainv2").mock( + return_value=Response(401) + ) with pytest.raises(KworkAuthError): await client.catalog.get_list() @@ -213,10 +199,12 @@ class TestErrorHandling: @respx.mock async def test_network_error(self): """Test network error handling.""" + import httpx + client = KworkClient(token="test") respx.post(f"{client.base_url}/catalogMainv2").mock( - side_effect=Exception("Connection refused") + side_effect=httpx.RequestError("Connection refused", request=None) ) with pytest.raises(KworkNetworkError): @@ -231,9 +219,6 @@ class TestContextManager: 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