- Add smart two-phase TLS detection with caching in tests/utils/tls.py - Add auto-discovery for Chromium-GOST and Yandex.Browser - Add --tls-mode and --ignore-https-errors CLI options - Update browser_page context with ignore_https_errors and unified viewport - Add unit tests in tests/utils/test_tls.py - Update README.md and .env.example
180 lines
7.6 KiB
Python
180 lines
7.6 KiB
Python
import os
|
||
# pyrefly: ignore [missing-import]
|
||
import pytest
|
||
# pyrefly: ignore [missing-import]
|
||
from dotenv import load_dotenv
|
||
# pyrefly: ignore [missing-import]
|
||
from playwright.sync_api import Page, Playwright
|
||
|
||
from tests.utils.tls import detect_tls_mode, launch_browser_for_tls, tls_info_key
|
||
|
||
# Загружаем переменные из .env
|
||
load_dotenv()
|
||
|
||
# Базовый URL портала — берётся из .env или дефолт
|
||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||
|
||
# Учётные данные для разных ролей — берутся из .env
|
||
CREDENTIALS = {
|
||
"vetdept": {
|
||
"login": os.getenv("VETDEPT_LOGIN"),
|
||
"password": os.getenv("VETDEPT_PASSWORD"),
|
||
},
|
||
"vetstation": {
|
||
"login": os.getenv("VETSTATION_LOGIN"),
|
||
"password": os.getenv("VETSTATION_PASSWORD"),
|
||
},
|
||
"prefecture": {
|
||
"login": os.getenv("PREFECTURE_LOGIN"),
|
||
"password": os.getenv("PREFECTURE_PASSWORD"),
|
||
},
|
||
}
|
||
|
||
|
||
def _get_role(request) -> str:
|
||
"""Определяет роль теста по pytest-маркеру."""
|
||
for marker in request.node.iter_markers():
|
||
if marker.name in CREDENTIALS:
|
||
return marker.name
|
||
pytest.fail("ОШИБКА: У теста нет маркера роли (например, @pytest.mark.vetdept)!")
|
||
|
||
|
||
def _login(page: Page, role: str) -> None:
|
||
"""Выполняет авторизацию на портале под указанной ролью."""
|
||
creds = CREDENTIALS[role]
|
||
if not creds["login"] or not creds["password"]:
|
||
pytest.fail(
|
||
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
|
||
f"Скопируй .env.example в .env и заполни данные."
|
||
)
|
||
print(f"\n[АВТОЛОГИН] Роль: {role} | URL: {BASE_URL}")
|
||
page.goto(f"{BASE_URL}/auth")
|
||
page.wait_for_load_state("networkidle")
|
||
page.get_by_role("textbox", name="Email").fill(creds["login"])
|
||
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
|
||
page.get_by_role("button", name="Войти в аккаунт").click()
|
||
|
||
# Ожидаем успешного входа: переход с /auth в личный кабинет (/dashboard и т.д.)
|
||
page.wait_for_url(lambda url: "/auth" not in url, timeout=15000)
|
||
page.wait_for_load_state("networkidle")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Фикстура: браузер с авто-адаптацией под тип TLS (session scope)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture(scope="session")
|
||
def _auto_browser(playwright: Playwright, browser_type_launch_args: dict, pytestconfig):
|
||
"""
|
||
Сессионный браузер, адаптированный под тип TLS целевого портала.
|
||
Применяет аргументы запуска из pytest.ini и CLI (--headed, --slowmo, --tls-mode).
|
||
"""
|
||
# Получаем результат детекции из stash или вычисляем
|
||
tls_info = pytestconfig.stash.get(tls_info_key, None)
|
||
if tls_info is None:
|
||
cli_tls_mode = pytestconfig.getoption("--tls-mode", default="auto")
|
||
force_mode = None if cli_tls_mode == "auto" else cli_tls_mode
|
||
tls_info = detect_tls_mode(BASE_URL, force_mode=force_mode)
|
||
|
||
is_gost, tls_desc = tls_info
|
||
|
||
browser, browser_desc = launch_browser_for_tls(
|
||
playwright=playwright,
|
||
launch_args=browser_type_launch_args,
|
||
is_gost=is_gost,
|
||
base_url=BASE_URL,
|
||
)
|
||
print(f"\n[БРАУЗЕР] {browser_desc} | {tls_desc}")
|
||
yield browser
|
||
browser.close()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Фикстура для бизнес-процессов (scope=class)
|
||
# Один браузер на весь класс — авторизация выполняется один раз.
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture(scope="class")
|
||
def browser_page(_auto_browser, request, pytestconfig):
|
||
"""
|
||
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
|
||
Все шаги внутри класса работают в одной вкладке браузера.
|
||
|
||
Браузер выбирается автоматически:
|
||
- Стандартный TLS -> встроенный Chromium
|
||
- ГОСТ TLS -> Chromium GOST (exe или CDP)
|
||
"""
|
||
role = _get_role(request)
|
||
ignore_https = pytestconfig.getoption("--ignore-https-errors", default=True)
|
||
context = _auto_browser.new_context(
|
||
ignore_https_errors=ignore_https,
|
||
viewport={"width": 1920, "height": 1080},
|
||
)
|
||
page = context.new_page()
|
||
_login(page, role)
|
||
yield page
|
||
context.close()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Контейнер для передачи данных между шагами одного БП (scope=class)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture(scope="class")
|
||
def bp_state() -> dict:
|
||
"""
|
||
Словарь для передачи данных между шагами одного бизнес-процесса.
|
||
Создаётся при старте класса, уничтожается после его завершения.
|
||
|
||
Пример:
|
||
def test_s01_create(self, browser_page, bp_state):
|
||
bp_state["claim_id"] = "12345"
|
||
|
||
def test_s02_verify(self, browser_page, bp_state):
|
||
assert bp_state["claim_id"] == "12345"
|
||
"""
|
||
return {}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Устаревшая фикстура — оставлена для обратной совместимости
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture(scope="function")
|
||
def role_page(_auto_browser, request, pytestconfig):
|
||
"""
|
||
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
|
||
Для новых тестов используй browser_page (scope=class).
|
||
"""
|
||
role = _get_role(request)
|
||
ignore_https = pytestconfig.getoption("--ignore-https-errors", default=True)
|
||
context = _auto_browser.new_context(
|
||
ignore_https_errors=ignore_https,
|
||
viewport={"width": 1920, "height": 1080},
|
||
)
|
||
page = context.new_page()
|
||
_login(page, role)
|
||
yield page
|
||
context.close()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Хук для отображения читаемых имён шагов в pytest-html отчёте
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.hookimpl(hookwrapper=True)
|
||
def pytest_runtest_makereport(item, call):
|
||
"""
|
||
Заменяет технический путь к тесту в колонке Test отчёта pytest-html
|
||
на docstring метода (например, 'Шаг 1: Открыть главную страницу').
|
||
|
||
Если docstring отсутствует — оставляет оригинальный nodeid.
|
||
"""
|
||
outcome = yield
|
||
report = outcome.get_result()
|
||
|
||
# Берём docstring тестовой функции
|
||
doc = getattr(item.function, "__doc__", None)
|
||
if doc:
|
||
description = doc.strip().splitlines()[0] # только первая строка
|
||
report.nodeid = description |