test: fix E2E tests to handle empty API responses

This commit is contained in:
root 2026-03-29 23:35:37 +00:00
parent e985e03ddb
commit bf2fa20a9d
6 changed files with 166 additions and 90 deletions

View File

@ -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=<httpcore._backends.anyio.AnyIOStream object at 0x723917c9fa70>\n",
"DEBUG:httpcore.connection:start_tls.started ssl_context=<ssl.SSLContext object at 0x723917df30d0> server_hostname='kwork.ru' timeout=30.0\n",
"DEBUG:httpcore.connection:start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x723924105d30>\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=<httpcore._backends.anyio.AnyIOStream object at 0x75bea599dd00>\n",
"DEBUG:httpcore.connection:start_tls.started ssl_context=<ssl.SSLContext object at 0x75bea597e0d0> server_hostname='api.kwork.ru' timeout=30.0\n",
"DEBUG:httpcore.connection:start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x75bec0323a10>\n",
"DEBUG:httpcore.http11:send_request_headers.started request=<Request [b'POST']>\n",
"DEBUG:httpcore.http11:send_request_headers.complete\n",
"DEBUG:httpcore.http11:send_request_body.started request=<Request [b'POST']>\n",
"DEBUG:httpcore.http11:send_request_body.complete\n",
"DEBUG:httpcore.http11:receive_response_headers.started request=<Request [b'POST']>\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=<Request [b'POST']>\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=<Request [b'POST']>\n",
"DEBUG:httpcore.http11:send_request_headers.complete\n",
"DEBUG:httpcore.http11:send_request_body.started request=<Request [b'POST']>\n",
"DEBUG:httpcore.http11:send_request_body.complete\n",
"DEBUG:httpcore.http11:receive_response_headers.started request=<Request [b'POST']>\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=<Request [b'POST']>\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"
}

View File

@ -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)

View File

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

View File

@ -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()

View File

@ -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()