diff --git a/pyproject.toml b/pyproject.toml index 8615426..e478768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ line-ending = "auto" [tool.pytest.ini_options] asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "module" +asyncio_default_fixture_loop_scope = "session" testpaths = ["tests"] addopts = "-v --tb=short" diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 1af71d7..51d1865 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -4,12 +4,15 @@ E2E тесты для Kwork API. Требуют реальных credentials и запускаются только локально. """ +import asyncio import os from pathlib import Path import pytest from dotenv import load_dotenv +from kwork_api import KworkClient + # Загружаем .env load_dotenv(Path(__file__).parent / ".env") @@ -34,6 +37,42 @@ def require_credentials(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") def slowmo(request): """Задержка между тестами для rate limiting.""" diff --git a/tests/e2e/test_catalog.py b/tests/e2e/test_catalog.py index 4dc5a36..1709e0d 100644 --- a/tests/e2e/test_catalog.py +++ b/tests/e2e/test_catalog.py @@ -1,6 +1,7 @@ """ E2E тесты для каталога и проектов. +Используют session-scoped e2e_client fixture - логин ОДИН РАЗ для всех тестов. Все тесты read-only - ничего не изменяют на сервере. Endpoints основаны на HAR анализе (mitmproxy + har-analyzer skill). """ @@ -11,110 +12,73 @@ from kwork_api import KworkClient @pytest.mark.e2e -async def test_get_catalog_list(require_credentials): +async def test_get_catalog_list(e2e_client): """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 - finally: - await client.close() + # Первая страница каталога + catalog = await e2e_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 @pytest.mark.e2e -async def test_get_kwork_details(require_credentials): +async def test_get_kwork_details(e2e_client, catalog_kwork_id): """E2E: Получить детали кворка. HAR: POST https://api.kwork.ru/getKworkDetails """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - # Сначала получаем каталог чтобы найти реальный ID - catalog = await client.catalog.get_list(page=1) - - # Если каталог пустой - пропускаем тест - if len(catalog.kworks) == 0: - pytest.skip("Catalog is empty") - - 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() + # Пропускаем если каталог пустой + if catalog_kwork_id is None: + pytest.skip("Catalog is empty") + + # Получаем детали + details = await e2e_client.catalog.get_details(catalog_kwork_id) + + assert details is not None + assert details.id == catalog_kwork_id + assert details.title is not None + assert details.price is not None @pytest.mark.e2e -async def test_get_projects_list(require_credentials): +async def test_get_projects_list(e2e_client): """E2E: Получить список проектов с биржи. HAR: POST https://api.kwork.ru/projects """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - 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() + projects = await e2e_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 @pytest.mark.e2e -async def test_get_user_info(require_credentials): +async def test_get_user_info(e2e_client): """E2E: Получить информацию о текущем пользователе. HAR: POST https://api.kwork.ru/user """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - user = await client.user.get_info() - assert user is not None - # API возвращает dict с данными пользователя - assert isinstance(user, dict) - finally: - await client.close() + user = await e2e_client.user.get_info() + assert user is not None + # API возвращает dict с данными пользователя + assert isinstance(user, dict) @pytest.mark.e2e -async def test_get_reference_data(require_credentials): +async def test_get_reference_data(e2e_client): """E2E: Получить справочные данные (города, страны, фичи). 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/getBadgesInfo """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - # Города (может вернуть пустой список) - 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() + # Города (может вернуть пустой список) + cities = await e2e_client.reference.get_cities() + assert isinstance(cities, list) + + # Страны (может вернуть пустой список) + countries = await e2e_client.reference.get_countries() + assert isinstance(countries, list) + + # Фичи + features = await e2e_client.reference.get_features() + assert isinstance(features, list) + + # Бейджи + badges = await e2e_client.reference.get_badges_info() + assert isinstance(badges, list) @pytest.mark.e2e -async def test_get_notifications(require_credentials): +async def test_get_notifications(e2e_client): """E2E: Получить уведомления. HAR: POST https://api.kwork.ru/notifications """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - notifications = await client.notifications.get_list() - assert notifications is not None - # Уведомления могут быть пустыми - assert hasattr(notifications, 'notifications') - finally: - await client.close() + notifications = await e2e_client.notifications.get_list() + assert notifications is not None + # Уведомления могут быть пустыми + assert hasattr(notifications, 'notifications') @pytest.mark.e2e -async def test_get_user_orders(require_credentials): +async def test_get_user_orders(e2e_client): """E2E: Получить заказы пользователя. HAR endpoints: - POST https://api.kwork.ru/payerOrders - POST https://api.kwork.ru/workerOrders """ - client = await KworkClient.login( - username=require_credentials["username"], - password=require_credentials["password"], - ) - - try: - # Заказы как заказчик - 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() + # Заказы как заказчик + payer_orders = await e2e_client.projects.get_payer_orders() + assert isinstance(payer_orders, list) + + # Заказы как исполнитель + worker_orders = await e2e_client.projects.get_worker_orders() + assert isinstance(worker_orders, list)