test: use session-scoped e2e_client fixture (login once per session)

- Add event_loop fixture with session scope
- Add e2e_client fixture that logs in ONCE for all catalog tests
- Add catalog_kwork_id fixture that fetches kwork ID ONCE
- All catalog tests now reuse the same authenticated client
- Reduces login calls from 10 to 1 per test session
This commit is contained in:
root 2026-03-29 23:55:38 +00:00
parent adcb22dc75
commit 94978ccdf1
3 changed files with 108 additions and 129 deletions

View File

@ -89,7 +89,7 @@ line-ending = "auto"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "module" asyncio_default_fixture_loop_scope = "session"
testpaths = ["tests"] testpaths = ["tests"]
addopts = "-v --tb=short" addopts = "-v --tb=short"

View File

@ -4,12 +4,15 @@ E2E тесты для Kwork API.
Требуют реальных credentials и запускаются только локально. Требуют реальных credentials и запускаются только локально.
""" """
import asyncio
import os import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from dotenv import load_dotenv from dotenv import load_dotenv
from kwork_api import KworkClient
# Загружаем .env # Загружаем .env
load_dotenv(Path(__file__).parent / ".env") load_dotenv(Path(__file__).parent / ".env")
@ -34,6 +37,42 @@ def require_credentials(kwork_credentials):
return kwork_credentials return kwork_credentials
@pytest.fixture(scope="session")
def event_loop():
"""Create session-scoped event loop for all E2E tests."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def e2e_client(require_credentials):
"""
E2E клиент - логинится ОДИН РАЗ для всех тестов сессии.
Используется во всех тестах кроме test_auth.py (там тестируем сам логин).
"""
client = await KworkClient.login(
username=require_credentials["username"],
password=require_credentials["password"],
)
yield client
await client.close()
@pytest.fixture(scope="session")
async def catalog_kwork_id(e2e_client):
"""
Получить ID первого кворка из каталога.
Выполняется ОДИН РАЗ в начале сессии и переиспользуется.
"""
catalog = await e2e_client.catalog.get_list(page=1)
if len(catalog.kworks) > 0:
return catalog.kworks[0].id
return None
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def slowmo(request): def slowmo(request):
"""Задержка между тестами для rate limiting.""" """Задержка между тестами для rate limiting."""

View File

@ -1,6 +1,7 @@
""" """
E2E тесты для каталога и проектов. E2E тесты для каталога и проектов.
Используют session-scoped e2e_client fixture - логин ОДИН РАЗ для всех тестов.
Все тесты read-only - ничего не изменяют на сервере. Все тесты read-only - ничего не изменяют на сервере.
Endpoints основаны на HAR анализе (mitmproxy + har-analyzer skill). Endpoints основаны на HAR анализе (mitmproxy + har-analyzer skill).
""" """
@ -11,110 +12,73 @@ from kwork_api import KworkClient
@pytest.mark.e2e @pytest.mark.e2e
async def test_get_catalog_list(require_credentials): async def test_get_catalog_list(e2e_client):
"""E2E: Получить список кворков из каталога. """E2E: Получить список кворков из каталога.
HAR: POST https://api.kwork.ru/catalogMainv2 HAR: POST https://api.kwork.ru/catalogMainv2
""" """
client = await KworkClient.login( # Первая страница каталога
username=require_credentials["username"], catalog = await e2e_client.catalog.get_list(page=1)
password=require_credentials["password"],
)
try: assert catalog is not None
# Первая страница каталога # API может вернуть пустой список (это нормально)
catalog = await client.catalog.get_list(page=1) if len(catalog.kworks) > 0:
# Проверка структуры первого кворка
assert catalog is not None first_kwork = catalog.kworks[0]
# API может вернуть пустой список (это нормально) assert first_kwork.id is not None
if len(catalog.kworks) > 0: assert first_kwork.title is not None
# Проверка структуры первого кворка assert first_kwork.price is not None
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
finally:
await client.close()
@pytest.mark.e2e @pytest.mark.e2e
async def test_get_kwork_details(require_credentials): async def test_get_kwork_details(e2e_client, catalog_kwork_id):
"""E2E: Получить детали кворка. """E2E: Получить детали кворка.
HAR: POST https://api.kwork.ru/getKworkDetails HAR: POST https://api.kwork.ru/getKworkDetails
""" """
client = await KworkClient.login( # Пропускаем если каталог пустой
username=require_credentials["username"], if catalog_kwork_id is None:
password=require_credentials["password"], pytest.skip("Catalog is empty")
)
try: # Получаем детали
# Сначала получаем каталог чтобы найти реальный ID details = await e2e_client.catalog.get_details(catalog_kwork_id)
catalog = await client.catalog.get_list(page=1)
# Если каталог пустой - пропускаем тест assert details is not None
if len(catalog.kworks) == 0: assert details.id == catalog_kwork_id
pytest.skip("Catalog is empty") assert details.title is not None
assert details.price is not None
kwork_id = catalog.kworks[0].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 @pytest.mark.e2e
async def test_get_projects_list(require_credentials): async def test_get_projects_list(e2e_client):
"""E2E: Получить список проектов с биржи. """E2E: Получить список проектов с биржи.
HAR: POST https://api.kwork.ru/projects HAR: POST https://api.kwork.ru/projects
""" """
client = await KworkClient.login( projects = await e2e_client.projects.get_list(page=1)
username=require_credentials["username"],
password=require_credentials["password"],
)
try: assert projects is not None
projects = await client.projects.get_list(page=1) # Проекты могут быть пустыми
if len(projects.projects) > 0:
assert projects is not None first_project = projects.projects[0]
# Проекты могут быть пустыми assert first_project.id is not None
if len(projects.projects) > 0: assert first_project.title is not None
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 @pytest.mark.e2e
async def test_get_user_info(require_credentials): async def test_get_user_info(e2e_client):
"""E2E: Получить информацию о текущем пользователе. """E2E: Получить информацию о текущем пользователе.
HAR: POST https://api.kwork.ru/user HAR: POST https://api.kwork.ru/user
""" """
client = await KworkClient.login( user = await e2e_client.user.get_info()
username=require_credentials["username"], assert user is not None
password=require_credentials["password"], # API возвращает dict с данными пользователя
) assert isinstance(user, dict)
try:
user = await client.user.get_info()
assert user is not None
# API возвращает dict с данными пользователя
assert isinstance(user, dict)
finally:
await client.close()
@pytest.mark.e2e @pytest.mark.e2e
async def test_get_reference_data(require_credentials): async def test_get_reference_data(e2e_client):
"""E2E: Получить справочные данные (города, страны, фичи). """E2E: Получить справочные данные (города, страны, фичи).
HAR endpoints: HAR endpoints:
@ -123,71 +87,47 @@ async def test_get_reference_data(require_credentials):
- POST https://api.kwork.ru/getAvailableFeatures - POST https://api.kwork.ru/getAvailableFeatures
- POST https://api.kwork.ru/getBadgesInfo - POST https://api.kwork.ru/getBadgesInfo
""" """
client = await KworkClient.login( # Города (может вернуть пустой список)
username=require_credentials["username"], cities = await e2e_client.reference.get_cities()
password=require_credentials["password"], assert isinstance(cities, list)
)
try: # Страны (может вернуть пустой список)
# Города (может вернуть пустой список) countries = await e2e_client.reference.get_countries()
cities = await client.reference.get_cities() assert isinstance(countries, list)
assert isinstance(cities, list)
# Страны (может вернуть пустой список) # Фичи
countries = await client.reference.get_countries() features = await e2e_client.reference.get_features()
assert isinstance(countries, list) assert isinstance(features, list)
# Фичи # Бейджи
features = await client.reference.get_features() badges = await e2e_client.reference.get_badges_info()
assert isinstance(features, list) assert isinstance(badges, list)
# Бейджи
badges = await client.reference.get_badges_info()
assert isinstance(badges, list)
finally:
await client.close()
@pytest.mark.e2e @pytest.mark.e2e
async def test_get_notifications(require_credentials): async def test_get_notifications(e2e_client):
"""E2E: Получить уведомления. """E2E: Получить уведомления.
HAR: POST https://api.kwork.ru/notifications HAR: POST https://api.kwork.ru/notifications
""" """
client = await KworkClient.login( notifications = await e2e_client.notifications.get_list()
username=require_credentials["username"], assert notifications is not None
password=require_credentials["password"], # Уведомления могут быть пустыми
) assert hasattr(notifications, 'notifications')
try:
notifications = await client.notifications.get_list()
assert notifications is not None
# Уведомления могут быть пустыми
assert hasattr(notifications, 'notifications')
finally:
await client.close()
@pytest.mark.e2e @pytest.mark.e2e
async def test_get_user_orders(require_credentials): async def test_get_user_orders(e2e_client):
"""E2E: Получить заказы пользователя. """E2E: Получить заказы пользователя.
HAR endpoints: HAR endpoints:
- POST https://api.kwork.ru/payerOrders - POST https://api.kwork.ru/payerOrders
- POST https://api.kwork.ru/workerOrders - POST https://api.kwork.ru/workerOrders
""" """
client = await KworkClient.login( # Заказы как заказчик
username=require_credentials["username"], payer_orders = await e2e_client.projects.get_payer_orders()
password=require_credentials["password"], assert isinstance(payer_orders, list)
)
try: # Заказы как исполнитель
# Заказы как заказчик worker_orders = await e2e_client.projects.get_worker_orders()
payer_orders = await client.projects.get_payer_orders() assert isinstance(worker_orders, list)
assert isinstance(payer_orders, list)
# Заказы как исполнитель
worker_orders = await client.projects.get_worker_orders()
assert isinstance(worker_orders, list)
finally:
await client.close()