diff --git a/Untitled.ipynb b/Untitled.ipynb index a0585d4..861b305 100644 --- a/Untitled.ipynb +++ b/Untitled.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "f28552f1-618c-4853-92e2-566554a2de2c", "metadata": {}, "outputs": [ @@ -12,13 +12,14 @@ "True" ] }, - "execution_count": 4, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import asyncio\n", + "import logging\n", "from kwork_api import KworkClient\n", "from dotenv import load_dotenv\n", "import os\n", @@ -30,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "953d142e-a575-41b7-927d-8cd1546d2747", "metadata": {}, "outputs": [ @@ -38,28 +39,40 @@ "name": "stderr", "output_type": "stream", "text": [ - "INFO:kwork_api.client:Login request: POST https://kwork.ru/api/user/login (user: JTJagOmega)\n", - "DEBUG:kwork_api.client:Login payload: {'l_username': 'JTJagOmega', 'l_password': '8AQhyzQRcTJ6v81maCNa', 'jlog': 1, 'recaptcha_pass_token': '', 'track_client_id': False, 'smart-token': '', 'l_remember_me': '1'}\n", - "DEBUG:httpcore.connection:connect_tcp.started host='kwork.ru' port=443 local_address=None timeout=30.0 socket_options=None\n", - "DEBUG:httpcore.connection:connect_tcp.complete return_value=\n", - "DEBUG:httpcore.connection:start_tls.started ssl_context= server_hostname='kwork.ru' timeout=30.0\n", - "DEBUG:httpcore.connection:start_tls.complete return_value=\n", + "INFO:kwork_api.client:Login request: POST https://api.kwork.ru/signIn (user: JTJagOmega)\n", + "DEBUG:kwork_api.client:Login payload: {'login': 'JTJagOmega', 'password': '8AQhyzQRcTJ6v81maCNa', 'uad': '', 'device': ''}\n", + "DEBUG:httpcore.connection:connect_tcp.started host='api.kwork.ru' port=443 local_address=None timeout=30.0 socket_options=None\n", + "DEBUG:httpcore.connection:connect_tcp.complete return_value=\n", + "DEBUG:httpcore.connection:start_tls.started ssl_context= server_hostname='api.kwork.ru' timeout=30.0\n", + "DEBUG:httpcore.connection:start_tls.complete return_value=\n", "DEBUG:httpcore.http11:send_request_headers.started request=\n", "DEBUG:httpcore.http11:send_request_headers.complete\n", "DEBUG:httpcore.http11:send_request_body.started request=\n", "DEBUG:httpcore.http11:send_request_body.complete\n", "DEBUG:httpcore.http11:receive_response_headers.started request=\n", - "DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Server', b'QRATOR'), (b'Date', b'Sun, 29 Mar 2026 22:22:41 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Keep-Alive', b'timeout=15'), (b'Vary', b'Accept-Encoding, User-Agent'), (b'Content-Security-Policy', b\"frame-ancestors 'self' https://webvisor.com https://awards.ratingruneta.ru\"), (b'Set-Cookie', b'referrer_url=https%3A%2F%2Fkwork.ru%2F; expires=Sun, 05-Apr-2026 22:22:41 GMT; Max-Age=604800; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'uad=1884597369c9a63194ed0624319983; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'RORSSQIHEK=f15239f2927f4fd08e6945c15ed635c2; expires=Wed, 01-Apr-2026 22:22:41 GMT; Max-Age=259200; path=/; secure; HttpOnly; SameSite=None'), (b'Expires', b'Thu, 19 Nov 1981 08:52:00 GMT'), (b'Cache-Control', b'no-store, no-cache, must-revalidate'), (b'Pragma', b'no-cache'), (b'Set-Cookie', b'csrf_user_token=43ed1b44d6a5a480418b39929da62605; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'userId=18845973; expires=Wed, 26-Mar-2036 22:22:41 GMT; Max-Age=315360000; path=/; secure; SameSite=None'), (b'Set-Cookie', b'slrememberme=18845973_%242y%2410%24GEnC83HAU.ejn2CQB3OMTewWzSYxC0NYcSB3n2ck6eNvcz2aStK0W; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'_kmid=7fb7f3a407728e8d0ffa5ab4d19ff2b6; expires=Wed, 26-Mar-2036 22:22:41 GMT; Max-Age=315360000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'_kmfvt=1774822961; expires=Wed, 26-Mar-2036 22:22:41 GMT; Max-Age=315360000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'csrf_user_token=515cb2f621700da0faf4c3da66efbbfb; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Cache-Control', b'no-cache, private'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'X-Content-Type-Options', b'nosniff'), (b'Content-Encoding', b'gzip')])\n", - "INFO:httpx:HTTP Request: POST https://kwork.ru/api/user/login \"HTTP/1.1 200 OK\"\n", + "DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Server', b'QRATOR'), (b'Date', b'Sun, 29 Mar 2026 23:32:52 GMT'), (b'Content-Type', b'application/json; charset=utf-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Keep-Alive', b'timeout=15'), (b'Vary', b'Accept-Encoding, User-Agent'), (b'Content-Security-Policy', b\"frame-ancestors 'self' https://webvisor.com https://awards.ratingruneta.ru\"), (b'Set-Cookie', b'RORSSQIHEK=a4fe3ce9aadf6c71d101f2914ddc4594; expires=Wed, 01-Apr-2026 23:32:52 GMT; Max-Age=259200; path=/; secure; HttpOnly; SameSite=None'), (b'Expires', b'Thu, 19 Nov 1981 08:52:00 GMT'), (b'Cache-Control', b'no-store, no-cache, must-revalidate'), (b'Pragma', b'no-cache'), (b'Set-Cookie', b'csrf_user_token=fd9c64e5301c952c692d6b60e0693d05; expires=Mon, 29-Mar-2027 23:32:52 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'userId=18845973; expires=Wed, 26-Mar-2036 23:32:52 GMT; Max-Age=315360000; path=/; secure; SameSite=None'), (b'Set-Cookie', b'slrememberme=18845973_%242y%2410%24tbgXEdroo8Dg3L6TjfGfDe6tmbPkwd1.aU4lMgEODFpxNTqKlr3v6; expires=Mon, 29-Mar-2027 23:32:52 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'Content-Encoding', b'gzip')])\n", + "INFO:httpx:HTTP Request: POST https://api.kwork.ru/signIn \"HTTP/1.1 200 OK\"\n", "DEBUG:httpcore.http11:receive_response_body.started request=\n", "DEBUG:httpcore.http11:receive_response_body.complete\n", "DEBUG:httpcore.http11:response_closed.started\n", "DEBUG:httpcore.http11:response_closed.complete\n", "DEBUG:kwork_api.client:Login response status: 200\n", - "DEBUG:kwork_api.client:Login response headers: {'server': 'QRATOR', 'date': 'Sun, 29 Mar 2026 22:22:41 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'keep-alive': 'timeout=15', 'vary': 'Accept-Encoding, User-Agent', 'content-security-policy': \"frame-ancestors 'self' https://webvisor.com https://awards.ratingruneta.ru\", 'set-cookie': 'referrer_url=https%3A%2F%2Fkwork.ru%2F; expires=Sun, 05-Apr-2026 22:22:41 GMT; Max-Age=604800; path=/; secure; HttpOnly; SameSite=None, uad=1884597369c9a63194ed0624319983; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None, RORSSQIHEK=f15239f2927f4fd08e6945c15ed635c2; expires=Wed, 01-Apr-2026 22:22:41 GMT; Max-Age=259200; path=/; secure; HttpOnly; SameSite=None, csrf_user_token=43ed1b44d6a5a480418b39929da62605; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None, userId=18845973; expires=Wed, 26-Mar-2036 22:22:41 GMT; Max-Age=315360000; path=/; secure; SameSite=None, slrememberme=18845973_%242y%2410%24GEnC83HAU.ejn2CQB3OMTewWzSYxC0NYcSB3n2ck6eNvcz2aStK0W; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None, _kmid=7fb7f3a407728e8d0ffa5ab4d19ff2b6; expires=Wed, 26-Mar-2036 22:22:41 GMT; Max-Age=315360000; path=/; secure; HttpOnly; SameSite=None, _kmfvt=1774822961; expires=Wed, 26-Mar-2036 22:22:41 GMT; Max-Age=315360000; path=/; secure; HttpOnly; SameSite=None, csrf_user_token=515cb2f621700da0faf4c3da66efbbfb; expires=Mon, 29-Mar-2027 22:22:41 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None', 'expires': 'Thu, 19 Nov 1981 08:52:00 GMT', 'cache-control': 'no-store, no-cache, must-revalidate, no-cache, private', 'pragma': 'no-cache', 'strict-transport-security': 'max-age=15552000', 'x-content-type-options': 'nosniff', 'content-encoding': 'gzip'}\n", - "INFO:kwork_api.client:Login successful: user_id=18845973, csrf_token=515cb2f621700da0faf4\n", - "DEBUG:kwork_api.client:Login response data: {'success': True, 'error': '', 'redirect': '', 'action_after': '', 'isUserVerified': True, 'need_2fa': False, 'csrftoken': '515cb2f621700da0faf4c3da66efbbfb'}\n", - "DEBUG:kwork_api.client:Login cookies: ['referrer_url', 'uad', 'RORSSQIHEK', 'csrf_user_token', 'userId', 'slrememberme', '_kmid', '_kmfvt']\n", + "DEBUG:kwork_api.client:Login response headers: {'server': 'QRATOR', 'date': 'Sun, 29 Mar 2026 23:32:52 GMT', 'content-type': 'application/json; charset=utf-8', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'keep-alive': 'timeout=15', 'vary': 'Accept-Encoding, User-Agent', 'content-security-policy': \"frame-ancestors 'self' https://webvisor.com https://awards.ratingruneta.ru\", 'set-cookie': 'RORSSQIHEK=a4fe3ce9aadf6c71d101f2914ddc4594; expires=Wed, 01-Apr-2026 23:32:52 GMT; Max-Age=259200; path=/; secure; HttpOnly; SameSite=None, csrf_user_token=fd9c64e5301c952c692d6b60e0693d05; expires=Mon, 29-Mar-2027 23:32:52 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None, userId=18845973; expires=Wed, 26-Mar-2036 23:32:52 GMT; Max-Age=315360000; path=/; secure; SameSite=None, slrememberme=18845973_%242y%2410%24tbgXEdroo8Dg3L6TjfGfDe6tmbPkwd1.aU4lMgEODFpxNTqKlr3v6; expires=Mon, 29-Mar-2027 23:32:52 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None', 'expires': 'Thu, 19 Nov 1981 08:52:00 GMT', 'cache-control': 'no-store, no-cache, must-revalidate', 'pragma': 'no-cache', 'strict-transport-security': 'max-age=15552000', 'x-frame-options': 'DENY', 'x-content-type-options': 'nosniff', 'content-encoding': 'gzip'}\n", + "INFO:kwork_api.client:Login successful: user_id=18845973, csrf_token=N/A\n", + "DEBUG:kwork_api.client:Login response data: {'success': True, 'response': {'token': '48e75666c8b5c3ffa97f5ae4bfa6ecfa', 'expired': 31536000, 'need_2fa': False}}\n", + "DEBUG:kwork_api.client:Login cookies: ['RORSSQIHEK', 'csrf_user_token', 'userId', 'slrememberme']\n", + "DEBUG:httpcore.http11:send_request_headers.started request=\n", + "DEBUG:httpcore.http11:send_request_headers.complete\n", + "DEBUG:httpcore.http11:send_request_body.started request=\n", + "DEBUG:httpcore.http11:send_request_body.complete\n", + "DEBUG:httpcore.http11:receive_response_headers.started request=\n", + "DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Server', b'QRATOR'), (b'Date', b'Sun, 29 Mar 2026 23:32:52 GMT'), (b'Content-Type', b'application/json; charset=utf-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Keep-Alive', b'timeout=15'), (b'Vary', b'Accept-Encoding, User-Agent'), (b'Content-Security-Policy', b\"frame-ancestors 'self' https://webvisor.com https://awards.ratingruneta.ru\"), (b'Expires', b'Thu, 19 Nov 1981 08:52:00 GMT'), (b'Cache-Control', b'no-store, no-cache, must-revalidate'), (b'Pragma', b'no-cache'), (b'Set-Cookie', b'slrememberme=18845973_%242y%2410%24Kn3Qr%2FBaJ24e5CWBpUvbBuMP38SflcAdTUtmlW1XXVEJFDVbOmO96; expires=Mon, 29-Mar-2027 23:32:52 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'uad=1884597369c9b6a47efb8415191577; expires=Mon, 29-Mar-2027 23:32:52 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=None'), (b'Set-Cookie', b'mobile_token=48e75666c8b5c3ffa97f5ae4bfa6ecfa; expires=Sun, 29-Mar-2026 23:33:52 GMT; Max-Age=60; path=/; secure; HttpOnly; SameSite=None'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'Content-Encoding', b'gzip')])\n", + "INFO:httpx:HTTP Request: POST https://api.kwork.ru/getWebAuthToken \"HTTP/1.1 200 OK\"\n", + "DEBUG:httpcore.http11:receive_response_body.started request=\n", + "DEBUG:httpcore.http11:receive_response_body.complete\n", + "DEBUG:httpcore.http11:response_closed.started\n", + "DEBUG:httpcore.http11:response_closed.complete\n", + "INFO:kwork_api.client:Got web_auth_token: DKZL5BjWmWo75GQmmCus...\n", "DEBUG:httpcore.connection:close.started\n", "DEBUG:httpcore.connection:close.complete\n" ] @@ -68,7 +81,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "✅ Logged in as: 18845973_%242y%2410%...\n" + "✅ Logged in as: DKZL5BjWmWo75GQmmCus...\n" ] } ], @@ -85,17 +98,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "655aa71e-5645-4c7a-aadd-5b044a0713c9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'18845973_%242y%2410%24GEnC83HAU.ejn2CQB3OMTewWzSYxC0NYcSB3n2ck6eNvcz2aStK0W'" + "'DKZL5BjWmWo75GQmmCusW48U3K1ZL9YUWVGs4oGnvVX5xYPYVrdzP1L6b0ko'" ] }, - "execution_count": 7, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } diff --git a/src/kwork_api/__pycache__/client.cpython-312.pyc b/src/kwork_api/__pycache__/client.cpython-312.pyc index f153b6e..5c8b1e2 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/client.py b/src/kwork_api/client.py index a19adf6..6bb6b50 100644 --- a/src/kwork_api/client.py +++ b/src/kwork_api/client.py @@ -267,13 +267,17 @@ class KworkClient: logger.debug(f"Login response headers: {dict(response.headers)}") if response.status_code != 200: - logger.error(f"Login failed with status {response.status_code}: {response.text[:200]}") + logger.error( + f"Login failed with status {response.status_code}: {response.text[:200]}" + ) raise KworkAuthError(f"Login failed: {response.status_code}") response_data = response.json() cookies = dict(response.cookies) - logger.info(f"Login successful: user_id={cookies.get('userId')}, csrf_token={response_data.get('csrftoken', 'N/A')[:20] if response_data.get('csrftoken') else 'N/A'}") + logger.info( + f"Login successful: user_id={cookies.get('userId')}, csrf_token={response_data.get('csrftoken', 'N/A')[:20] if response_data.get('csrftoken') else 'N/A'}" + ) logger.debug(f"Login response data: {response_data}") logger.debug(f"Login cookies: {list(cookies.keys())}") @@ -281,7 +285,9 @@ class KworkClient: user_id = cookies.get("userId") if not user_id: - raise KworkAuthError(f"Login failed: no userId in cookies. Response: {response_data}") + raise KworkAuthError( + f"Login failed: no userId in cookies. Response: {response_data}" + ) # HAR: getWebAuthToken endpoint for API token (same headers as signIn) token_response = await http_client.post( @@ -294,19 +300,19 @@ class KworkClient: "Accept": "application/json", }, ) - + if token_response.status_code != 200: raise KworkAuthError(f"Token request failed: {token_response.status_code}") - + token_data = token_response.json() # HAR shows: {"success":true,"response":{"token":"...", "expires_at":..., "url":...}} web_token = token_data.get("response", {}).get("token") - + if not web_token: raise KworkAuthError(f"No token in response: {token_data}") - + logger.info(f"Got web_auth_token: {web_token[:20]}...") - + # Create new client with token return cls(token=web_token, cookies=cookies, timeout=timeout) diff --git a/tests/e2e/test_auth.py b/tests/e2e/test_auth.py index 3afac91..87592b8 100644 --- a/tests/e2e/test_auth.py +++ b/tests/e2e/test_auth.py @@ -32,7 +32,7 @@ async def test_login_invalid_credentials(): @pytest.mark.e2e async def test_restore_session(require_credentials): """E2E: Восстановление сессии из token. - + HAR shows: POST /user endpoint works with proper auth token. """ # First login diff --git a/tests/e2e/test_catalog.py b/tests/e2e/test_catalog.py index e0d4a08..2df452f 100644 --- a/tests/e2e/test_catalog.py +++ b/tests/e2e/test_catalog.py @@ -2,7 +2,7 @@ E2E тесты для каталога и проектов. Все тесты read-only - ничего не изменяют на сервере. -Endpoints основаны на анализе HAR файла. +Endpoints основаны на HAR анализе (mitmproxy + har-analyzer skill). """ import pytest @@ -10,11 +10,69 @@ import pytest from kwork_api import KworkClient +@pytest.mark.e2e +async def test_get_catalog_list(require_credentials): + """E2E: Получить список кворков из каталога. + + HAR: POST https://api.kwork.ru/catalogMainv2 + """ + client = await KworkClient.login( + username=require_credentials["username"], + password=require_credentials["password"], + ) + + try: + # Первая страница каталога + catalog = await client.catalog.get_list(page=1) + + assert catalog is not None + # API может вернуть пустой список (это нормально) + if len(catalog.kworks) > 0: + # Проверка структуры первого кворка + first_kwork = catalog.kworks[0] + assert first_kwork.id is not None + assert first_kwork.title is not None + assert first_kwork.price is not None + + # Пагинация + if catalog.pagination: + assert catalog.pagination.current_page >= 1 + finally: + await client.close() + + +@pytest.mark.e2e +async def test_get_kwork_details(require_credentials): + """E2E: Получить детали кворка. + + HAR: POST https://api.kwork.ru/getKworkDetails + """ + client = await KworkClient.login( + username=require_credentials["username"], + password=require_credentials["password"], + ) + + try: + # HAR: используем известный ID кворка для теста + # В реальном использовании можно получить ID из каталога + kwork_id = 1 # Тестовый ID + + # Получаем детали + details = await client.catalog.get_details(kwork_id) + + assert details is not None + assert details.id == kwork_id + assert details.title is not None + assert details.price is not None + finally: + await client.close() + + @pytest.mark.e2e async def test_get_projects_list(require_credentials): """E2E: Получить список проектов с биржи. - Endpoint: GET https://kwork.ru/projects + HAR: POST https://api.kwork.ru/projects """ client = await KworkClient.login( username=require_credentials["username"], @@ -22,19 +80,23 @@ async def test_get_projects_list(require_credentials): ) try: - # Note: Это может возвращать HTML страницу, не JSON API - # Пока просто проверяем что запрос работает - # В будущем нужно реализовать парсинг HTML или найти JSON API endpoint - assert client is not None + projects = await client.projects.get_list(page=1) + + assert projects is not None + # Проекты могут быть пустыми + if len(projects.projects) > 0: + first_project = projects.projects[0] + assert first_project.id is not None + assert first_project.title is not None finally: await client.close() @pytest.mark.e2e -async def test_get_categories(require_credentials): - """E2E: Получить категорию. +async def test_get_user_info(require_credentials): + """E2E: Получить информацию о текущем пользователе. - Endpoint: GET https://kwork.ru/categories/{slug} + HAR: POST https://api.kwork.ru/user """ client = await KworkClient.login( username=require_credentials["username"], @@ -42,18 +104,23 @@ async def test_get_categories(require_credentials): ) try: - # Note: Это возвращает HTML страницу категории - # Пока просто проверяем что запрос работает - assert client is not None + user = await client.user.get_info() + assert user is not None + # API возвращает dict с данными пользователя + assert isinstance(user, dict) finally: await client.close() @pytest.mark.e2e -async def test_get_user_profile(require_credentials): - """E2E: Получить профиль пользователя. +async def test_get_reference_data(require_credentials): + """E2E: Получить справочные данные (города, страны, фичи). - Endpoint: GET https://kwork.ru/user/{username} + HAR endpoints: + - POST https://api.kwork.ru/cities + - POST https://api.kwork.ru/countries + - POST https://api.kwork.ru/getAvailableFeatures + - POST https://api.kwork.ru/getBadgesInfo """ client = await KworkClient.login( username=require_credentials["username"], @@ -61,18 +128,30 @@ async def test_get_user_profile(require_credentials): ) try: - # Note: Это возвращает HTML страницу профиля - # Пока просто проверяем что запрос работает - assert client is not None + # Города + cities = await client.reference.get_cities() + assert isinstance(cities, list) + + # Страны (может вернуть пустой список) + countries = await client.reference.get_countries() + assert isinstance(countries, list) + + # Фичи + features = await client.reference.get_features() + assert isinstance(features, list) + + # Бейджи + badges = await client.reference.get_badges_info() + assert isinstance(badges, list) finally: await client.close() @pytest.mark.e2e -async def test_api_checknotify(require_credentials): - """E2E: Проверить уведомления. +async def test_get_notifications(require_credentials): + """E2E: Получить уведомления. - Endpoint: POST https://kwork.ru/api/user/checknotify + HAR: POST https://api.kwork.ru/notifications """ client = await KworkClient.login( username=require_credentials["username"], @@ -80,18 +159,21 @@ async def test_api_checknotify(require_credentials): ) try: - # Note: Нужно реализовать endpoint в client.py - # Пока просто проверяем что логин работает - assert client.token is not None + notifications = await client.notifications.get_list() + assert notifications is not None + # Уведомления могут быть пустыми + assert hasattr(notifications, 'notifications') finally: await client.close() @pytest.mark.e2e -async def test_api_addview(require_credentials): - """E2E: Добавить просмотр (read-only операция). +async def test_get_user_orders(require_credentials): + """E2E: Получить заказы пользователя. - Endpoint: POST https://kwork.ru/api/offer/addview + HAR endpoints: + - POST https://api.kwork.ru/payerOrders + - POST https://api.kwork.ru/workerOrders """ client = await KworkClient.login( username=require_credentials["username"], @@ -99,31 +181,12 @@ async def test_api_addview(require_credentials): ) try: - # Note: Нужно реализовать endpoint в client.py - # Пока просто проверяем что логин работает - assert client.token is not None - finally: - await client.close() - - -@pytest.mark.e2e -async def test_get_reviews(require_credentials): - """E2E: Получить отзывы пользователя. - - Endpoint: POST https://kwork.ru/user/get_reviews - - HAR shows: - POST https://kwork.ru/user/get_reviews - {"userId":126921,"type":"positive"} - """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - # Note: Нужно реализовать endpoint в client.py с правильным путём - # Пока просто проверяем что логин работает - assert client.token is not None + # Заказы как заказчик + payer_orders = await client.projects.get_payer_orders() + assert isinstance(payer_orders, list) + + # Заказы как исполнитель + worker_orders = await client.projects.get_worker_orders() + assert isinstance(worker_orders, list) finally: await client.close() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 980f2fb..ae3ac0f 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -157,9 +157,7 @@ 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() @@ -175,9 +173,7 @@ 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) @@ -189,9 +185,7 @@ 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()