Compare commits
7 Commits
d156fa0664
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 63b829471d | |||
| 63194d1824 | |||
| 21109221bc | |||
| fdafff1e3f | |||
| 162445481b | |||
| cdca9d6003 | |||
| f511102289 |
28
.env.example
28
.env.example
@ -17,3 +17,31 @@ PREFECTURE_PASSWORD=
|
|||||||
|
|
||||||
# Базовый URL портала
|
# Базовый URL портала
|
||||||
BASE_URL=https://vet.goznak.ru
|
BASE_URL=https://vet.goznak.ru
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Режим TLS и браузер (проверяется автоматически при старте тестов)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Режим TLS: auto (по умолчанию), gost (принудительно ГОСТ TLS), standard (обычный TLS)
|
||||||
|
# Также можно передать флаг в pytest: pytest --tls-mode=gost
|
||||||
|
TLS_MODE=auto
|
||||||
|
|
||||||
|
# Вариант 1 (автозапуск): путь к исполняемому файлу Chromium GOST / Яндекс.Браузер
|
||||||
|
# ОПЦИОНАЛЬНО: если браузер установлен в стандартный каталог (Program Files, AppData),
|
||||||
|
# он будет найден автоматически без заполнения этой строки.
|
||||||
|
# Скачать Chromium GOST: https://github.com/deemru/chromium-gost/releases
|
||||||
|
CHROMIUM_GOST_PATH=
|
||||||
|
|
||||||
|
# Вариант 2: порт CDP для подключения к предзапущенному Chromium GOST
|
||||||
|
# Запуск: chrome.exe --remote-debugging-port=9222 --no-first-run
|
||||||
|
CHROMIUM_GOST_CDP_PORT=9222
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Кастомизация отчёта
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Название среды тестирования (отображается в блоке Environment отчёта)
|
||||||
|
TEST_ENV=Production
|
||||||
|
|
||||||
|
# Имя тестировщика (отображается в блоке Environment отчёта)
|
||||||
|
TESTER_NAME=QA Team
|
||||||
|
|||||||
177
README.md
177
README.md
@ -10,9 +10,11 @@
|
|||||||
- [Структура проекта](#структура-проекта)
|
- [Структура проекта](#структура-проекта)
|
||||||
- [Быстрый старт](#быстрый-старт)
|
- [Быстрый старт](#быстрый-старт)
|
||||||
- [Запуск тестов](#запуск-тестов)
|
- [Запуск тестов](#запуск-тестов)
|
||||||
|
- [Поддержка Стандартного и ГОСТ TLS](#поддержка-стандартного-и-гост-tls)
|
||||||
- [Создание нового теста](#создание-нового-теста)
|
- [Создание нового теста](#создание-нового-теста)
|
||||||
- [Добавление генератора данных (ФЛК)](#добавление-генератора-данных-флк)
|
- [Добавление генератора данных (ФЛК)](#добавление-генератора-данных-флк)
|
||||||
- [Добавление статического артефакта](#добавление-статического-артефакта)
|
- [Добавление статического артефакта](#добавление-статического-артефакта)
|
||||||
|
- [Создание теста через визуальный рекордер (Codegen)](#создание-теста-через-визуальный-рекордер-codegen)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -20,15 +22,19 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
e2e_Pets/
|
e2e_Pets/
|
||||||
├── .env # Учётные данные (не в git)
|
├── .env # Учётные данные и настройки среды (не в git)
|
||||||
├── .env.example # Шаблон .env
|
├── .env.example # Шаблон .env
|
||||||
|
├── conftest.py # Корневой conftest (CLI-флаги, HTML-отчёт и метаданные)
|
||||||
├── pytest.ini # Конфигурация pytest
|
├── pytest.ini # Конфигурация pytest
|
||||||
├── requirements.txt # Зависимости
|
├── requirements.txt # Зависимости
|
||||||
│
|
│
|
||||||
└── tests/
|
└── tests/
|
||||||
├── conftest.py # Глобальные фикстуры (browser_page, bp_state)
|
├── conftest.py # Глобальные фикстуры (browser_page, bp_state, _auto_browser)
|
||||||
├── testdata/
|
├── testdata/
|
||||||
│ └── generators.py # Генераторы случайных данных по ФЛК
|
│ └── generators.py # Генераторы случайных данных по ФЛК
|
||||||
|
├── utils/
|
||||||
|
│ ├── tls.py # Детектор TLS и адаптивный менеджер браузеров
|
||||||
|
│ └── test_tls.py # Модульные тесты механизмов TLS
|
||||||
│
|
│
|
||||||
├── 01_vetdept/ # ЛК: Управление ветеринарии
|
├── 01_vetdept/ # ЛК: Управление ветеринарии
|
||||||
│ ├── conftest.py # fixtures_dir, shared_state
|
│ ├── conftest.py # fixtures_dir, shared_state
|
||||||
@ -64,90 +70,160 @@ python -m venv venv
|
|||||||
.\venv\Scripts\playwright.exe install chromium
|
.\venv\Scripts\playwright.exe install chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Настроить учётные данные
|
### 3. Настроить конфигурацию и учётные данные
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Copy-Item .env.example .env
|
Copy-Item .env.example .env
|
||||||
# Открыть .env и заполнить логины/пароли
|
# Открыть .env и указать BASE_URL, логины и пароли ролей
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4. (Опционально) Настройка ГОСТ-браузера
|
||||||
|
|
||||||
|
Если целевой портал защищён ГОСТ TLS (ГОСТ Р 34.12-2015, 34.10-2012) или вы хотите тестировать ГОСТ-контур:
|
||||||
|
1. Скачайте архив **[Chromium-GOST](https://github.com/deemru/chromium-gost/releases)** (например, `chromium-gost-...-windows-amd64.zip`).
|
||||||
|
2. Распакуйте его в стандартный каталог `%LOCALAPPDATA%\Chromium-Gost` (или `C:\Program Files\Chromium-Gost`). Механизм тестов обнаружит его **автоматически**.
|
||||||
|
3. *Альтернатива:* если браузер распакован в произвольную папку, укажите путь в `.env`:
|
||||||
|
```ini
|
||||||
|
CHROMIUM_GOST_PATH=C:\MyPrograms\chromium-gost\chrome.exe
|
||||||
|
```
|
||||||
|
4. Убедитесь, что на компьютере установлен криптопровайдер с ГОСТ-алгоритмами (КриптоПро CSP, Security Code CSP / Континент-АП).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Запуск тестов
|
## Запуск тестов
|
||||||
|
|
||||||
> Все команды выполняются из корня проекта (`e2e_Pets/`).
|
> Все команды выполняются из корня проекта (`e2e_Pets/`).
|
||||||
> HTML-отчёт автоматически создаётся в `html-report/report.html`.
|
> HTML-отчёт со статусом, типом TLS и запущенным браузером автоматически формируется в `html-report/report.html`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Запустить ВСЕ тесты
|
### Автоматический запуск (рекомендуемый)
|
||||||
|
|
||||||
|
По умолчанию включён режим `--tls-mode=auto`. Система перед стартом проверяет сокет целевого `BASE_URL`:
|
||||||
|
- Если сайт доступен по **стандартному TLS** → автоматически запускается встроенный **Chromium Playwright**.
|
||||||
|
- Если сайт защищён **ГОСТ TLS** → автоматически запускается **Chromium-GOST**.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
# Запуск всех тестов в авто-режиме
|
||||||
.\venv\Scripts\pytest.exe
|
.\venv\Scripts\pytest.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Запустить тесты конкретного ЛК (роли)
|
### Принудительный выбор режима TLS
|
||||||
|
|
||||||
|
Если вы хотите явно зафиксировать браузер в обход автоопределения:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# ЛК Управление ветеринарии
|
# Принудительно запустить через Chromium-GOST
|
||||||
|
.\venv\Scripts\pytest.exe --tls-mode=gost
|
||||||
|
|
||||||
|
# Принудительно запустить через стандартный Chromium Playwright
|
||||||
|
.\venv\Scripts\pytest.exe --tls-mode=standard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Запуск тестов конкретного ЛК (роли)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# ЛК Управление ветеринарии (все бизнес-процессы)
|
||||||
.\venv\Scripts\pytest.exe tests/01_vetdept/
|
.\venv\Scripts\pytest.exe tests/01_vetdept/
|
||||||
|
|
||||||
# ЛК Ветеринарная станция
|
# ЛК Ветеринарная станция (все бизнес-процессы)
|
||||||
.\venv\Scripts\pytest.exe tests/02_vetstation/
|
.\venv\Scripts\pytest.exe tests/02_vetstation/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Запустить конкретный бизнес-процесс
|
### Запуск конкретного бизнес-процесса или шага
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# BP01 навигации в ЛК Управление ветеринарии
|
# Запуск BP01 в ЛК Управление ветеринарии
|
||||||
.\venv\Scripts\pytest.exe tests/01_vetdept/BP01_navigation/
|
.\venv\Scripts\pytest.exe tests/01_vetdept/BP01_navigation/
|
||||||
|
|
||||||
# BP01 дашборда в ЛК Ветеринарная станция
|
# Запуск только шага 1 (Dashboard)
|
||||||
.\venv\Scripts\pytest.exe tests/02_vetstation/BP01_dashboard/
|
.\venv\Scripts\pytest.exe tests/01_vetdept/BP01_navigation/ -k "s01"
|
||||||
|
|
||||||
|
# Запуск шагов 1 и 2
|
||||||
|
.\venv\Scripts\pytest.exe tests/01_vetdept/BP01_navigation/ -k "s01 or s02"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Запустить конкретный шаг
|
### Полезные флаги запуска
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Только шаг 3 из BP01 ветстанции
|
|
||||||
.\venv\Scripts\pytest.exe tests/02_vetstation/BP01_dashboard/ -k "s03"
|
|
||||||
|
|
||||||
# Только шаги 1 и 2
|
|
||||||
.\venv\Scripts\pytest.exe tests/02_vetstation/BP01_dashboard/ -k "s01 or s02"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Полезные флаги
|
|
||||||
|
|
||||||
| Флаг | Описание |
|
| Флаг | Описание |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-v` | Подробный вывод с именами шагов |
|
| `-v` | Подробный вывод с отображением docstring каждого шага |
|
||||||
| `--headed` | Показывать браузер (по умолчанию включён в `pytest.ini`) |
|
| `--tls-mode=auto` | Режим TLS: `auto` (по умолчанию), `gost` или `standard` |
|
||||||
| `--headless` | Запустить без браузера (быстрее) |
|
| `--ignore-https-errors` | Игнорировать ошибки сертификатов SSL (по умолчанию включено) |
|
||||||
| `--slowmo 500` | Замедление действий на 500мс (для отладки) |
|
| `-k "BP01"` | Фильтр тестов по подстроке в имени класса или метода |
|
||||||
| `-k "BP01"` | Запустить только тесты содержащие "BP01" в имени |
|
| `--co -q` | Только показать список тестов без их выполнения |
|
||||||
| `--co -q` | Только показать список тестов без запуска |
|
| `--slowmo 500` | Замедление действий в браузере (в мс, по умолчанию 1000мс в `pytest.ini`) |
|
||||||
|
| `-o addopts=""` | Сбросить дефолтные флаги `pytest.ini` (например, для headless-запуска в CI) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Показать список всех тестов (без запуска)
|
# Просмотр дерева тестов без запуска браузера
|
||||||
.\venv\Scripts\pytest.exe --co -q
|
.\venv\Scripts\pytest.exe --co -q
|
||||||
|
|
||||||
# Запустить без браузера и без замедления
|
# Быстрый запуск без задержки slowmo
|
||||||
.\venv\Scripts\pytest.exe --headless --slowmo 0
|
.\venv\Scripts\pytest.exe tests/01_vetdept/ --slowmo 0
|
||||||
|
|
||||||
# Запустить в headless-режиме только vetdept
|
# Запуск в режиме ГОСТ с подробным выводом
|
||||||
.\venv\Scripts\pytest.exe tests/01_vetdept/ --headless --slowmo 0
|
.\venv\Scripts\pytest.exe tests/01_vetdept/ --tls-mode=gost -v
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Поддержка Стандартного и ГОСТ TLS
|
||||||
|
|
||||||
|
В проект встроен универсальный механизм запуска, который автоматически определяет, какой протокол шифрования используется на целевом портале (`BASE_URL`), и выбирает подходящий браузер.
|
||||||
|
|
||||||
|
### Как работает автоопределение:
|
||||||
|
1. **Двухфазная детекция**:
|
||||||
|
- Выполняется тестовое SSL-рукопожатие сокета.
|
||||||
|
- Если сайт использует стандартные алгоритмы (TLS 1.2 / TLS 1.3, AES, CHACHA20 и др.), даже при наличии самоподписанного или тестового корпоративного сертификата, система распознаёт его как **Стандартный TLS** и запускает встроенный **Chromium Playwright**.
|
||||||
|
- Если сервер требует российские криптографические алгоритмы ГОСТ (ГОСТ Р 34.12-2015 «Кузнечик»/«Магма», ГОСТ Р 34.10-2012, 28147-89), которые не поддерживаются штатным OpenSSL в Python, система идентифицирует **ГОСТ TLS**.
|
||||||
|
|
||||||
|
2. **Автопоиск браузера для ГОСТ**:
|
||||||
|
- Система автоматически сканирует стандартные каталоги установки:
|
||||||
|
- `%LOCALAPPDATA%\Chromium-Gost\Application\chrome.exe`
|
||||||
|
- `%PROGRAMFILES%\Chromium-Gost\chrome.exe` (и в `Program Files (x86)`)
|
||||||
|
- **Яндекс.Браузер** (при установленном КриптоПро CSP): `%LOCALAPPDATA%\Yandex\...`, `%PROGRAMFILES%\Yandex\...`
|
||||||
|
- Системный `PATH` (`chromium-gost`, `yandex-browser`).
|
||||||
|
- Если браузер найден в системе, тесты стартуют автоматически без необходимости вручную прописывать путь в `.env`.
|
||||||
|
|
||||||
|
3. **Варианты запуска под ГОСТ**:
|
||||||
|
- **Автозапуск (рекомендуется)**: Установить [Chromium-GOST](https://github.com/deemru/chromium-gost/releases) или Яндекс.Браузер с КриптоПро CSP. При нестандартном расположении указать в `.env`:
|
||||||
|
```ini
|
||||||
|
CHROMIUM_GOST_PATH=C:\CustomPath\chrome.exe
|
||||||
|
```
|
||||||
|
- **Подключение по CDP (если браузер уже запущен вручную)**:
|
||||||
|
```powershell
|
||||||
|
chrome.exe --remote-debugging-port=9222 --no-first-run
|
||||||
|
```
|
||||||
|
Тесты автоматически подключатся к открытому браузеру на порту `9222`.
|
||||||
|
|
||||||
|
4. **Управление режимом TLS**:
|
||||||
|
- Через аргумент CLI:
|
||||||
|
```powershell
|
||||||
|
.\venv\Scripts\pytest.exe --tls-mode=auto # Автоопределение (по умолчанию)
|
||||||
|
.\venv\Scripts\pytest.exe --tls-mode=gost # Принудительно ГОСТ
|
||||||
|
.\venv\Scripts\pytest.exe --tls-mode=standard # Принудительно Стандартный
|
||||||
|
```
|
||||||
|
- Либо через `.env`:
|
||||||
|
```ini
|
||||||
|
TLS_MODE=auto # auto | gost | standard
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Отображение в HTML-отчёте**:
|
||||||
|
- В блоке **Environment** сгенерированного отчёта `html-report/report.html` фиксируются:
|
||||||
|
- Реальный режим TLS и использованный шифр (например, `Стандартный TLS (TLSv1.3, TLS_AES_256_GCM_SHA384)`).
|
||||||
|
- Тип и путь фактически запущенного браузера (`Chromium (Playwright)` / `Chromium GOST` / `Яндекс.Браузер`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Создание нового теста
|
## Создание нового теста
|
||||||
|
|
||||||
### Нужен новый бизнес-процесс?
|
### Нужен новый бизнес-процесс?
|
||||||
@ -296,17 +372,32 @@ Python-код, который воспроизводит твои действи
|
|||||||
|
|
||||||
### Запуск рекордера
|
### Запуск рекордера
|
||||||
|
|
||||||
|
#### Вариант 1: Универсальный запуск с поддержкой ГОСТ TLS (рекомендуемый)
|
||||||
|
Штатная команда `playwright codegen` не умеет работать со сторонними браузерами напрямую. Для записи тестов на сайтах с **ГОСТ TLS** (а также обычным TLS) используйте встроенный скрипт `codegen.py`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Открыть браузер с рекордером (без авторизации)
|
# Запуск для URL по умолчанию из .env (с автоопределением TLS и выбором браузера):
|
||||||
|
.\venv\Scripts\python.exe codegen.py
|
||||||
|
|
||||||
|
# Принудительный запуск в режиме ГОСТ TLS (через Chromium-GOST):
|
||||||
|
.\venv\Scripts\python.exe codegen.py --gost
|
||||||
|
|
||||||
|
# Запуск для произвольного адреса:
|
||||||
|
.\venv\Scripts\python.exe codegen.py https://vet.goznak.ru
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Вариант 2: Штатный запуск (только для сайтов со стандартным TLS)
|
||||||
|
```powershell
|
||||||
|
# Открыть встроенный Chromium с рекордером
|
||||||
.\venv\Scripts\playwright.exe codegen https://vet.goznak.ru
|
.\venv\Scripts\playwright.exe codegen https://vet.goznak.ru
|
||||||
|
|
||||||
# Открыть браузер с рекордером и сохранить результат в файл
|
# Открыть рекордер и сразу сохранить результат в файл
|
||||||
.\venv\Scripts\playwright.exe codegen https://vet.goznak.ru --output tests\codegen_draft.py
|
.\venv\Scripts\playwright.exe codegen https://vet.goznak.ru --output tests\codegen_draft.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Откроются два окна:
|
При запуске откроются два окна:
|
||||||
- **Браузер** — управляй им как обычно: кликай, заполняй формы, переходи по разделам
|
- **Браузер (Chromium-GOST или Playwright Chromium)** — кликайте, заполняйте формы и переходите по разделам.
|
||||||
- **Playwright Inspector** — в реальном времени показывает сгенерированный код
|
- **Playwright Inspector** — окно записи, в котором в реальном времени формируется Python-код.
|
||||||
|
|
||||||
### Порядок работы
|
### Порядок работы
|
||||||
|
|
||||||
|
|||||||
125
codegen.py
Normal file
125
codegen.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Инструмент запуска визуального рекордера Playwright Codegen
|
||||||
|
с автоматической поддержкой Стандартного и ГОСТ TLS.
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
.\\venv\\Scripts\\python.exe codegen.py [URL] [опции]
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
# Автоопределение TLS для URL из .env (по умолчанию):
|
||||||
|
.\\venv\\Scripts\\python.exe codegen.py
|
||||||
|
|
||||||
|
# Запуск для конкретного URL:
|
||||||
|
.\\venv\\Scripts\\python.exe codegen.py https://vet.goznak.ru
|
||||||
|
|
||||||
|
# Принудительно через Chromium-GOST:
|
||||||
|
.\\venv\\Scripts\\python.exe codegen.py --gost
|
||||||
|
|
||||||
|
# Принудительно через стандартный Chromium:
|
||||||
|
.\\venv\\Scripts\\python.exe codegen.py --standard
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
from tests.utils.tls import detect_tls_mode, find_gost_browser_executable
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
DEFAULT_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Визуальный рекордер Playwright с поддержкой Chromium-GOST и стандартного Chromium."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"url",
|
||||||
|
nargs="?",
|
||||||
|
default=DEFAULT_URL,
|
||||||
|
help=f"Целевой URL портала (по умолчанию из .env: {DEFAULT_URL})",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--gost",
|
||||||
|
action="store_true",
|
||||||
|
help="Принудительно запустить рекордер через Chromium-GOST",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--standard",
|
||||||
|
action="store_true",
|
||||||
|
help="Принудительно запустить рекордер через встроенный Chromium Playwright",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Если передан URL без схемы (например, paws.vet.goznak.ru) — добавляем https://
|
||||||
|
url = args.url
|
||||||
|
if not url.startswith("http://") and not url.startswith("https://"):
|
||||||
|
url = f"https://{url}"
|
||||||
|
|
||||||
|
# Определение режима TLS
|
||||||
|
if args.gost:
|
||||||
|
is_gost = True
|
||||||
|
tls_desc = "Принудительно (ГОСТ TLS)"
|
||||||
|
elif args.standard:
|
||||||
|
is_gost = False
|
||||||
|
tls_desc = "Принудительно (Стандартный TLS)"
|
||||||
|
else:
|
||||||
|
is_gost, tls_desc = detect_tls_mode(url)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("ЗАПУСК ВИЗУАЛЬНОГО РЕКОРДЕРА PLAYWRIGHT")
|
||||||
|
print(f"Целевой URL: {url}")
|
||||||
|
print(f"Режим TLS: {tls_desc}")
|
||||||
|
|
||||||
|
# Включаем режим отладчика/инспектора Playwright
|
||||||
|
os.environ["PWDEBUG"] = "1"
|
||||||
|
|
||||||
|
with sync_playwright() as playwright:
|
||||||
|
if is_gost:
|
||||||
|
gost_path = find_gost_browser_executable()
|
||||||
|
if not gost_path or not os.path.exists(gost_path):
|
||||||
|
print("\n[ОШИБКА] Chromium-GOST не найден в системе!")
|
||||||
|
print("Скачайте его с https://github.com/deemru/chromium-gost/releases")
|
||||||
|
print("и распакуйте в %LOCALAPPDATA%\\Chromium-Gost,")
|
||||||
|
print("либо укажите путь в .env: CHROMIUM_GOST_PATH=...")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"Браузер: Chromium GOST22 ({gost_path})")
|
||||||
|
browser = playwright.chromium.launch(
|
||||||
|
executable_path=gost_path,
|
||||||
|
headless=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Браузер: Chromium (Playwright)")
|
||||||
|
browser = playwright.chromium.launch(headless=False)
|
||||||
|
|
||||||
|
context = browser.new_context(
|
||||||
|
ignore_https_errors=True,
|
||||||
|
viewport={"width": 1920, "height": 1080},
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print("1. Открылись окна: Браузер и Playwright Inspector.")
|
||||||
|
print("2. В окне инспектора нажмите кнопку записи (красный кружок 'Record').")
|
||||||
|
print("3. Кликайте и заполняйте формы — код генерируется в окне инспектора.")
|
||||||
|
print("4. Для завершения просто закройте окно браузера.")
|
||||||
|
print(f"{'=' * 70}\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
page.goto(url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Переход на {url}: {e}")
|
||||||
|
|
||||||
|
# page.pause() активирует панель рекордера/инспектора
|
||||||
|
page.pause()
|
||||||
|
|
||||||
|
context.close()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
91
conftest.py
Normal file
91
conftest.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Корневой conftest.py — конфигурация на уровне всей сессии.
|
||||||
|
Здесь размещены хуки, которые должны работать глобально.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
import pytest
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from tests.utils.tls import detect_tls_mode, tls_info_key, find_gost_browser_executable
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Регистрация CLI аргументов запуска
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
"""Регистрирует дополнительные флаги командной строки для pytest."""
|
||||||
|
parser.addoption(
|
||||||
|
"--tls-mode",
|
||||||
|
action="store",
|
||||||
|
default="auto",
|
||||||
|
choices=["auto", "gost", "standard"],
|
||||||
|
help="Режим TLS: auto (по умолчанию — автоопределение), gost (принудительно ГОСТ TLS), standard (обычный TLS).",
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--ignore-https-errors",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="Игнорировать ошибки SSL сертификатов в браузере (по умолчанию: True).",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Настройка блока Environment и заголовка HTML-отчёта
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def pytest_sessionstart(session):
|
||||||
|
"""
|
||||||
|
Заменяет технические данные в блоке Environment отчёта на значимые.
|
||||||
|
|
||||||
|
pytest-html 4.x читает данные через config.stash[metadata_key]
|
||||||
|
(API pytest-metadata 3.x). Наш хук изменяет именно этот stash.
|
||||||
|
"""
|
||||||
|
config = session.config
|
||||||
|
|
||||||
|
# Получаем режим TLS из CLI аргумента или .env
|
||||||
|
cli_tls_mode = config.getoption("--tls-mode", default="auto")
|
||||||
|
force_mode = None if cli_tls_mode == "auto" else cli_tls_mode
|
||||||
|
|
||||||
|
# Определяем режим TLS с кэшированием и сохраняем в stash сессии
|
||||||
|
is_gost, tls_desc = detect_tls_mode(BASE_URL, force_mode=force_mode)
|
||||||
|
config.stash[tls_info_key] = (is_gost, tls_desc)
|
||||||
|
|
||||||
|
if is_gost:
|
||||||
|
gost_exec = find_gost_browser_executable()
|
||||||
|
if gost_exec and "yandex" in gost_exec.lower():
|
||||||
|
browser_name = "Яндекс.Браузер (ГОСТ)"
|
||||||
|
else:
|
||||||
|
browser_name = "Chromium GOST"
|
||||||
|
else:
|
||||||
|
browser_name = "Chromium (Playwright)"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from pytest_metadata.plugin import metadata_key
|
||||||
|
metadata = config.stash[metadata_key]
|
||||||
|
except (ImportError, KeyError):
|
||||||
|
metadata = getattr(config, "_metadata", None)
|
||||||
|
if metadata is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Сбрасываем технические данные от pytest-metadata
|
||||||
|
metadata.clear()
|
||||||
|
|
||||||
|
# Заполняем значимыми данными для отчёта руководителю
|
||||||
|
metadata["Проект"] = "e2e_Pets — Учёт домашних животных"
|
||||||
|
metadata["Портал"] = BASE_URL
|
||||||
|
metadata["TLS Режим"] = tls_desc
|
||||||
|
metadata["Браузер"] = browser_name
|
||||||
|
metadata["Окружение"] = os.getenv("TEST_ENV", "Production")
|
||||||
|
metadata["Тестировщик"] = os.getenv("TESTER_NAME", "QA Team")
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_html_report_title(report):
|
||||||
|
"""Задаёт заголовок HTML-отчёта."""
|
||||||
|
report.title = "Отчёт E2E тестирования — Портал учёта животных"
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
import pytest
|
import pytest
|
||||||
# pyrefly: ignore [missing-import]
|
# pyrefly: ignore [missing-import]
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import pytest
|
|||||||
# pyrefly: ignore [missing-import]
|
# pyrefly: ignore [missing-import]
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
# pyrefly: ignore [missing-import]
|
# pyrefly: ignore [missing-import]
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page, Playwright
|
||||||
|
|
||||||
# Загружаем переменные из .env (файл находится в корне проекта)
|
from tests.utils.tls import detect_tls_mode, launch_browser_for_tls, tls_info_key
|
||||||
|
|
||||||
|
# Загружаем переменные из .env
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Базовый URL портала — берётся из .env или дефолт
|
# Базовый URL портала — берётся из .env или дефолт
|
||||||
@ -45,29 +47,69 @@ def _login(page: Page, role: str) -> None:
|
|||||||
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
|
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
|
||||||
f"Скопируй .env.example в .env и заполни данные."
|
f"Скопируй .env.example в .env и заполни данные."
|
||||||
)
|
)
|
||||||
print(f"\n[АВТОЛОГИН] Вход под ролью: {role}")
|
print(f"\n[АВТОЛОГИН] Роль: {role} | URL: {BASE_URL}")
|
||||||
page.goto(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="Email").fill(creds["login"])
|
||||||
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
|
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
|
||||||
page.get_by_role("button", name="Войти в аккаунт").click()
|
page.get_by_role("button", name="Войти в аккаунт").click()
|
||||||
page.wait_for_selector("#app", timeout=10000)
|
|
||||||
|
# Ожидаем успешного входа: переход с /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)
|
# Фикстура для бизнес-процессов (scope=class)
|
||||||
# Один браузер на весь класс — авторизация выполняется один раз.
|
# Один браузер на весь класс — авторизация выполняется один раз.
|
||||||
# Создаёт собственный context и page, чтобы избежать ScopeMismatch
|
|
||||||
# с function-scoped page фикстурой pytest-playwright.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def browser_page(browser, request):
|
def browser_page(_auto_browser, request, pytestconfig):
|
||||||
"""
|
"""
|
||||||
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
|
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
|
||||||
Все шаги внутри класса работают в одной вкладке браузера.
|
Все шаги внутри класса работают в одной вкладке браузера.
|
||||||
|
|
||||||
|
Браузер выбирается автоматически:
|
||||||
|
- Стандартный TLS -> встроенный Chromium
|
||||||
|
- ГОСТ TLS -> Chromium GOST (exe или CDP)
|
||||||
"""
|
"""
|
||||||
role = _get_role(request)
|
role = _get_role(request)
|
||||||
context = browser.new_context()
|
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()
|
page = context.new_page()
|
||||||
_login(page, role)
|
_login(page, role)
|
||||||
yield page
|
yield page
|
||||||
@ -96,19 +138,24 @@ def bp_state() -> dict:
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Устаревшая фикстура — оставлена для обратной совместимости
|
# Устаревшая фикстура — оставлена для обратной совместимости
|
||||||
# Используй browser_page для новых тестов
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def role_page(page: Page, request):
|
def role_page(_auto_browser, request, pytestconfig):
|
||||||
"""
|
"""
|
||||||
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
|
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
|
||||||
Для новых тестов используй browser_page (scope=class).
|
Для новых тестов используй browser_page (scope=class).
|
||||||
"""
|
"""
|
||||||
role = _get_role(request)
|
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)
|
_login(page, role)
|
||||||
yield page
|
yield page
|
||||||
page.context.clear_cookies()
|
context.close()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
3
tests/utils/__init__.py
Normal file
3
tests/utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .tls import detect_tls_mode, launch_browser_for_tls
|
||||||
|
|
||||||
|
__all__ = ["detect_tls_mode", "launch_browser_for_tls"]
|
||||||
82
tests/utils/test_tls.py
Normal file
82
tests/utils/test_tls.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Тесты модуля tests/utils/tls.py:
|
||||||
|
Проверка двухфазной детекции TLS, кэширования и автопоиска ГОСТ-браузера.
|
||||||
|
"""
|
||||||
|
import ssl
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import pytest
|
||||||
|
from tests.utils.tls import detect_tls_mode, find_gost_browser_executable, _probe_tls_socket
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_tls_mode_http():
|
||||||
|
is_gost, desc = detect_tls_mode("http://example.com")
|
||||||
|
assert is_gost is False
|
||||||
|
assert "HTTP" in desc
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_tls_mode_forced_gost():
|
||||||
|
is_gost, desc = detect_tls_mode("https://any-site.com", force_mode="gost")
|
||||||
|
assert is_gost is True
|
||||||
|
assert "ГОСТ" in desc
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_tls_mode_forced_standard():
|
||||||
|
is_gost, desc = detect_tls_mode("https://any-site.com", force_mode="standard")
|
||||||
|
assert is_gost is False
|
||||||
|
assert "Стандартный" in desc
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_tls_mode_real_goznak():
|
||||||
|
# Реальный вызов к тестовому порталу
|
||||||
|
is_gost, desc = detect_tls_mode("https://vet.goznak.ru")
|
||||||
|
assert is_gost is False
|
||||||
|
assert "TLS" in desc
|
||||||
|
|
||||||
|
|
||||||
|
def test_probe_tls_untrusted_standard_cert():
|
||||||
|
"""
|
||||||
|
Проверяет, что если сайт на стандартном TLS имеет недоверенный/самоподписанный сертификат,
|
||||||
|
он НЕ определяется ошибочно как ГОСТ.
|
||||||
|
"""
|
||||||
|
with patch("ssl.create_default_context") as mock_def_ctx, \
|
||||||
|
patch("ssl._create_unverified_context") as mock_unv_ctx, \
|
||||||
|
patch("socket.socket") as mock_socket:
|
||||||
|
|
||||||
|
# Первая попытка с проверкой сертификата падает с SSLCertVerificationError
|
||||||
|
mock_sock_inst = MagicMock()
|
||||||
|
mock_def_ctx.return_value.wrap_socket.side_effect = ssl.SSLCertVerificationError("certificate verify failed")
|
||||||
|
|
||||||
|
# Вторая попытка с CERT_NONE успешна
|
||||||
|
mock_unv_sock = MagicMock()
|
||||||
|
mock_unv_sock.version.return_value = "TLSv1.3"
|
||||||
|
mock_unv_sock.cipher.return_value = ("TLS_AES_256_GCM_SHA384", "TLSv1.3", 256)
|
||||||
|
mock_unv_ctx.return_value.wrap_socket.return_value.__enter__.return_value = mock_unv_sock
|
||||||
|
|
||||||
|
is_gost, desc = _probe_tls_socket("self-signed.example.com", 443)
|
||||||
|
assert is_gost is False
|
||||||
|
assert "недоверенный сертификат" in desc
|
||||||
|
|
||||||
|
|
||||||
|
def test_probe_tls_real_gost_cipher_error():
|
||||||
|
"""
|
||||||
|
Проверяет, что если handshake падает с ошибкой шифров (несовместимый протокол/шифр ГОСТ),
|
||||||
|
он определяется как ГОСТ TLS.
|
||||||
|
"""
|
||||||
|
with patch("ssl.create_default_context") as mock_def_ctx, \
|
||||||
|
patch("ssl._create_unverified_context") as mock_unv_ctx, \
|
||||||
|
patch("socket.socket") as mock_socket:
|
||||||
|
|
||||||
|
mock_def_ctx.return_value.wrap_socket.side_effect = ssl.SSLError(ssl.SSL_ERROR_SSL, "UNSUPPORTED_PROTOCOL")
|
||||||
|
mock_unv_ctx.return_value.wrap_socket.side_effect = ssl.SSLError(ssl.SSL_ERROR_SSL, "NO_CIPHERS_AVAILABLE")
|
||||||
|
|
||||||
|
is_gost, desc = _probe_tls_socket("gost.example.com", 443)
|
||||||
|
assert is_gost is True
|
||||||
|
assert "ГОСТ TLS" in desc
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_gost_browser_executable():
|
||||||
|
"""Проверяет поиск ГОСТ-браузера."""
|
||||||
|
path = find_gost_browser_executable()
|
||||||
|
# На данной системе должен найти установленный браузер или вернуть None
|
||||||
|
if path:
|
||||||
|
assert ("chrome.exe" in path.lower()) or ("browser.exe" in path.lower())
|
||||||
237
tests/utils/tls.py
Normal file
237
tests/utils/tls.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
"""
|
||||||
|
Утилита для определения типа TLS (стандартный vs ГОСТ TLS)
|
||||||
|
и управления запуском соответствующего браузера.
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
import pytest
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from playwright.sync_api import Browser, Playwright
|
||||||
|
|
||||||
|
# Ключ для сохранения результатов детекции TLS в pytest stash
|
||||||
|
tls_info_key = pytest.StashKey[Tuple[bool, str]]()
|
||||||
|
|
||||||
|
|
||||||
|
def find_gost_browser_executable() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Выполняет автопоиск исполняемого файла Chromium-GOST или Яндекс.Браузера,
|
||||||
|
поддерживающего ГОСТ TLS через системный криптопровайдер (КриптоПро CSP).
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
Абсолютный путь к найденному браузеру или None.
|
||||||
|
"""
|
||||||
|
# 1. Приоритет — путь, явно заданный в .env
|
||||||
|
custom_path = os.getenv("CHROMIUM_GOST_PATH", "").strip()
|
||||||
|
if custom_path and os.path.exists(custom_path):
|
||||||
|
return os.path.abspath(custom_path)
|
||||||
|
|
||||||
|
# 2. Список типовых путей установки в ОС Windows
|
||||||
|
local_app_data = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
prog_files = os.environ.get("ProgramFiles", "")
|
||||||
|
prog_files_x86 = os.environ.get("ProgramFiles(x86)", "")
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
# Пути официального установщика Chromium-GOST (устанавливается в папку Chromium\Application)
|
||||||
|
os.path.join(local_app_data, "Chromium", "Application", "chrome.exe"),
|
||||||
|
os.path.join(local_app_data, "Chromium", "chrome.exe"),
|
||||||
|
os.path.join(local_app_data, "Programs", "Chromium", "Application", "chrome.exe"),
|
||||||
|
os.path.join(prog_files, "Chromium", "Application", "chrome.exe"),
|
||||||
|
os.path.join(prog_files, "Chromium", "chrome.exe"),
|
||||||
|
os.path.join(prog_files_x86, "Chromium", "Application", "chrome.exe"),
|
||||||
|
os.path.join(prog_files_x86, "Chromium", "chrome.exe"),
|
||||||
|
|
||||||
|
# Портативные пути и архивы Chromium-Gost
|
||||||
|
os.path.join(local_app_data, "Chromium-Gost", "chrome.exe"),
|
||||||
|
os.path.join(local_app_data, "Chromium-Gost", "Application", "chrome.exe"),
|
||||||
|
os.path.join(local_app_data, "Programs", "Chromium-Gost", "chrome.exe"),
|
||||||
|
os.path.join(local_app_data, "Programs", "Chromium-Gost", "Application", "chrome.exe"),
|
||||||
|
os.path.join(prog_files, "Chromium-Gost", "chrome.exe"),
|
||||||
|
os.path.join(prog_files, "Chromium-Gost", "Application", "chrome.exe"),
|
||||||
|
os.path.join(prog_files_x86, "Chromium-Gost", "chrome.exe"),
|
||||||
|
os.path.join(prog_files_x86, "Chromium-Gost", "Application", "chrome.exe"),
|
||||||
|
|
||||||
|
# Linux / macOS пути
|
||||||
|
"/opt/chromium-gost/chrome",
|
||||||
|
"/usr/bin/chromium-gost",
|
||||||
|
"/Applications/Chromium-Gost.app/Contents/MacOS/Chromium-Gost",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in candidates:
|
||||||
|
if path and os.path.exists(path):
|
||||||
|
return os.path.abspath(path)
|
||||||
|
|
||||||
|
# 3. Поиск в системном PATH
|
||||||
|
for name in ("chromium-gost", "chrome-gost", "chromium"):
|
||||||
|
found = shutil.which(name)
|
||||||
|
if found:
|
||||||
|
return os.path.abspath(found)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _probe_tls_socket(host: str, port: int) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Выполняет двухфазную проверку TLS сокета:
|
||||||
|
Фаза 1: стандартная проверка с валидацией цепочки сертификатов.
|
||||||
|
Фаза 2: если ошибка связана с недоверенным/тестовым сертификатом,
|
||||||
|
проверяется handshake без валидации сертификата (CERT_NONE).
|
||||||
|
Если он успешен — это стандартный TLS (не ГОСТ!).
|
||||||
|
Если же handshake не может договориться по ciphers/protocols — это ГОСТ TLS.
|
||||||
|
"""
|
||||||
|
# Фаза 1: Обычный контекст с валидацией
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
try:
|
||||||
|
with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
|
||||||
|
s.settimeout(5)
|
||||||
|
s.connect((host, port))
|
||||||
|
cipher = s.cipher()
|
||||||
|
cipher_name = cipher[0] if cipher else "Unknown"
|
||||||
|
return False, f"Стандартный TLS ({s.version()}, {cipher_name})"
|
||||||
|
except ssl.SSLCertVerificationError as e:
|
||||||
|
# Сертификат недоверен / самоподписан, но шифрование может быть стандартным!
|
||||||
|
# Проверяем Фазу 2 с CERT_NONE
|
||||||
|
try:
|
||||||
|
unverified_ctx = ssl._create_unverified_context()
|
||||||
|
with unverified_ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
|
||||||
|
s.settimeout(5)
|
||||||
|
s.connect((host, port))
|
||||||
|
cipher = s.cipher()
|
||||||
|
cipher_name = cipher[0] if cipher else "Unknown"
|
||||||
|
return False, f"Стандартный TLS (недоверенный сертификат: {s.version()}, {cipher_name})"
|
||||||
|
except ssl.SSLError:
|
||||||
|
pass
|
||||||
|
return True, f"ГОСТ TLS (ошибка верификации и шифра: {e})"
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
reason = getattr(e, "reason", str(e))
|
||||||
|
# Пробуем без проверки сертификата на случай, если причина в самоподписанном сертификате
|
||||||
|
try:
|
||||||
|
unverified_ctx = ssl._create_unverified_context()
|
||||||
|
with unverified_ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
|
||||||
|
s.settimeout(5)
|
||||||
|
s.connect((host, port))
|
||||||
|
cipher = s.cipher()
|
||||||
|
cipher_name = cipher[0] if cipher else "Unknown"
|
||||||
|
return False, f"Стандартный TLS (без валидации: {s.version()}, {cipher_name})"
|
||||||
|
except Exception:
|
||||||
|
# Стандартный OpenSSL не понимает ГОСТ ciphers / протокол
|
||||||
|
return True, f"ГОСТ TLS (OpenSSL handshake: {reason})"
|
||||||
|
except Exception as e:
|
||||||
|
# Эвристика: если сервис paws.* недоступен по таймауту/сети, но является целевым ГОСТ-контуром
|
||||||
|
if "paws" in host.lower():
|
||||||
|
return True, f"ГОСТ TLS (домен {host}, fallback: {e})"
|
||||||
|
return False, f"Стандартный TLS (fallback: {e})"
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=32)
|
||||||
|
def detect_tls_mode(url: str, force_mode: Optional[str] = None) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Определяет тип TLS соединения для целевого URL с кэшированием результатов.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
(is_gost: bool, description: str)
|
||||||
|
|
||||||
|
Приоритет:
|
||||||
|
1. Аргумент force_mode (например, переданный из CLI pytest --tls-mode)
|
||||||
|
2. Переменная USE_GOST или TLS_MODE в .env
|
||||||
|
3. Двухфазная проверка через сокет (handshake):
|
||||||
|
- Успешное подключение стандартным OpenSSL -> Стандартный TLS (False)
|
||||||
|
- Ошибка шифрования (UNSUPPORTED_PROTOCOL, NO_CIPHERS_AVAILABLE и т.д.) -> ГОСТ TLS (True)
|
||||||
|
- Домен содержит "paws" при таймауте -> ГОСТ TLS (True)
|
||||||
|
"""
|
||||||
|
# 1. Приоритет — явный force_mode
|
||||||
|
mode = (force_mode or os.getenv("TLS_MODE") or os.getenv("USE_GOST") or "").strip().lower()
|
||||||
|
if mode in ("gost", "true", "1", "yes", "on"):
|
||||||
|
return True, "Принудительно (ГОСТ TLS)"
|
||||||
|
if mode in ("standard", "regular", "false", "0", "no", "off"):
|
||||||
|
return False, "Принудительно (Стандартный TLS)"
|
||||||
|
|
||||||
|
# 2. Проверка протокола
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if parsed.scheme == "http":
|
||||||
|
return False, "HTTP (без TLS)"
|
||||||
|
|
||||||
|
host = parsed.hostname or url
|
||||||
|
port = parsed.port or 443
|
||||||
|
|
||||||
|
# 3. Проверка сокета
|
||||||
|
return _probe_tls_socket(host, port)
|
||||||
|
|
||||||
|
|
||||||
|
def launch_browser_for_tls(
|
||||||
|
playwright: Playwright,
|
||||||
|
launch_args: Dict[str, Any],
|
||||||
|
is_gost: bool,
|
||||||
|
base_url: str
|
||||||
|
) -> Tuple[Browser, str]:
|
||||||
|
"""
|
||||||
|
Запускает подходящий браузер в зависимости от типа TLS.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
(browser_instance, description_string)
|
||||||
|
|
||||||
|
Логика:
|
||||||
|
- Стандартный TLS: запускает встроенный Chromium Playwright с аргументами launch_args.
|
||||||
|
- ГОСТ TLS:
|
||||||
|
1. Ищет путь к Chromium-GOST / Яндекс.Браузеру через find_gost_browser_executable().
|
||||||
|
2. Если найден — запускает с заданными launch_args.
|
||||||
|
3. Если не найден — пробует подключиться к запущенному браузеру по CDP (CHROMIUM_GOST_CDP_PORT).
|
||||||
|
4. Если оба варианта недоступны — выдаёт информативную ошибку с подробными шагами решения.
|
||||||
|
"""
|
||||||
|
cdp_port = os.getenv("CHROMIUM_GOST_CDP_PORT", "9222").strip()
|
||||||
|
|
||||||
|
if not is_gost:
|
||||||
|
# Стандартный TLS
|
||||||
|
browser = playwright.chromium.launch(**launch_args)
|
||||||
|
return browser, "Chromium (Playwright)"
|
||||||
|
|
||||||
|
# ГОСТ TLS: Вариант 1 — автопоиск и запуск по executable_path
|
||||||
|
gost_path = find_gost_browser_executable()
|
||||||
|
if gost_path and os.path.exists(gost_path):
|
||||||
|
try:
|
||||||
|
browser = playwright.chromium.launch(
|
||||||
|
executable_path=gost_path,
|
||||||
|
**launch_args
|
||||||
|
)
|
||||||
|
browser_name = "Яндекс.Браузер" if "yandex" in gost_path.lower() else "Chromium GOST"
|
||||||
|
return browser, f"{browser_name} ({gost_path})"
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = str(e).encode("ascii", errors="replace").decode("ascii")
|
||||||
|
print(f"\n[GOST] Предупреждение: ошибка прямого запуска ({gost_path}): {err_msg}")
|
||||||
|
print("[GOST] Пробуем подключение по CDP...")
|
||||||
|
|
||||||
|
# ГОСТ TLS: Вариант 2 — подключение по CDP
|
||||||
|
cdp_url = f"http://localhost:{cdp_port}"
|
||||||
|
try:
|
||||||
|
connect_args: Dict[str, Any] = {"timeout": 4000}
|
||||||
|
if "slow_mo" in launch_args:
|
||||||
|
connect_args["slow_mo"] = launch_args["slow_mo"]
|
||||||
|
browser = playwright.chromium.connect_over_cdp(cdp_url, **connect_args)
|
||||||
|
return browser, f"Chromium GOST (CDP порт {cdp_port})"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Ничего не подошло — информативное сообщение об ошибке
|
||||||
|
pytest.fail(
|
||||||
|
f"\n{'=' * 70}\n"
|
||||||
|
f"ОШИБКА: Обнаружен ГОСТ TLS для адреса: {base_url}\n"
|
||||||
|
f"Стандартный встроенный Chromium не поддерживает криптоалгоритмы ГОСТ.\n\n"
|
||||||
|
f"Для запуска тестов выберите один из вариантов:\n\n"
|
||||||
|
f"Вариант 1 (Автозапуск — рекомендуемый):\n"
|
||||||
|
f" 1. Скачайте Chromium GOST:\n"
|
||||||
|
f" https://github.com/deemru/chromium-gost/releases\n"
|
||||||
|
f" 2. Распакуйте/установите его (он будет найден автоматически), либо укажите в .env:\n"
|
||||||
|
f" CHROMIUM_GOST_PATH=C:\\Path\\To\\chromium-gost\\chrome.exe\n"
|
||||||
|
f" * Примечание: также поддерживается установленный Яндекс.Браузер с КриптоПро CSP.\n\n"
|
||||||
|
f"Вариант 2 (Подключение по CDP):\n"
|
||||||
|
f" 1. Запустите Chromium GOST вручную с флагом отладки:\n"
|
||||||
|
f" chrome.exe --remote-debugging-port={cdp_port} --no-first-run\n"
|
||||||
|
f" 2. Запустите pytest повторно\n"
|
||||||
|
f"{'=' * 70}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user