Initial commit
This commit is contained in:
commit
9795660e1f
43 changed files with 2757 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.pytest_cache/
|
||||||
|
allure-report/
|
||||||
|
allure-results/
|
||||||
|
logs/
|
||||||
|
reports/
|
||||||
|
screenshots/
|
||||||
0
Dockerfile
Normal file
0
Dockerfile
Normal file
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
BIN
config/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
config/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/api_config.cpython-313.pyc
Normal file
BIN
config/__pycache__/api_config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/environment.cpython-313.pyc
Normal file
BIN
config/__pycache__/environment.cpython-313.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/session_config.cpython-313.pyc
Normal file
BIN
config/__pycache__/session_config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/settings.cpython-313.pyc
Normal file
BIN
config/__pycache__/settings.cpython-313.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/ui_config.cpython-313.pyc
Normal file
BIN
config/__pycache__/ui_config.cpython-313.pyc
Normal file
Binary file not shown.
76
config/api_config.py
Normal file
76
config/api_config.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from config.environment import EnvironmentLoader
|
||||||
|
|
||||||
|
class APIConfig:
|
||||||
|
"""Настройки API"""
|
||||||
|
|
||||||
|
# Базовые настройки API
|
||||||
|
API_BASE_URL = EnvironmentLoader.get_env_variable('API_BASE_URL', 'http://localhost:8080/api/v1')
|
||||||
|
API_VERSION = EnvironmentLoader.get_env_variable('API_VERSION', 'v1')
|
||||||
|
API_TIMEOUT = EnvironmentLoader.get_env_variable('API_TIMEOUT', 30) # секунды
|
||||||
|
|
||||||
|
# Настройки запросов
|
||||||
|
MAX_RETRIES = EnvironmentLoader.get_env_variable('MAX_RETRIES', 3)
|
||||||
|
RETRY_DELAY = EnvironmentLoader.get_env_variable('RETRY_DELAY', 1) # секунды
|
||||||
|
RETRY_BACKOFF_FACTOR = EnvironmentLoader.get_env_variable('RETRY_BACKOFF_FACTOR', 2)
|
||||||
|
|
||||||
|
# Настройки валидации
|
||||||
|
VALIDATE_SCHEMAS = EnvironmentLoader.get_env_variable('VALIDATE_SCHEMAS', True)
|
||||||
|
STRICT_VALIDATION = EnvironmentLoader.get_env_variable('STRICT_VALIDATION', False)
|
||||||
|
|
||||||
|
# Настройки тестовых данных
|
||||||
|
TEST_DATA_PREFIX = EnvironmentLoader.get_env_variable('TEST_DATA_PREFIX', 'test_')
|
||||||
|
CLEANUP_AFTER_TESTS = EnvironmentLoader.get_env_variable('CLEANUP_AFTER_TESTS', True)
|
||||||
|
CLEANUP_ONLY_FAILED = EnvironmentLoader.get_env_variable('CLEANUP_ONLY_FAILED', False)
|
||||||
|
|
||||||
|
# Endpoints (можно переопределить через окружение)
|
||||||
|
ENDPOINTS = {
|
||||||
|
'auth_login': EnvironmentLoader.get_env_variable('ENDPOINT_AUTH_LOGIN', '/login'),
|
||||||
|
'auth_logout': EnvironmentLoader.get_env_variable('ENDPOINT_AUTH_LOGOUT', '/logout'),
|
||||||
|
'users_create': EnvironmentLoader.get_env_variable('ENDPOINT_USERS_CREATE', '/team/'),
|
||||||
|
'users_get_all': EnvironmentLoader.get_env_variable('ENDPOINT_USERS_GET_ALL', '/members/'),
|
||||||
|
'users_get_by_id': EnvironmentLoader.get_env_variable('ENDPOINT_USERS_GET_BY_ID', '/members/{id}'),
|
||||||
|
'users_get_by_name': EnvironmentLoader.get_env_variable('ENDPOINT_USERS_GET_BY_NAME', '/members/name/{name}'),
|
||||||
|
'users_update': EnvironmentLoader.get_env_variable('ENDPOINT_USERS_UPDATE', '/team/{id}'),
|
||||||
|
'users_delete': EnvironmentLoader.get_env_variable('ENDPOINT_USERS_DELETE', '/team/{id}'),
|
||||||
|
'posts_create': EnvironmentLoader.get_env_variable('ENDPOINT_POSTS_CREATE', '/post'),
|
||||||
|
'posts_get_all': EnvironmentLoader.get_env_variable('ENDPOINT_POSTS_GET_ALL', '/post'),
|
||||||
|
'posts_get_by_id': EnvironmentLoader.get_env_variable('ENDPOINT_POSTS_GET_BY_ID', '/post/{id}'),
|
||||||
|
'posts_get_by_offset': EnvironmentLoader.get_env_variable('ENDPOINT_POSTS_GET_BY_OFFSET', '/post/offset/{offset}'),
|
||||||
|
'posts_update': EnvironmentLoader.get_env_variable('ENDPOINT_POSTS_UPDATE', '/post/{id}'),
|
||||||
|
'posts_delete': EnvironmentLoader.get_env_variable('ENDPOINT_POSTS_DELETE', '/post/{id}'),
|
||||||
|
'images_upload': EnvironmentLoader.get_env_variable('ENDPOINT_IMAGES_UPLOAD', '/images/'),
|
||||||
|
'images_get': EnvironmentLoader.get_env_variable('ENDPOINT_IMAGES_GET', '/images/{path}'),
|
||||||
|
'images_delete': EnvironmentLoader.get_env_variable('ENDPOINT_IMAGES_DELETE', '/images/{path}'),
|
||||||
|
'images_list': EnvironmentLoader.get_env_variable('ENDPOINT_IMAGES_LIST', '/images'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Настройки по умолчанию для запросов
|
||||||
|
DEFAULT_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Accept-Language': EnvironmentLoader.get_env_variable('API_ACCEPT_LANGUAGE', 'en-US,en;q=0.9'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Настройки прокси (если нужно)
|
||||||
|
PROXY_ENABLED = EnvironmentLoader.get_env_variable('PROXY_ENABLED', False)
|
||||||
|
PROXY_HTTP = EnvironmentLoader.get_env_variable('PROXY_HTTP', None)
|
||||||
|
PROXY_HTTPS = EnvironmentLoader.get_env_variable('PROXY_HTTPS', None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_endpoint(cls, endpoint_name: str, **kwargs) -> str:
|
||||||
|
"""Получение endpoint с подстановкой параметров"""
|
||||||
|
endpoint_template = cls.ENDPOINTS.get(endpoint_name)
|
||||||
|
if not endpoint_template:
|
||||||
|
raise ValueError(f"Endpoint '{endpoint_name}' not found in configuration")
|
||||||
|
|
||||||
|
return cls.API_BASE_URL + endpoint_template.format(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_endpoints(cls) -> Dict[str, str]:
|
||||||
|
"""Получение всех endpoints"""
|
||||||
|
return cls.ENDPOINTS.copy()
|
||||||
|
|
||||||
|
# Экспорт конфигурации
|
||||||
|
api_config = APIConfig
|
||||||
152
config/environment.py
Normal file
152
config/environment.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class EnvironmentLoader:
|
||||||
|
"""Класс для загрузки переменных окружения из .env файлов"""
|
||||||
|
|
||||||
|
# Порядок загрузки файлов .env (от более специфичного к общему)
|
||||||
|
ENV_FILES_ORDER = [
|
||||||
|
'.env.local', # Локальные переопределения (не в git)
|
||||||
|
f'.env.{os.getenv("ENV", "development")}', # Окружение: .env.test, .env.production
|
||||||
|
'.env', # Основной файл
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_environment(cls, env_file: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Загрузка переменных окружения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_file: Конкретный файл для загрузки (если None, используется порядок из ENV_FILES_ORDER)
|
||||||
|
"""
|
||||||
|
if env_file:
|
||||||
|
# Загрузка конкретного файла
|
||||||
|
env_path = Path(env_file)
|
||||||
|
if env_path.exists():
|
||||||
|
logger.info(f"Loading environment from specified file: {env_path}")
|
||||||
|
load_dotenv(dotenv_path=env_path, override=True)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Specified env file not found: {env_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Автоматическая загрузка в определенном порядке
|
||||||
|
env_loaded = False
|
||||||
|
|
||||||
|
for env_filename in cls.ENV_FILES_ORDER:
|
||||||
|
env_path = find_dotenv(env_filename, usecwd=True)
|
||||||
|
if env_path:
|
||||||
|
logger.info(f"Loading environment from: {env_path}")
|
||||||
|
load_dotenv(dotenv_path=env_path, override=True)
|
||||||
|
env_loaded = True
|
||||||
|
|
||||||
|
if not env_loaded:
|
||||||
|
logger.warning("No .env files found, using system environment variables")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_env_variable(cls, key: str, default: Any = None, required: bool = False) -> Any:
|
||||||
|
"""
|
||||||
|
Получение переменной окружения с проверкой.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Ключ переменной окружения
|
||||||
|
default: Значение по умолчанию, если переменная не найдена
|
||||||
|
required: Обязательна ли переменная (вызывает исключение, если не найдена)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Значение переменной окружения
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Если переменная required=True и не найдена
|
||||||
|
"""
|
||||||
|
value = os.getenv(key)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
if required:
|
||||||
|
raise ValueError(f"Required environment variable '{key}' is not set")
|
||||||
|
return default
|
||||||
|
|
||||||
|
# Автоматическое преобразование типов
|
||||||
|
if value.lower() in ('true', 'false'):
|
||||||
|
return value.lower() == 'true'
|
||||||
|
elif value.isdigit():
|
||||||
|
return int(value)
|
||||||
|
elif cls._is_float(value):
|
||||||
|
return float(value)
|
||||||
|
elif value.startswith('[') and value.endswith(']'):
|
||||||
|
# Список значений, разделенных запятыми
|
||||||
|
return [item.strip() for item in value[1:-1].split(',') if item.strip()]
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_float(value: str) -> bool:
|
||||||
|
"""Проверка, можно ли преобразовать строку в float"""
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_environment(cls):
|
||||||
|
"""Валидация обязательных переменных окружения"""
|
||||||
|
required_vars = [
|
||||||
|
# 'API_BASE_URL',
|
||||||
|
# 'UI_BASE_URL',
|
||||||
|
]
|
||||||
|
|
||||||
|
missing_vars = []
|
||||||
|
for var in required_vars:
|
||||||
|
if not os.getenv(var):
|
||||||
|
missing_vars.append(var)
|
||||||
|
|
||||||
|
if missing_vars:
|
||||||
|
raise EnvironmentError(
|
||||||
|
f"Missing required environment variables: {', '.join(missing_vars)}\n"
|
||||||
|
f"Please set them in .env file or system environment."
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def print_environment_info(cls):
|
||||||
|
"""Вывод информации о текущем окружении (для отладки)"""
|
||||||
|
env_info = {
|
||||||
|
'ENV': os.getenv('ENV', 'development'),
|
||||||
|
'API_BASE_URL': os.getenv('API_BASE_URL'),
|
||||||
|
'UI_BASE_URL': os.getenv('UI_BASE_URL'),
|
||||||
|
'DEBUG': os.getenv('DEBUG', 'False'),
|
||||||
|
'LOG_LEVEL': os.getenv('LOG_LEVEL', 'INFO'),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Current environment configuration:")
|
||||||
|
for key, value in env_info.items():
|
||||||
|
logger.info(f" {key}: {value}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_env_variables(cls, prefix: str = '') -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Получение всех переменных окружения с опциональным префиксом.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prefix: Фильтр по префиксу (например, 'API_' для всех API настроек)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь переменных окружения
|
||||||
|
"""
|
||||||
|
env_vars = {}
|
||||||
|
|
||||||
|
for key, _ in os.environ.items():
|
||||||
|
if len(prefix) == 0 and not key.startswith(prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
env_vars[key] = cls.get_env_variable(key)
|
||||||
|
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
# Инициализация при импорте модуля
|
||||||
|
EnvironmentLoader.load_environment()
|
||||||
42
config/session_config.py
Normal file
42
config/session_config.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from typing import Dict
|
||||||
|
from config.environment import EnvironmentLoader
|
||||||
|
|
||||||
|
class SessionConfig:
|
||||||
|
"""Настройки сессий и авторизации"""
|
||||||
|
|
||||||
|
# Настройки сессий
|
||||||
|
SESSION_TIMEOUT = EnvironmentLoader.get_env_variable('SESSION_TIMEOUT', 1800) # 30 минут
|
||||||
|
SESSION_COOKIE_NAME = EnvironmentLoader.get_env_variable('SESSION_COOKIE_NAME', 'session')
|
||||||
|
SESSION_COOKIE_SECURE = EnvironmentLoader.get_env_variable('SESSION_COOKIE_SECURE', True)
|
||||||
|
SESSION_COOKIE_HTTPONLY = EnvironmentLoader.get_env_variable('SESSION_COOKIE_HTTPONLY', False)
|
||||||
|
|
||||||
|
# Настройки администратора для тестов
|
||||||
|
ADMIN_USERNAME = EnvironmentLoader.get_env_variable('ADMIN_USERNAME', 'muts')
|
||||||
|
ADMIN_PASSWORD = EnvironmentLoader.get_env_variable('ADMIN_PASSWORD', 'Abc1205')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_admin_credentials(cls) -> Dict[str, str]:
|
||||||
|
"""Получение учетных данных администратора"""
|
||||||
|
return {
|
||||||
|
"username": cls.ADMIN_USERNAME,
|
||||||
|
"password": cls.ADMIN_PASSWORD,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_session_headers(cls) -> Dict[str, str]:
|
||||||
|
"""Получение заголовков для сессий"""
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Host": "localhost:8080",
|
||||||
|
"Origin": "http://localhost:8080",
|
||||||
|
}
|
||||||
|
|
||||||
|
if cls.SESSION_COOKIE_SECURE:
|
||||||
|
headers["X-Requested-With"] = "XMLHttpRequest"
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
# Экспорт конфигурации
|
||||||
|
session_config = SessionConfig
|
||||||
168
config/settings.py
Normal file
168
config/settings.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
from config.environment import EnvironmentLoader
|
||||||
|
|
||||||
|
# Инициализация логгера
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""Основные настройки приложения"""
|
||||||
|
|
||||||
|
# Загрузка переменных окружения
|
||||||
|
ENV = EnvironmentLoader.get_env_variable('ENV', 'development')
|
||||||
|
DEBUG = EnvironmentLoader.get_env_variable('DEBUG', False)
|
||||||
|
|
||||||
|
# Базовые URL
|
||||||
|
API_BASE_URL = EnvironmentLoader.get_env_variable('API_BASE_URL', 'http://localhost:8080/api/v1')
|
||||||
|
UI_BASE_URL = EnvironmentLoader.get_env_variable('UI_BASE_URL', 'http://localhost:5173')
|
||||||
|
|
||||||
|
# Настройки логирования
|
||||||
|
LOG_LEVEL = EnvironmentLoader.get_env_variable('LOG_LEVEL', 'INFO').upper()
|
||||||
|
LOG_FORMAT = EnvironmentLoader.get_env_variable(
|
||||||
|
'LOG_FORMAT',
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
LOG_FILE = EnvironmentLoader.get_env_variable('LOG_FILE', 'logs/tests.log')
|
||||||
|
|
||||||
|
# Настройки тестов
|
||||||
|
TEST_TIMEOUT = EnvironmentLoader.get_env_variable('TEST_TIMEOUT', 30) # секунды
|
||||||
|
TEST_RETRIES = EnvironmentLoader.get_env_variable('TEST_RETRIES', 3)
|
||||||
|
TEST_PARALLEL_WORKERS = EnvironmentLoader.get_env_variable('TEST_PARALLEL_WORKERS', 'auto')
|
||||||
|
|
||||||
|
# Настройки Allure
|
||||||
|
ALLURE_RESULTS_DIR = EnvironmentLoader.get_env_variable('ALLURE_RESULTS_DIR', 'allure-results')
|
||||||
|
ALLURE_REPORT_DIR = EnvironmentLoader.get_env_variable('ALLURE_REPORT_DIR', 'allure-report')
|
||||||
|
|
||||||
|
# Настройки ожиданий
|
||||||
|
IMPLICIT_WAIT = EnvironmentLoader.get_env_variable('IMPLICIT_WAIT', 10) # секунды
|
||||||
|
EXPLICIT_WAIT = EnvironmentLoader.get_env_variable('EXPLICIT_WAIT', 30) # секунды
|
||||||
|
POLL_FREQUENCY = EnvironmentLoader.get_env_variable('POLL_FREQUENCY', 0.5) # секунды
|
||||||
|
|
||||||
|
# Базовые пути
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
TESTS_DIR = PROJECT_ROOT / 'tests'
|
||||||
|
DATA_DIR = TESTS_DIR / 'data'
|
||||||
|
REPORTS_DIR = PROJECT_ROOT / 'reports'
|
||||||
|
|
||||||
|
# Создание необходимых директорий
|
||||||
|
@classmethod
|
||||||
|
def create_directories(cls):
|
||||||
|
"""Создание необходимых директорий"""
|
||||||
|
directories = [
|
||||||
|
cls.REPORTS_DIR,
|
||||||
|
cls.DATA_DIR,
|
||||||
|
Path(cls.LOG_FILE).parent if cls.LOG_FILE else None,
|
||||||
|
Path(cls.ALLURE_RESULTS_DIR).parent if cls.ALLURE_RESULTS_DIR else None,
|
||||||
|
]
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
if directory and not directory.exists():
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
logger.debug(f"Created directory: {directory}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_logging_config(cls) -> Dict[str, Any]:
|
||||||
|
"""Конфигурация логирования"""
|
||||||
|
return {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'standard': {
|
||||||
|
'format': cls.LOG_FORMAT,
|
||||||
|
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||||
|
},
|
||||||
|
'detailed': {
|
||||||
|
'format': '%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s',
|
||||||
|
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'level': cls.LOG_LEVEL,
|
||||||
|
'formatter': 'standard',
|
||||||
|
'stream': 'ext://sys.stdout',
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'level': cls.LOG_LEVEL,
|
||||||
|
'formatter': 'detailed',
|
||||||
|
'filename': cls.LOG_FILE,
|
||||||
|
'mode': 'a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'': { # root logger
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': cls.LOG_LEVEL,
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
'tests': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': cls.LOG_LEVEL,
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'api': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': cls.LOG_LEVEL,
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'ui': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': cls.LOG_LEVEL,
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_logging(cls):
|
||||||
|
"""Настройка логирования"""
|
||||||
|
import logging.config
|
||||||
|
logging.config.dictConfig(cls.get_logging_config())
|
||||||
|
logger.info(f"Logging configured with level: {cls.LOG_LEVEL}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls):
|
||||||
|
"""Валидация настроек"""
|
||||||
|
# Проверка обязательных переменных
|
||||||
|
EnvironmentLoader.validate_environment()
|
||||||
|
|
||||||
|
# Проверка URL
|
||||||
|
import validators
|
||||||
|
if not validators.url(cls.API_BASE_URL):
|
||||||
|
logger.warning(f"API_BASE_URL might be invalid: {cls.API_BASE_URL}")
|
||||||
|
|
||||||
|
if not validators.url(cls.UI_BASE_URL):
|
||||||
|
logger.warning(f"UI_BASE_URL might be invalid: {cls.UI_BASE_URL}")
|
||||||
|
|
||||||
|
# Создание директорий
|
||||||
|
cls.create_directories()
|
||||||
|
|
||||||
|
logger.info(f"Settings validated for environment: {cls.ENV}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def print_summary(cls):
|
||||||
|
"""Вывод сводки настроек"""
|
||||||
|
summary = f"""
|
||||||
|
===== Environment Settings =====
|
||||||
|
Environment: {cls.ENV}
|
||||||
|
Debug Mode: {cls.DEBUG}
|
||||||
|
API Base URL: {cls.API_BASE_URL}
|
||||||
|
UI Base URL: {cls.UI_BASE_URL}
|
||||||
|
Log Level: {cls.LOG_LEVEL}
|
||||||
|
Test Timeout: {cls.TEST_TIMEOUT}s
|
||||||
|
Test Retries: {cls.TEST_RETRIES}
|
||||||
|
Parallel Workers: {cls.TEST_PARALLEL_WORKERS}
|
||||||
|
=================================
|
||||||
|
"""
|
||||||
|
logger.info(summary)
|
||||||
|
|
||||||
|
# Инициализация настроек при импорте
|
||||||
|
Settings.validate()
|
||||||
|
Settings.setup_logging()
|
||||||
|
Settings.print_summary()
|
||||||
|
|
||||||
|
# Экспорт настроек
|
||||||
|
settings = Settings
|
||||||
90
config/ui_config.py
Normal file
90
config/ui_config.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
from typing import Dict, Any
|
||||||
|
from config.environment import EnvironmentLoader
|
||||||
|
|
||||||
|
class UIConfig:
|
||||||
|
"""Настройки UI тестов"""
|
||||||
|
|
||||||
|
# Базовые настройки
|
||||||
|
UI_BASE_URL = EnvironmentLoader.get_env_variable('UI_BASE_URL', 'http://localhost:5173')
|
||||||
|
UI_TIMEOUT = EnvironmentLoader.get_env_variable('UI_TIMEOUT', 30) # секунды
|
||||||
|
|
||||||
|
# Настройки браузера
|
||||||
|
BROWSER_NAME = EnvironmentLoader.get_env_variable('BROWSER_NAME', 'firefox').lower()
|
||||||
|
BROWSER_HEADLESS = EnvironmentLoader.get_env_variable('BROWSER_HEADLESS', True)
|
||||||
|
BROWSER_WIDTH = EnvironmentLoader.get_env_variable('BROWSER_WIDTH', 1920)
|
||||||
|
BROWSER_HEIGHT = EnvironmentLoader.get_env_variable('BROWSER_HEIGHT', 1080)
|
||||||
|
BROWSER_LANGUAGE = EnvironmentLoader.get_env_variable('BROWSER_LANGUAGE', 'en')
|
||||||
|
BROWSER_FULLSCREEN = EnvironmentLoader.get_env_variable('BROWSER_FULLSCREEN', False)
|
||||||
|
|
||||||
|
# Настройки Chrome
|
||||||
|
CHROME_HEADLESS_NEW = EnvironmentLoader.get_env_variable('CHROME_HEADLESS_NEW', True)
|
||||||
|
CHROME_DISABLE_GPU = EnvironmentLoader.get_env_variable('CHROME_DISABLE_GPU', True)
|
||||||
|
CHROME_NO_SANDBOX = EnvironmentLoader.get_env_variable('CHROME_NO_SANDBOX', True)
|
||||||
|
CHROME_DISABLE_DEV_SHM = EnvironmentLoader.get_env_variable('CHROME_DISABLE_DEV_SHM', True)
|
||||||
|
|
||||||
|
# Настройки Firefox
|
||||||
|
FIREFOX_HEADLESS = EnvironmentLoader.get_env_variable('FIREFOX_HEADLESS', True)
|
||||||
|
|
||||||
|
# Настройки ожиданий
|
||||||
|
IMPLICIT_WAIT = EnvironmentLoader.get_env_variable('IMPLICIT_WAIT', 10) # секунды
|
||||||
|
EXPLICIT_WAIT = EnvironmentLoader.get_env_variable('EXPLICIT_WAIT', 30) # секунды
|
||||||
|
PAGE_LOAD_TIMEOUT = EnvironmentLoader.get_env_variable('PAGE_LOAD_TIMEOUT', 60) # секунды
|
||||||
|
SCRIPT_TIMEOUT = EnvironmentLoader.get_env_variable('SCRIPT_TIMEOUT', 30) # секунды
|
||||||
|
|
||||||
|
# Настройки скриншотов
|
||||||
|
SCREENSHOTS_ON_FAILURE = EnvironmentLoader.get_env_variable('SCREENSHOTS_ON_FAILURE', True)
|
||||||
|
SCREENSHOTS_DIR = EnvironmentLoader.get_env_variable('SCREENSHOTS_DIR', 'screenshots')
|
||||||
|
SCREENSHOTS_FORMAT = EnvironmentLoader.get_env_variable('SCREENSHOTS_FORMAT', 'png')
|
||||||
|
|
||||||
|
# Пути
|
||||||
|
URLS = {
|
||||||
|
"team": EnvironmentLoader.get_env_variable('UI_URL_TEAM', '/team'),
|
||||||
|
"blog": EnvironmentLoader.get_env_variable('UI_URL_BLOG', '/blog')
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_url(cls, url_name: str, **kwargs) -> str:
|
||||||
|
"""Получение endpoint с подстановкой параметров"""
|
||||||
|
url_template = cls.URLS.get(url_name)
|
||||||
|
if not url_template:
|
||||||
|
raise ValueError(f"URL '{url_name}' not found in configuration")
|
||||||
|
|
||||||
|
return cls.UI_BASE_URL + url_template.format(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_browser_options(cls) -> Dict[str, Any]:
|
||||||
|
"""Получение опций браузера"""
|
||||||
|
options = {
|
||||||
|
'headless': cls.BROWSER_HEADLESS,
|
||||||
|
'width': cls.BROWSER_WIDTH,
|
||||||
|
'height': cls.BROWSER_HEIGHT,
|
||||||
|
'language': cls.BROWSER_LANGUAGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cls.BROWSER_NAME == 'chrome':
|
||||||
|
options.update({
|
||||||
|
'headless_new': cls.CHROME_HEADLESS_NEW,
|
||||||
|
'disable_gpu': cls.CHROME_DISABLE_GPU,
|
||||||
|
'no_sandbox': cls.CHROME_NO_SANDBOX,
|
||||||
|
'disable_dev_shm': cls.CHROME_DISABLE_DEV_SHM,
|
||||||
|
})
|
||||||
|
elif cls.BROWSER_NAME == 'firefox':
|
||||||
|
options.update({
|
||||||
|
'headless': cls.FIREFOX_HEADLESS,
|
||||||
|
})
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_wait_config(cls) -> Dict[str, Any]:
|
||||||
|
"""Получение конфигурации ожиданий"""
|
||||||
|
return {
|
||||||
|
'implicit': cls.IMPLICIT_WAIT,
|
||||||
|
'explicit': cls.EXPLICIT_WAIT,
|
||||||
|
'page_load': cls.PAGE_LOAD_TIMEOUT,
|
||||||
|
'script': cls.SCRIPT_TIMEOUT,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Экспорт конфигурации
|
||||||
|
ui_config = UIConfig
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
selenium==3.141.0
|
||||||
|
pytest==7.4.3
|
||||||
|
urllib3<=2.0
|
||||||
|
allure-pytest
|
||||||
|
dotenv
|
||||||
|
validators
|
||||||
|
colorama
|
||||||
33
scripts/run_tests.sh
Executable file
33
scripts/run_tests.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
ENV_FILE="${1:-.env.test}"
|
||||||
|
|
||||||
|
echo "Loading environment from: $ENV_FILE"
|
||||||
|
|
||||||
|
# Экспорт переменных окружения
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
export $(grep -v '^#' "$ENV_FILE" | xargs)
|
||||||
|
else
|
||||||
|
echo "Warning: Environment file $ENV_FILE not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir allure-results
|
||||||
|
|
||||||
|
# Запуск тестов
|
||||||
|
echo "Running tests..."
|
||||||
|
pytest tests/ \
|
||||||
|
-v \
|
||||||
|
--alluredir=allure-results \
|
||||||
|
--junitxml=reports/junit.xml \
|
||||||
|
--log-level=DEBUG
|
||||||
|
|
||||||
|
# Генерация Allure отчета
|
||||||
|
if command -v allure &>/dev/null; then
|
||||||
|
echo "Generating Allure report..."
|
||||||
|
allure generate allure-results -o allure-report
|
||||||
|
echo "Allure report generated: allure-report/index.html"
|
||||||
|
echo "Serve allure results"
|
||||||
|
allure serve allure-results
|
||||||
|
else
|
||||||
|
echo "Allure CLI not found, skipping report generation"
|
||||||
|
fi
|
||||||
0
tests/api/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
64
tests/api/conftest.py
Normal file
64
tests/api/conftest.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import logging
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from config.session_config import session_config
|
||||||
|
from tests.api.utils.api_client import APIClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def api_client(api_base_url):
|
||||||
|
"""Клиент для работы с API"""
|
||||||
|
return APIClient(base_url=api_base_url)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def admin_credentials():
|
||||||
|
"""Учетные данные администратора"""
|
||||||
|
return session_config.get_admin_credentials()
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def auth_admin(api_client: APIClient, admin_credentials):
|
||||||
|
with allure.step("Admin authentication fixture"):
|
||||||
|
logger.info("Authentificate admin")
|
||||||
|
|
||||||
|
success = api_client.login(
|
||||||
|
admin_credentials["username"],
|
||||||
|
admin_credentials["password"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert success is True
|
||||||
|
assert api_client.logged_in is True
|
||||||
|
|
||||||
|
logger.info("Admin authenticated")
|
||||||
|
|
||||||
|
yield api_client
|
||||||
|
|
||||||
|
api_client.logout()
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def auth_user(api_client: APIClient, auth_admin: APIClient, api_user_auth_data):
|
||||||
|
id = ''
|
||||||
|
|
||||||
|
with allure.step("User auth fixture"):
|
||||||
|
logger.info("Creating new user for auth")
|
||||||
|
|
||||||
|
resp = auth_admin.create_user(api_user_auth_data)
|
||||||
|
assert resp.status_code is 201
|
||||||
|
id = resp.json()['id']
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(f"Auth as user: {api_user_auth_data['username']}")
|
||||||
|
|
||||||
|
resp = api_client.login(
|
||||||
|
api_user_auth_data['username'],
|
||||||
|
api_user_auth_data['password']
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp
|
||||||
|
assert api_client.logged_in
|
||||||
|
|
||||||
|
yield api_client
|
||||||
|
|
||||||
|
api_client.logout()
|
||||||
|
auth_admin.delete_user(id)
|
||||||
|
|
||||||
206
tests/api/test_posts.py
Normal file
206
tests/api/test_posts.py
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
from typing import Any, Dict
|
||||||
|
import pytest
|
||||||
|
import allure
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from tests.api.utils.api_client import APIClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Фикстура для получения списка постов из API
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def list_of_posts(api_client):
|
||||||
|
with allure.step("Get all posts"):
|
||||||
|
resp = api_client.get_all_posts()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
# Отдельные классы вариант
|
||||||
|
@allure.feature("Posts")
|
||||||
|
@allure.story("Guest permissions")
|
||||||
|
class TestGuestPosts:
|
||||||
|
"""Тестирование операций с постами под гостем (неавторизованный доступ)"""
|
||||||
|
|
||||||
|
@allure.title("Guest: Creating new posts - should fail")
|
||||||
|
def test_guest_posts_creating(self, api_client, api_post_data):
|
||||||
|
with allure.step("Logged as guest"):
|
||||||
|
logger.info("Guest trying to create posts")
|
||||||
|
for post in api_post_data:
|
||||||
|
resp = api_client.create_post(post)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
@allure.title("Guest: Update posts - should fail")
|
||||||
|
def test_guest_posts_update(self, api_client, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
|
||||||
|
with allure.step("Guest trying to change posts data"):
|
||||||
|
for post in posts:
|
||||||
|
logger.info(f"Guest changing post: {post['title']}")
|
||||||
|
post["title"] = "Changed by guest"
|
||||||
|
post["description"] = "Changed by guest"
|
||||||
|
post["content"] = "Changed by guest"
|
||||||
|
|
||||||
|
resp = api_client.update_post(post["id"], post)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
@allure.title("Guest: Get all posts - should succeed")
|
||||||
|
def test_guest_get_all_posts(self, api_client):
|
||||||
|
with allure.step("Guest getting all posts"):
|
||||||
|
logger.info("Guest getting all posts")
|
||||||
|
resp = api_client.get_all_posts()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("Guest: Get post by ID - should succeed")
|
||||||
|
def test_guest_get_post_by_id(self, api_client, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
if posts:
|
||||||
|
post_id = posts[0]["id"]
|
||||||
|
|
||||||
|
with allure.step(f"Guest getting post with ID {post_id}"):
|
||||||
|
logger.info(f"Guest getting post: {post_id}")
|
||||||
|
resp = api_client.get_post(post_id)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("Guest: Delete posts - should fail")
|
||||||
|
def test_guest_posts_deleting(self, api_client, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
|
||||||
|
with allure.step("Guest trying to delete posts"):
|
||||||
|
for post in posts:
|
||||||
|
logger.info(f"Guest deleting post: {post['title']}")
|
||||||
|
resp = api_client.delete_post(post["id"])
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@allure.feature("Posts")
|
||||||
|
@allure.story("Admin permissions")
|
||||||
|
class TestAdminPosts:
|
||||||
|
"""Тестирование операций с постами под администратором"""
|
||||||
|
|
||||||
|
@allure.title("Admin: Creating new posts - should succeed")
|
||||||
|
def test_admin_posts_creating(self, auth_admin, api_post_data, api_user_data):
|
||||||
|
id = ''
|
||||||
|
with allure.step("Logged as admin"):
|
||||||
|
pass
|
||||||
|
with allure.step("Create new user"):
|
||||||
|
user = api_user_data[0]
|
||||||
|
resp = auth_admin.create_user(user)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
id = resp.json()['id']
|
||||||
|
with allure.step("Create posts"):
|
||||||
|
logger.info("Admin creating posts")
|
||||||
|
for post in api_post_data:
|
||||||
|
post['userId'] = id
|
||||||
|
resp = auth_admin.create_post(post)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
@allure.title("Admin: Update posts - should succeed")
|
||||||
|
def test_admin_posts_update(self, auth_admin, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
|
||||||
|
with allure.step("Admin changing posts data"):
|
||||||
|
for post in posts:
|
||||||
|
logger.info(f"Admin changing post: {post['title']}")
|
||||||
|
new_post: Dict[str, Any] = {}
|
||||||
|
new_post["title"] = "Changed by admin."
|
||||||
|
new_post["description"] = "Changed by admin. Test data."
|
||||||
|
new_post["content"] = "Changed by admin."
|
||||||
|
|
||||||
|
resp = auth_admin.update_post(post["id"], new_post)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("Admin: Get all posts - should succeed")
|
||||||
|
def test_admin_get_all_posts(self, auth_admin):
|
||||||
|
with allure.step("Admin getting all posts"):
|
||||||
|
logger.info("Admin getting all posts")
|
||||||
|
resp = auth_admin.get_all_posts()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("Admin: Get post by ID - should succeed")
|
||||||
|
def test_admin_get_post_by_id(self, auth_admin, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
if posts:
|
||||||
|
post_id = posts[0]["id"]
|
||||||
|
|
||||||
|
with allure.step(f"Admin getting post with ID {post_id}"):
|
||||||
|
logger.info(f"Admin getting post: {post_id}")
|
||||||
|
resp = auth_admin.get_post(post_id)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("Admin: Delete posts - should succeed")
|
||||||
|
def test_admin_posts_deleting(self, auth_admin, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
id = posts[0]['userId']
|
||||||
|
|
||||||
|
with allure.step("Admin deleting posts"):
|
||||||
|
for post in posts:
|
||||||
|
logger.info(f"Admin deleting post: {post['title']}")
|
||||||
|
resp = auth_admin.delete_post(post["id"])
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
with allure.step("Delete user"):
|
||||||
|
resp = auth_admin.delete_user(id)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@allure.feature("Posts")
|
||||||
|
@allure.story("Regular user permissions")
|
||||||
|
class TestRegularUserPosts:
|
||||||
|
"""Тестирование операций с постами под обычным пользователем"""
|
||||||
|
|
||||||
|
@allure.title("User: Creating new posts - should succeed")
|
||||||
|
def test_user_posts_creating(self, auth_user: APIClient, api_post_data):
|
||||||
|
id = auth_user.get_all_users().json()[0]['id']
|
||||||
|
|
||||||
|
with allure.step("Logged as user"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with allure.step("Create new posts"):
|
||||||
|
logger.info("User creating posts")
|
||||||
|
for post in api_post_data:
|
||||||
|
post['userId'] = id
|
||||||
|
resp = auth_user.create_post(post)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
@allure.title("User: Update posts - should succeed")
|
||||||
|
def test_user_posts_update(self, auth_user, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
|
||||||
|
with allure.step("User changing posts data"):
|
||||||
|
for post in posts:
|
||||||
|
logger.info(f"User changing post: {post['title']}")
|
||||||
|
post["title"] = "Changed by user"
|
||||||
|
post["description"] = "Changed by user"
|
||||||
|
post["content"] = "Changed by user"
|
||||||
|
|
||||||
|
resp = auth_user.update_post(post["id"], post)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("User: Get all posts - should succeed")
|
||||||
|
def test_user_get_all_posts(self, auth_user):
|
||||||
|
with allure.step("User getting all posts"):
|
||||||
|
logger.info("User getting all posts")
|
||||||
|
resp = auth_user.get_all_posts()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("User: Get post by ID - should succeed")
|
||||||
|
def test_user_get_post_by_id(self, auth_user, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
if posts:
|
||||||
|
post_id = posts[0]["id"]
|
||||||
|
|
||||||
|
with allure.step(f"User getting post with ID {post_id}"):
|
||||||
|
logger.info(f"User getting post: {post_id}")
|
||||||
|
resp = auth_user.get_post(post_id)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("User: Delete posts - should succeed")
|
||||||
|
def test_user_posts_deleting(self, auth_user, list_of_posts):
|
||||||
|
posts = list_of_posts
|
||||||
|
|
||||||
|
with allure.step("User deleting posts"):
|
||||||
|
for post in posts:
|
||||||
|
logger.info(f"User deleting post: {post['title']}")
|
||||||
|
resp = auth_user.delete_post(post["id"])
|
||||||
|
assert resp.status_code == 200
|
||||||
133
tests/api/test_users.py
Normal file
133
tests/api/test_users.py
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import pytest
|
||||||
|
import allure
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from tests.api.utils.api_client import APIClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Фикстура для получения списка пользователей
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def list_of_users(api_client):
|
||||||
|
with allure.step("Get all users"):
|
||||||
|
resp = api_client.get_all_users()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
# Тест для гостя (неавторизованный пользователь)
|
||||||
|
@allure.story("Guest permissions")
|
||||||
|
class TestGuestUsers:
|
||||||
|
"""Тестирование операций с пользователями под гостем (неавторизованный доступ)"""
|
||||||
|
|
||||||
|
@allure.title("Guest: Creating new user - should fail")
|
||||||
|
def test_guest_users_creating(self, api_client: APIClient, api_user_data):
|
||||||
|
assert api_client.logged_in == False
|
||||||
|
with allure.step("Logged as guest - trying to create users"):
|
||||||
|
logger.info("Guest trying to create users")
|
||||||
|
for user in api_user_data:
|
||||||
|
resp = api_client.create_user(user)
|
||||||
|
assert resp.status_code == 401, \
|
||||||
|
f"Guest should not be able to create users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
@allure.title("Guest: Update user - should fail")
|
||||||
|
def test_guest_users_update(self, api_client: APIClient, list_of_users):
|
||||||
|
users = list_of_users
|
||||||
|
|
||||||
|
with allure.step("Guest trying to change users data"):
|
||||||
|
for user in users:
|
||||||
|
logger.info(f"Guest changing user: {user['username']}")
|
||||||
|
user["motto"] = "Changed by guest"
|
||||||
|
resp = api_client.update_user(user["id"], user)
|
||||||
|
assert resp.status_code == 401, \
|
||||||
|
f"Guest should not be able to update users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
@allure.title("Guest: Deleting users - should fail")
|
||||||
|
def test_guest_users_deleting(self, api_client: APIClient, list_of_users):
|
||||||
|
users = list_of_users
|
||||||
|
|
||||||
|
with allure.step("Guest trying to delete users"):
|
||||||
|
for user in users:
|
||||||
|
logger.info(f"Guest deleting user: {user['username']}")
|
||||||
|
resp = api_client.delete_user(user["id"])
|
||||||
|
assert resp.status_code == 401, \
|
||||||
|
f"Guest should not be able to delete users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
|
||||||
|
# Тест для администратора
|
||||||
|
@allure.story("Admin permissions")
|
||||||
|
class TestAdminUsers:
|
||||||
|
"""Тестирование операций с пользователями под администратором"""
|
||||||
|
|
||||||
|
@allure.title("Admin: Creating new user - should succeed")
|
||||||
|
def test_admin_users_creating(self, auth_admin, api_user_data):
|
||||||
|
with allure.step("Logged as admin - creating users"):
|
||||||
|
logger.info("Admin creating users")
|
||||||
|
for user in api_user_data:
|
||||||
|
resp = auth_admin.create_user(user)
|
||||||
|
assert resp.status_code == 201, \
|
||||||
|
f"Admin should be able to create users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
@allure.title("Admin: Update user - should succeed")
|
||||||
|
def test_admin_users_update(self, auth_admin, list_of_users):
|
||||||
|
users = list_of_users
|
||||||
|
|
||||||
|
with allure.step("Admin changing users data"):
|
||||||
|
for user in users:
|
||||||
|
logger.info(f"Admin changing user: {user['username']}")
|
||||||
|
user["motto"] = "Changed by admin"
|
||||||
|
user["password"] = "SomeRandomPass1205"
|
||||||
|
resp = auth_admin.update_user(user["id"], user)
|
||||||
|
assert resp.status_code == 200, \
|
||||||
|
f"Admin should be able to update users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
@allure.title("Admin: Deleting users - should succeed")
|
||||||
|
def test_admin_users_deleting(self, auth_admin, list_of_users):
|
||||||
|
users = list_of_users
|
||||||
|
|
||||||
|
with allure.step("Admin deleting users"):
|
||||||
|
for user in users:
|
||||||
|
logger.info(f"Admin deleting user: {user['username']}")
|
||||||
|
resp = auth_admin.delete_user(user["id"])
|
||||||
|
assert resp.status_code == 200, \
|
||||||
|
f"Admin should be able to delete users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
|
||||||
|
# Тест для обычного пользователя
|
||||||
|
@allure.story("Regular user permissions")
|
||||||
|
class TestRegularUserUsers:
|
||||||
|
"""Тестирование операций с пользователями под обычным пользователем"""
|
||||||
|
|
||||||
|
@allure.title("User: Creating new user - should succeed")
|
||||||
|
def test_user_users_creating(self, auth_user, api_user_data):
|
||||||
|
with allure.step("Logged as regular user - creating users"):
|
||||||
|
logger.info("Regular user creating users")
|
||||||
|
for user in api_user_data:
|
||||||
|
resp = auth_user.create_user(user)
|
||||||
|
assert resp.status_code == 201, \
|
||||||
|
f"Regular user should be able to create users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
@allure.title("User: Update user - should succeed")
|
||||||
|
def test_user_users_update(self, auth_user, list_of_users):
|
||||||
|
users = list_of_users
|
||||||
|
|
||||||
|
with allure.step("Regular user changing users data"):
|
||||||
|
for user in users:
|
||||||
|
logger.info(f"Regular user changing: {user['username']}")
|
||||||
|
user["motto"] = "Changed by regular user"
|
||||||
|
user["password"] = "SomeRandomPass1205"
|
||||||
|
resp = auth_user.update_user(user["id"], user)
|
||||||
|
assert resp.status_code == 200, \
|
||||||
|
f"Regular user should be able to update users. Got {resp.status_code}"
|
||||||
|
|
||||||
|
@allure.title("User: Deleting users - should succeed")
|
||||||
|
def test_user_users_deleting(self, auth_user, list_of_users):
|
||||||
|
users = list_of_users
|
||||||
|
|
||||||
|
with allure.step("Regular user deleting users"):
|
||||||
|
for user in users:
|
||||||
|
logger.info(f"Regular user deleting: {user['username']}")
|
||||||
|
resp = auth_user.delete_user(user["id"])
|
||||||
|
assert resp.status_code == 200, \
|
||||||
|
f"Regular user should be able to delete users. Got {resp.status_code}"
|
||||||
213
tests/api/utils/api_client.py
Normal file
213
tests/api/utils/api_client.py
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import allure
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from config.api_config import api_config
|
||||||
|
from config.session_config import session_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class APIClient:
|
||||||
|
def __init__(self, base_url: Optional[str] = None):
|
||||||
|
self.base_url = base_url or api_config.API_BASE_URL
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.cookies.clear()
|
||||||
|
self.logged_in = False
|
||||||
|
logger.info("New API created.")
|
||||||
|
|
||||||
|
# Настройка сессии
|
||||||
|
self._configure_session()
|
||||||
|
|
||||||
|
def _configure_session(self):
|
||||||
|
"""Настройка HTTP сессии"""
|
||||||
|
# Установка заголовков по умолчанию
|
||||||
|
self.session.headers.update(api_config.DEFAULT_HEADERS)
|
||||||
|
self.session.headers.update(session_config.get_session_headers())
|
||||||
|
|
||||||
|
logger.debug(f"API Client configured for {self.base_url}")
|
||||||
|
|
||||||
|
def login(self, username: str, password: str) -> bool:
|
||||||
|
"""Логин пользователя (создание сессии)"""
|
||||||
|
login_data = {
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint = api_config.get_endpoint('auth_login')
|
||||||
|
response = self.post(endpoint, json=login_data)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
self.logged_in = True
|
||||||
|
logger.info(f"Successfully logged in as {username}")
|
||||||
|
|
||||||
|
# Логирование cookies для отладки
|
||||||
|
if self.session.cookies:
|
||||||
|
cookies_info = dict(self.session.cookies)
|
||||||
|
logger.info(f"Session cookies: {cookies_info}")
|
||||||
|
|
||||||
|
# Проверяем наличие сессионного cookie
|
||||||
|
session_cookie = session_config.SESSION_COOKIE_NAME
|
||||||
|
if session_cookie in cookies_info:
|
||||||
|
logger.info(f"Session cookie '{session_cookie}' is set")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Login failed for {username}: {response.status_code}")
|
||||||
|
logger.debug(f"Response: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def logout(self) -> bool:
|
||||||
|
"""Выход из системы (закрытие сессии)"""
|
||||||
|
if not self.logged_in:
|
||||||
|
logger.warning("Attempted logout without being logged in")
|
||||||
|
return True
|
||||||
|
|
||||||
|
endpoint = api_config.get_endpoint('auth_logout')
|
||||||
|
response = self.get(endpoint)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
self.logged_in = False
|
||||||
|
|
||||||
|
# Очищаем cookies
|
||||||
|
self.session.cookies.clear()
|
||||||
|
|
||||||
|
logger.info("Successfully logged out")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Logout failed: {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ensure_logged_in(self, username: str, password: str):
|
||||||
|
"""Убедиться, что пользователь залогинен"""
|
||||||
|
if not self.logged_in:
|
||||||
|
return self.login(username, password)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _send_request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
endpoint: str,
|
||||||
|
**kwargs
|
||||||
|
) -> requests.Response:
|
||||||
|
"""Отправка HTTP запроса с поддержкой сессий"""
|
||||||
|
url = urljoin(self.base_url, endpoint)
|
||||||
|
|
||||||
|
with allure.step(f"{method.upper()} {endpoint}"):
|
||||||
|
# Добавляем логирование
|
||||||
|
self._log_request(method, url, kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.request(method, url, headers=api_config.DEFAULT_HEADERS, **kwargs)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Request failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Логирование ответа
|
||||||
|
self._log_response(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _log_request(self, method: str, url: str, kwargs: Dict[str, Any]):
|
||||||
|
"""Логирование деталей запроса"""
|
||||||
|
request_details = (
|
||||||
|
f"Request: {method} {url}\n"
|
||||||
|
f"Headers: {dict(self.session.headers)}\n"
|
||||||
|
f"Cookies: {dict(self.session.cookies)}\n"
|
||||||
|
f"Body: {kwargs.get('json', 'N/A')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
allure.attach(
|
||||||
|
request_details,
|
||||||
|
name="Request Details",
|
||||||
|
attachment_type=allure.attachment_type.TEXT
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"{method} {url}")
|
||||||
|
|
||||||
|
def _log_response(self, response: requests.Response):
|
||||||
|
"""Логирование деталей ответа"""
|
||||||
|
response_details = (
|
||||||
|
f"Status Code: {response.status_code}\n"
|
||||||
|
f"Response Headers: {dict(response.headers)}\n"
|
||||||
|
f"Response Cookies: {dict(response.cookies)}\n"
|
||||||
|
f"Response Body: {response.text[:500]}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
allure.attach(
|
||||||
|
response_details,
|
||||||
|
name="Response Details",
|
||||||
|
attachment_type=allure.attachment_type.TEXT
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"Response: {response.status_code}")
|
||||||
|
|
||||||
|
# Логирование ошибок
|
||||||
|
if response.status_code >= 400:
|
||||||
|
logger.warning(f"Request failed with status {response.status_code}")
|
||||||
|
logger.debug(f"Error response: {response.text}")
|
||||||
|
|
||||||
|
# Методы HTTP запросов
|
||||||
|
def get(self, endpoint: str, **kwargs) -> requests.Response:
|
||||||
|
return self._send_request("GET", endpoint, **kwargs)
|
||||||
|
|
||||||
|
def post(self, endpoint: str, **kwargs) -> requests.Response:
|
||||||
|
return self._send_request("POST", endpoint, **kwargs)
|
||||||
|
|
||||||
|
def put(self, endpoint: str, **kwargs) -> requests.Response:
|
||||||
|
return self._send_request("PUT", endpoint, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, endpoint: str, **kwargs) -> requests.Response:
|
||||||
|
return self._send_request("DELETE", endpoint, **kwargs)
|
||||||
|
|
||||||
|
def patch(self, endpoint: str, **kwargs) -> requests.Response:
|
||||||
|
return self._send_request("PATCH", endpoint, **kwargs)
|
||||||
|
|
||||||
|
# Специальные методы для endpoints
|
||||||
|
def create_user(self, user_data: Dict[str, Any]) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('users_create')
|
||||||
|
return self.post(endpoint, json=user_data)
|
||||||
|
|
||||||
|
def get_user(self, user_id: str) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('users_get_by_id', id=user_id)
|
||||||
|
return self.get(endpoint)
|
||||||
|
|
||||||
|
def get_all_users(self) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('users_get_all')
|
||||||
|
return self.get(endpoint)
|
||||||
|
|
||||||
|
def update_user(self, user_id: str, user_data: Dict[str, Any]) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('users_update', id=user_id)
|
||||||
|
return self.put(endpoint, json=user_data)
|
||||||
|
|
||||||
|
def delete_user(self, user_id: str) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('users_delete', id=user_id)
|
||||||
|
return self.delete(endpoint)
|
||||||
|
|
||||||
|
def create_post(self, post_data: Dict[str, Any]) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('posts_create')
|
||||||
|
return self.post(endpoint, json=post_data)
|
||||||
|
|
||||||
|
def get_post(self, post_id: str) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('posts_get_by_id', id=post_id)
|
||||||
|
return self.get(endpoint)
|
||||||
|
|
||||||
|
def get_all_posts(self) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('posts_get_all')
|
||||||
|
return self.get(endpoint)
|
||||||
|
|
||||||
|
def update_post(self, post_id: str, post_data: Dict[str, Any]) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('posts_update', id=post_id)
|
||||||
|
return self.put(endpoint, json=post_data)
|
||||||
|
|
||||||
|
def delete_post(self, post_id: str) -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint("posts_delete", id=post_id)
|
||||||
|
return self.delete(endpoint)
|
||||||
|
|
||||||
|
def upload_image(self, file_path: str, field_name: str = "file") -> requests.Response:
|
||||||
|
endpoint = api_config.get_endpoint('images_upload')
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
files = {field_name: (file_path, f)}
|
||||||
|
return self.post(endpoint, files=files)
|
||||||
106
tests/conftest.py
Normal file
106
tests/conftest.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
from config.settings import settings
|
||||||
|
from config.api_config import api_config
|
||||||
|
from config.ui_config import ui_config
|
||||||
|
from fixtures.data_fixtures import *
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Вывод информации о конфигурации
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info(f"Running tests in {settings.ENV} environment")
|
||||||
|
logger.info(f"API Base URL: {api_config.API_BASE_URL}")
|
||||||
|
logger.info(f"UI Base URL: {ui_config.UI_BASE_URL}")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
"""Добавление кастомных опций командной строки"""
|
||||||
|
parser.addoption(
|
||||||
|
"--env-file",
|
||||||
|
action="store",
|
||||||
|
default=None,
|
||||||
|
help="Path to custom .env file"
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--browser",
|
||||||
|
action="store",
|
||||||
|
default=ui_config.BROWSER_NAME,
|
||||||
|
help="Browser to use for UI tests"
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--headless",
|
||||||
|
action="store_true",
|
||||||
|
default=ui_config.BROWSER_HEADLESS,
|
||||||
|
help="Run browser in headless mode"
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--api-url",
|
||||||
|
action="store",
|
||||||
|
default=api_config.API_BASE_URL,
|
||||||
|
help="Override API base URL"
|
||||||
|
)
|
||||||
|
parser.addoption(
|
||||||
|
"--ui-url",
|
||||||
|
action="store",
|
||||||
|
default=ui_config.UI_BASE_URL,
|
||||||
|
help="Override UI base URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
"""Конфигурация pytest"""
|
||||||
|
# Переопределение настроек из командной строки
|
||||||
|
if config.getoption("--api-url"):
|
||||||
|
api_config.API_BASE_URL = config.getoption("--api-url")
|
||||||
|
|
||||||
|
if config.getoption("--ui-url"):
|
||||||
|
ui_config.UI_BASE_URL = config.getoption("--ui-url")
|
||||||
|
|
||||||
|
if config.getoption("--browser"):
|
||||||
|
ui_config.BROWSER_NAME = config.getoption("--browser")
|
||||||
|
|
||||||
|
if config.getoption("--headless") is not None:
|
||||||
|
ui_config.BROWSER_HEADLESS = config.getoption("--headless")
|
||||||
|
|
||||||
|
# Настройка маркеров
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers",
|
||||||
|
"smoke: Smoke tests - critical functionality"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers",
|
||||||
|
"regression: Regression tests - full functionality"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers",
|
||||||
|
"api: API tests"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers",
|
||||||
|
"ui: UI tests"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers",
|
||||||
|
"slow: Slow running tests"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def env_config():
|
||||||
|
"""Конфигурация окружения"""
|
||||||
|
return {
|
||||||
|
"env": settings.ENV,
|
||||||
|
"debug": settings.DEBUG,
|
||||||
|
"api_url": api_config.API_BASE_URL,
|
||||||
|
"ui_url": ui_config.UI_BASE_URL,
|
||||||
|
"log_level": settings.LOG_LEVEL,
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def api_base_url():
|
||||||
|
"""Базовый URL API"""
|
||||||
|
return api_config.API_BASE_URL
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def ui_base_url():
|
||||||
|
"""Базовый URL UI"""
|
||||||
|
return ui_config.UI_BASE_URL
|
||||||
38
tests/data/api_posts.json
Normal file
38
tests/data/api_posts.json
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"category": "Web Development",
|
||||||
|
"content": "Разбираем основы React.js: компоненты, состояние и пропсы. Создадим простое приложение-список задач, чтобы понять, как работает виртуальный DOM и управление состоянием.",
|
||||||
|
"description": "Введение в React.js для начинающих",
|
||||||
|
"tags": ["React", "фронтенд", "JavaScript"],
|
||||||
|
"title": "Первый проект на React.js: список задач"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Cybersecurity",
|
||||||
|
"content": "Обзор основных методов защиты веб‑приложений: HTTPS, CSP, защита от SQL‑инъекций и XSS. Приведём примеры кода для безопасной обработки пользовательских данных.",
|
||||||
|
"description": "Основы безопасности веб‑приложений",
|
||||||
|
"tags": ["безопасность", "веб", "защита данных"],
|
||||||
|
"title": "Как защитить веб‑приложение: 5 ключевых методов"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Cloud Computing",
|
||||||
|
"content": "Сравниваем AWS, Google Cloud и Azure: цены, сервисы и сценарии использования. Разберём, как выбрать облачную платформу для стартапа и крупного бизнеса.",
|
||||||
|
"description": "Выбор облачного провайдера: сравнение",
|
||||||
|
"tags": ["облако", "AWS", "Google Cloud", "Azure"],
|
||||||
|
"title": "AWS vs Google Cloud vs Azure: что выбрать?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DevOps",
|
||||||
|
"content": "Настройка CI/CD с GitHub Actions: автоматизируем тестирование и деплой. Покажем, как создать пайплайн для Node.js‑приложения за 10 минут.",
|
||||||
|
"description": "CI/CD на практике с GitHub Actions",
|
||||||
|
"tags": ["DevOps", "CI/CD", "GitHub Actions"],
|
||||||
|
"title": "Автоматизация сборки и деплоя: GitHub Actions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Mobile Development",
|
||||||
|
"content": "Создаём кросс‑платформенное приложение на Flutter: от установки SDK до первого экрана. Разберём архитектуру и преимущества Flutter перед Native разработкой.",
|
||||||
|
"description": "Начало работы с Flutter",
|
||||||
|
"tags": ["Flutter", "мобильная разработка", "кросс‑платформа"],
|
||||||
|
"title": "Flutter: пишем первое мобильное приложение"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
13
tests/data/api_user_auth.json
Normal file
13
tests/data/api_user_auth.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"avatar": "🔍",
|
||||||
|
"description": "QA‑инженер с фокусом на автоматизированное тестирование и CI/CD.",
|
||||||
|
"joinDate": "2023-10-01T20:00:00Z",
|
||||||
|
"motto": "Тестирование — это поиск истины.",
|
||||||
|
"name": "Андрей Васильев",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Web App Testing", "API Automation"],
|
||||||
|
"role": "QA Engineer",
|
||||||
|
"skills": ["Selenium", "JUnit", "Postman", "CI/CD", "TestRail"],
|
||||||
|
"speciality": "Good user",
|
||||||
|
"username": "andrey_user"
|
||||||
|
}
|
||||||
132
tests/data/api_users.json
Normal file
132
tests/data/api_users.json
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"avatar": "🧑",
|
||||||
|
"description": "Опытный разработчик Full‑Stack с 8‑летним стажем. Специализируется на веб‑приложениях и микросервисах.",
|
||||||
|
"joinDate": "2023-05-12T12:00:00Z",
|
||||||
|
"motto": "Код — это поэзия логики.",
|
||||||
|
"name": "Алексей Петров",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["E‑commerce Platform", "CRM System", "Task Manager"],
|
||||||
|
"role": "Senior Developer",
|
||||||
|
"skills": ["JavaScript", "Python", "Docker", "Kubernetes", "PostgreSQL"],
|
||||||
|
"speciality": "Web Development",
|
||||||
|
"username": "alex_dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "👩",
|
||||||
|
"description": "UX/UI‑дизайнер с фокусом на пользовательский опыт и адаптивные интерфейсы.",
|
||||||
|
"joinDate": "2022-11-03T11:00:00Z",
|
||||||
|
"motto": "Дизайн — это не про красоту, а про удобство.",
|
||||||
|
"name": "Мария Иванова",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Mobile Banking App", "E‑learning Platform"],
|
||||||
|
"role": "UI/UX Designer",
|
||||||
|
"skills": ["Figma", "Adobe XD", "User Research", "Prototyping"],
|
||||||
|
"speciality": "User Experience",
|
||||||
|
"username": "maria_design"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "👨💻",
|
||||||
|
"description": "Системный администратор с экспертизой в облачных решениях и кибербезопасности.",
|
||||||
|
"joinDate": "2024-01-20T10:00:00Z",
|
||||||
|
"motto": "Безопасность — не опция, а необходимость.",
|
||||||
|
"name": "Дмитрий Соколов",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Cloud Migration", "Network Security Audit"],
|
||||||
|
"role": "SysAdmin",
|
||||||
|
"skills": ["AWS", "Azure", "Linux", "Firewall", "VPN"],
|
||||||
|
"speciality": "Cloud & Security",
|
||||||
|
"username": "dmitry_sys"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "👩💼",
|
||||||
|
"description": "Менеджер проектов с опытом ведения кросс‑функциональных команд в IT‑сфере.",
|
||||||
|
"joinDate": "2023-08-15T14:12:00Z",
|
||||||
|
"motto": "Сроки — святое, но качество — важнее.",
|
||||||
|
"name": "Анна Кузнецова",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["ERP Implementation", "Agile Transformation"],
|
||||||
|
"role": "Project Manager",
|
||||||
|
"skills": ["Agile", "Scrum", "Jira", "Stakeholder Management"],
|
||||||
|
"speciality": "Project Management",
|
||||||
|
"username": "anna_pm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "🤖",
|
||||||
|
"description": "Специалист по машинному обучению и анализу данных. Работает с NLP и компьютерным зрением.",
|
||||||
|
"joinDate": "2024-03-10T16:34:00Z",
|
||||||
|
"motto": "Данные — новая нефть, а ML — двигатель.",
|
||||||
|
"name": "Иван Морозов",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Chatbot Development", "Image Recognition System"],
|
||||||
|
"role": "ML Engineer",
|
||||||
|
"skills": ["Python", "TensorFlow", "PyTorch", "NLP", "Data Visualization"],
|
||||||
|
"speciality": "Machine Learning",
|
||||||
|
"username": "ivan_ml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "🎨",
|
||||||
|
"description": "Графический дизайнер, создаёт брендовые стили и маркетинговые материалы.",
|
||||||
|
"joinDate": "2023-06-22T12:44:00Z",
|
||||||
|
"motto": "Каждый пиксель имеет значение.",
|
||||||
|
"name": "Елена Волкова",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Brand Identity", "Social Media Graphics"],
|
||||||
|
"role": "Graphic Designer",
|
||||||
|
"skills": ["Photoshop", "Illustrator", "InDesign", "Branding"],
|
||||||
|
"speciality": "Graphic Design",
|
||||||
|
"username": "elena_art"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "📊",
|
||||||
|
"description": "Аналитик данных с опытом работы в BI‑инструментах и SQL.",
|
||||||
|
"joinDate": "2022-09-05T14:32:00Z",
|
||||||
|
"motto": "Факты говорят громче слов.",
|
||||||
|
"name": "Сергей Новиков",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Sales Dashboard", "Customer Churn Analysis"],
|
||||||
|
"role": "Data Analyst",
|
||||||
|
"skills": ["SQL", "Power BI", "Excel", "Tableau", "Python"],
|
||||||
|
"speciality": "Data Analytics",
|
||||||
|
"username": "sergey_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "📱",
|
||||||
|
"description": "Мобильный разработчик под iOS и Android. Специализируется на кросс‑платформенных решениях.",
|
||||||
|
"joinDate": "2024-02-18T18:23:00Z",
|
||||||
|
"motto": "Приложения — мосты между людьми и технологиями.",
|
||||||
|
"name": "Ольга Фёдорова",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Fitness App", "Delivery Service"],
|
||||||
|
"role": "Mobile Developer",
|
||||||
|
"skills": ["Swift", "Kotlin", "Flutter", "React Native"],
|
||||||
|
"speciality": "Mobile Development",
|
||||||
|
"username": "olga_mobile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "🔍",
|
||||||
|
"description": "QA‑инженер с фокусом на автоматизированное тестирование и CI/CD.",
|
||||||
|
"joinDate": "2023-10-01T20:00:00Z",
|
||||||
|
"motto": "Тестирование — это поиск истины.",
|
||||||
|
"name": "Андрей Васильев",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Web App Testing", "API Automation"],
|
||||||
|
"role": "QA Engineer",
|
||||||
|
"skills": ["Selenium", "JUnit", "Postman", "CI/CD", "TestRail"],
|
||||||
|
"speciality": "Quality Assurance",
|
||||||
|
"username": "andrey_qa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"avatar": "📚",
|
||||||
|
"description": "Технический писатель, создаёт документацию и гайды для сложных систем.",
|
||||||
|
"joinDate": "2024-04-14T16:43:00Z",
|
||||||
|
"motto": "Ясность — ключ к пониманию.",
|
||||||
|
"name": "Наталья Смирнова",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["API Documentation", "User Manual for ERP"],
|
||||||
|
"role": "Technical Writer",
|
||||||
|
"skills": ["Markdown", "Confluence", "Diagrams", "API Docs"],
|
||||||
|
"speciality": "Technical Documentation",
|
||||||
|
"username": "natalia_docs"
|
||||||
|
}
|
||||||
|
]
|
||||||
0
tests/fixtures/__init__.py
vendored
Normal file
0
tests/fixtures/__init__.py
vendored
Normal file
24
tests/fixtures/data_fixtures.py
vendored
Normal file
24
tests/fixtures/data_fixtures.py
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def _load_data(filename: str):
|
||||||
|
current_file = Path(__file__).resolve()
|
||||||
|
data_path = current_file.parent.parent.joinpath("data")
|
||||||
|
data_file_path = data_path.joinpath(filename)
|
||||||
|
with open(data_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def api_user_data():
|
||||||
|
return _load_data('api_users.json')
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def api_post_data():
|
||||||
|
return _load_data('api_posts.json')
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def api_user_auth_data():
|
||||||
|
return _load_data('api_user_auth.json')
|
||||||
15
tests/pytest.ini
Normal file
15
tests/pytest.ini
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[pytest]
|
||||||
|
testpaths = tests
|
||||||
|
pythonpath = ../
|
||||||
|
addopts =
|
||||||
|
-v
|
||||||
|
--strict-markers
|
||||||
|
--alluredir=reports/allure-results/
|
||||||
|
--clean-alluredir
|
||||||
|
-p no:warnings
|
||||||
|
markers =
|
||||||
|
smoke: Smoke tests
|
||||||
|
regression: Regression tests
|
||||||
|
api: API tests
|
||||||
|
ui: UI tests
|
||||||
|
slow: Slow running tests
|
||||||
0
tests/ui/__init__.py
Normal file
0
tests/ui/__init__.py
Normal file
130
tests/ui/conftest.py
Normal file
130
tests/ui/conftest.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import logging
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from config.ui_config import UIConfig
|
||||||
|
from tests.api.utils.api_client import APIClient
|
||||||
|
from utils import waiters
|
||||||
|
|
||||||
|
from api.utils.api_client import APIClient
|
||||||
|
from api.conftest import *
|
||||||
|
from fixtures.data_fixtures import *
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def driver(request):
|
||||||
|
"""Фикстура для браузера"""
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("Initializing webdriver")
|
||||||
|
|
||||||
|
headless = UIConfig.BROWSER_HEADLESS
|
||||||
|
browser_name = UIConfig.BROWSER_NAME
|
||||||
|
fullscreen = UIConfig.BROWSER_FULLSCREEN
|
||||||
|
|
||||||
|
logger.info(f"Headless: {headless}")
|
||||||
|
logger.info("Browser: {browser_name}")
|
||||||
|
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
if browser_name == "chrome":
|
||||||
|
options = Options()
|
||||||
|
if headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
if fullscreen:
|
||||||
|
options.add_argument("--start-maximized")
|
||||||
|
options.add_argument("--no-sandbox")
|
||||||
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
|
options.add_argument("--window-size=1920,1080")
|
||||||
|
|
||||||
|
driver = webdriver.Chrome(
|
||||||
|
options=options
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
options = webdriver.FirefoxOptions()
|
||||||
|
if headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
if fullscreen:
|
||||||
|
options.add_argument("--kiosk")
|
||||||
|
options.add_argument("--width=1920")
|
||||||
|
options.add_argument("--height=1080")
|
||||||
|
driver = webdriver.Firefox(
|
||||||
|
options=options
|
||||||
|
)
|
||||||
|
|
||||||
|
driver.implicitly_wait(10)
|
||||||
|
driver.maximize_window()
|
||||||
|
|
||||||
|
waiters.waiter = WebDriverWait(driver, 10)
|
||||||
|
|
||||||
|
def fin():
|
||||||
|
if request.node.rep_call.failed:
|
||||||
|
try:
|
||||||
|
# Делаем скриншот при падении теста
|
||||||
|
screenshot = driver.get_screenshot_as_png()
|
||||||
|
allure.attach(
|
||||||
|
screenshot,
|
||||||
|
name=f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем текущий URL
|
||||||
|
current_url = driver.current_url
|
||||||
|
allure.attach(
|
||||||
|
current_url,
|
||||||
|
name="current_url",
|
||||||
|
attachment_type=allure.attachment_type.TEXT
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем исходный код страницы
|
||||||
|
page_source = driver.page_source
|
||||||
|
allure.attach(
|
||||||
|
page_source,
|
||||||
|
name="page_source",
|
||||||
|
attachment_type=allure.attachment_type.HTML
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
request.addfinalizer(fin)
|
||||||
|
|
||||||
|
yield driver
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
"""
|
||||||
|
Хук для получения результатов теста
|
||||||
|
"""
|
||||||
|
outcome = yield
|
||||||
|
rep = outcome.get_result()
|
||||||
|
setattr(item, "rep_" + rep.when, rep)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def gen_data_fixture(auth_admin: APIClient, api_user_data, api_post_data):
|
||||||
|
with allure.step("Generating data"):
|
||||||
|
id = ''
|
||||||
|
user = api_user_data[0]
|
||||||
|
resp = auth_admin.create_user(user)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
id = resp.json()['id']
|
||||||
|
|
||||||
|
post = api_post_data[0]
|
||||||
|
post['userId'] = id
|
||||||
|
auth_admin.create_post(post)
|
||||||
|
yield
|
||||||
|
|
||||||
|
with allure.step("Deleting generated data"):
|
||||||
|
posts = auth_admin.get_all_posts().json()
|
||||||
|
users = auth_admin.get_all_users().json()
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
auth_admin.delete_post(post['id'])
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
auth_admin.delete_user(user['id'])
|
||||||
0
tests/ui/pages/__init__.py
Normal file
0
tests/ui/pages/__init__.py
Normal file
134
tests/ui/pages/admin_page.py
Normal file
134
tests/ui/pages/admin_page.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import allure
|
||||||
|
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from tests.ui.pages.base_page import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPage(BasePage):
|
||||||
|
# Локаторы
|
||||||
|
|
||||||
|
# Логин
|
||||||
|
LOGIN_BLOCK = (By.XPATH, "//div[contains(@id, 'operations-user-post_login')]//button[contains(@class, 'opblock-summary-control')]")
|
||||||
|
LOGIN_TRYITOUT_BTN = (By.XPATH, "//div[contains(@id, 'operations-user-post_login')]//button[contains(@class, 'try-out__btn')]")
|
||||||
|
LOGIN_TEXTAREA = (By.XPATH, "//div[contains(@id, 'operations-user-post_login')]//textarea[contains(@class, 'body-param__text')]")
|
||||||
|
LOGIN_EXECUTE_BTN = (By.XPATH, "//div[contains(@id, 'operations-user-post_login')]//button[contains(@class, 'execute')]")
|
||||||
|
LOGIN_STATUS_CODE = (By.XPATH, "//div[contains(@id, 'operations-user-post_login')]//table[contains(@class, 'live-responses-table')]//tbody//td[contains(@class, 'response-col_status')]")
|
||||||
|
|
||||||
|
# Пользователь
|
||||||
|
USER_BLOCK = (By.XPATH, "//div[contains(@id, 'operations-user-post_team_')]//button[contains(@class, 'opblock-summary-control')]")
|
||||||
|
USER_TRYITOUT_BTN = (By.XPATH, "//div[contains(@id, 'operations-user-post_team_')]//button[contains(@class, 'try-out__btn')]")
|
||||||
|
USER_TEXTAREA = (By.XPATH, "//div[contains(@id, 'operations-user-post_team_')]//textarea[contains(@class, 'body-param__text')]")
|
||||||
|
USER_EXECUTE_BTN = (By.XPATH, "//div[contains(@id, 'operations-user-post_team_')]//button[contains(@class, 'execute')]")
|
||||||
|
USER_RESPONSE = (By.XPATH, "//div[contains(@id, 'operations-user-post_team_')]//table[contains(@class, 'live-responses-table')]//tbody//td[contains(@class, 'response-col_description')]//code")
|
||||||
|
USER_STATUS_CODE = (By.XPATH, "//div[contains(@id, 'operations-user-post_team_')]//table[contains(@class, 'live-responses-table')]//tbody//td[contains(@class, 'response-col_status')]")
|
||||||
|
|
||||||
|
# Данные для входа
|
||||||
|
LOGIN_DATA = [Keys.BACKSPACE, Keys.ENTER, '\t"password": "Abc1205",', Keys.ENTER, '\t"username": "muts"', Keys.ENTER, '}']
|
||||||
|
USER_RAW_DATA = """
|
||||||
|
"avatar": "🔍",
|
||||||
|
"description": "QA‑инженер с фокусом на автоматизированное тестирование и CI/CD.",
|
||||||
|
"joinDate": "2023-10-01T20:00:00Z",
|
||||||
|
"motto": "Тестирование — это поиск истины.",
|
||||||
|
"name": "Андрей Васильев",
|
||||||
|
"password": "TestPas1205",
|
||||||
|
"projects": ["Web App Testing", "API Automation"],
|
||||||
|
"role": "QA Engineer",
|
||||||
|
"skills": ["Selenium", "JUnit", "Postman", "CI/CD", "TestRail"],
|
||||||
|
"speciality": "Good user",
|
||||||
|
"username": "andrey_user"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
USER_DATA = [Keys.BACKSPACE, USER_RAW_DATA]
|
||||||
|
|
||||||
|
def __init__(self, driver: WebDriver):
|
||||||
|
super().__init__(driver)
|
||||||
|
|
||||||
|
@allure.step('Открытие страницы администратора')
|
||||||
|
def open_admin_page(self):
|
||||||
|
self.open('http://localhost:8080/swagger/index.html')
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия блока логина')
|
||||||
|
def is_login_block_visible(self):
|
||||||
|
return self.is_visible(self.LOGIN_BLOCK)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия кнопки "try it out"')
|
||||||
|
def is_login_tryitout_btn_visible(self):
|
||||||
|
return self.is_visible(self.LOGIN_TRYITOUT_BTN)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия поля ввода')
|
||||||
|
def is_login_textarea_visible(self):
|
||||||
|
return self.is_visible(self.LOGIN_TEXTAREA)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия кнопки выполнения запроса')
|
||||||
|
def is_login_execute_btn_visible(self):
|
||||||
|
return self.is_visible(self.LOGIN_EXECUTE_BTN)
|
||||||
|
|
||||||
|
@allure.step('Нажатие на блок логина')
|
||||||
|
def click_login_block(self):
|
||||||
|
self.click(self.LOGIN_BLOCK)
|
||||||
|
|
||||||
|
@allure.step('Нажатие на кнопку "try it out"')
|
||||||
|
def click_login_tryitout_btn(self):
|
||||||
|
self.click(self.LOGIN_TRYITOUT_BTN)
|
||||||
|
|
||||||
|
@allure.step('Ввод текста в поле логина')
|
||||||
|
def enter_text_to_login_textarea(self, text: list):
|
||||||
|
field = self.find_element(self.LOGIN_TEXTAREA)
|
||||||
|
field.clear()
|
||||||
|
|
||||||
|
for event in text:
|
||||||
|
field.send_keys(event)
|
||||||
|
|
||||||
|
@allure.step('Нажатие на кнопку выполнения запроса')
|
||||||
|
def click_login_execute_btn(self):
|
||||||
|
self.click(self.LOGIN_EXECUTE_BTN)
|
||||||
|
|
||||||
|
@allure.step('Получение статуса выполнения входа')
|
||||||
|
def get_login_status(self):
|
||||||
|
return self.get_text(self.LOGIN_STATUS_CODE)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия блока пользователя')
|
||||||
|
def is_user_block_visible(self):
|
||||||
|
return self.is_visible(self.USER_BLOCK)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия кнопки "try it out"')
|
||||||
|
def is_user_tryitout_btn_visible(self):
|
||||||
|
return self.is_visible(self.USER_TRYITOUT_BTN)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия поля ввода')
|
||||||
|
def is_user_textarea_visible(self):
|
||||||
|
return self.is_visible(self.USER_TEXTAREA)
|
||||||
|
|
||||||
|
@allure.step('Проверка наличия кнопки выполнения запроса')
|
||||||
|
def is_user_execute_btn_visible(self):
|
||||||
|
return self.is_visible(self.USER_EXECUTE_BTN)
|
||||||
|
|
||||||
|
@allure.step('Нажатие на блок пользователя')
|
||||||
|
def click_user_block(self):
|
||||||
|
self.click(self.USER_BLOCK)
|
||||||
|
|
||||||
|
@allure.step('Нажатие на кнопку "try it out"')
|
||||||
|
def click_user_tryitout_btn(self):
|
||||||
|
self.click(self.USER_TRYITOUT_BTN)
|
||||||
|
|
||||||
|
@allure.step('Ввод текста в поле добавления пользователя')
|
||||||
|
def enter_text_to_user_textarea(self, text: list):
|
||||||
|
field = self.find_element(self.USER_TEXTAREA)
|
||||||
|
field.clear()
|
||||||
|
|
||||||
|
for event in text:
|
||||||
|
field.send_keys(event)
|
||||||
|
|
||||||
|
@allure.step('Нажатие на кнопку выполнения запроса')
|
||||||
|
def click_user_execute_btn(self):
|
||||||
|
self.click(self.USER_EXECUTE_BTN)
|
||||||
|
|
||||||
|
@allure.step('Получение ответа на запрос создания пользователя')
|
||||||
|
def get_user_response(self):
|
||||||
|
elem = self.find_element(self.USER_RESPONSE)
|
||||||
|
return elem.get_attribute('innerText')
|
||||||
|
|
||||||
|
@allure.step('Получение статуса создания пользователя')
|
||||||
|
def get_user_status(self):
|
||||||
|
return self.get_text(self.USER_STATUS_CODE)
|
||||||
99
tests/ui/pages/base_page.py
Normal file
99
tests/ui/pages/base_page.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
from selenium.webdriver import ActionChains
|
||||||
|
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
||||||
|
import allure
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePage:
|
||||||
|
def __init__(self, driver: WebDriver):
|
||||||
|
self.driver = driver
|
||||||
|
self.wait = WebDriverWait(driver, 10)
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
@allure.step("Открыть URL: {url}")
|
||||||
|
def open(self, url):
|
||||||
|
self.driver.get(url)
|
||||||
|
self.logger.info(f"Открыта страница: {url}")
|
||||||
|
|
||||||
|
@allure.step("Обновить страницу")
|
||||||
|
def refresh(self):
|
||||||
|
self.driver.refresh()
|
||||||
|
self.logger.info("Страница обновлена")
|
||||||
|
|
||||||
|
@allure.step("Найти элемент: {locator}")
|
||||||
|
def find_element(self, locator):
|
||||||
|
try:
|
||||||
|
element = self.wait.until(EC.visibility_of_element_located(locator))
|
||||||
|
self.logger.debug(f"Элемент найден: {locator}")
|
||||||
|
return element
|
||||||
|
except TimeoutException:
|
||||||
|
self.logger.error(f"Элемент не найден: {locator}")
|
||||||
|
allure.attach(
|
||||||
|
self.driver.get_screenshot_as_png(),
|
||||||
|
name="element_not_found",
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@allure.step("Кликнуть на элемент: {locator}")
|
||||||
|
def click(self, locator):
|
||||||
|
element = self.find_element(locator)
|
||||||
|
element.click()
|
||||||
|
self.logger.info(f"Клик по элементу: {locator}")
|
||||||
|
|
||||||
|
@allure.step("Ввести текст '{text}' в элемент: {locator}")
|
||||||
|
def type_text(self, locator, text):
|
||||||
|
element = self.find_element(locator)
|
||||||
|
element.clear()
|
||||||
|
element.send_keys(text)
|
||||||
|
self.logger.info(f"Введен текст '{text}' в элемент: {locator}")
|
||||||
|
|
||||||
|
@allure.step("Получить текст элемента: {locator}")
|
||||||
|
def get_text(self, locator):
|
||||||
|
element = self.find_element(locator)
|
||||||
|
text = element.text
|
||||||
|
self.logger.info(f"Получен текст '{text}' из элемента: {locator}")
|
||||||
|
return text
|
||||||
|
|
||||||
|
@allure.step("Проверить, что элемент видим: {locator}")
|
||||||
|
def is_visible(self, locator):
|
||||||
|
try:
|
||||||
|
element = self.find_element(locator)
|
||||||
|
is_displayed = element.is_displayed()
|
||||||
|
self.logger.info(f"Элемент {locator} видим: {is_displayed}")
|
||||||
|
return is_displayed
|
||||||
|
except (TimeoutException, NoSuchElementException):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@allure.step("Получить текущий URL")
|
||||||
|
def get_current_url(self):
|
||||||
|
url = self.driver.current_url
|
||||||
|
self.logger.info(f"Текущий URL: {url}")
|
||||||
|
return url
|
||||||
|
|
||||||
|
@allure.step("Сделать скриншот")
|
||||||
|
def take_screenshot(self, name="screenshot"):
|
||||||
|
screenshot = self.driver.get_screenshot_as_png()
|
||||||
|
allure.attach(
|
||||||
|
screenshot,
|
||||||
|
name=name,
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
self.logger.info(f"Скриншот сохранен: {name}")
|
||||||
|
|
||||||
|
@allure.step("Ожидание загрузки элемента: {locator}")
|
||||||
|
def wait_for_element(self, locator, timeout=10):
|
||||||
|
wait = WebDriverWait(self.driver, timeout)
|
||||||
|
element = wait.until(EC.visibility_of_element_located(locator))
|
||||||
|
self.logger.info(f"Элемент загружен: {locator}")
|
||||||
|
return element
|
||||||
|
|
||||||
|
@allure.step("Скролл до элемента")
|
||||||
|
def scroll_to_element(self, locator):
|
||||||
|
element = self.find_element(locator)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
|
||||||
92
tests/ui/pages/home_page.py
Normal file
92
tests/ui/pages/home_page.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import allure
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from config.ui_config import UIConfig
|
||||||
|
from tests.ui.pages.base_page import BasePage
|
||||||
|
|
||||||
|
|
||||||
|
class HomePage(BasePage):
|
||||||
|
# Локаторы
|
||||||
|
LOGO = (By.CLASS_NAME, "logo-text")
|
||||||
|
TEAM_TITLE = (By.XPATH, "//div[contains(@class, 'team-panel')]//h3")
|
||||||
|
ARTICLE_TITLE = (By.XPATH, "//div[contains(@class, 'blog-panel')]//h3")
|
||||||
|
TIME = (By.ID, "current-time")
|
||||||
|
TEAM_SECTION = (By.CLASS_NAME, "team-carousel")
|
||||||
|
ARTICLE_SECTION = (By.CLASS_NAME, "articles-list")
|
||||||
|
TEAM_NOT_FOUND = (By.XPATH, "//div[contains(@class, 'team-carousel')]//div[contains(@class, 'empty-state')]/span[not(contains(@class, 'empty-icon'))]")
|
||||||
|
ARTICLE_NOT_FOUND = (By.XPATH, "//div[contains(@class, 'articles-list')]//div[contains(@class, 'empty-state')]/span[not(contains(@class, 'empty-icon'))]")
|
||||||
|
AUTHOR_CARD = (By.CLASS_NAME, "team-member-card")
|
||||||
|
ARTICLE_CARD = (By.CLASS_NAME, "article-preview")
|
||||||
|
TEAM_VIEW_ALL = (By.XPATH, "//div[contains(@class, 'team-panel')]//a[contains(@class, 'view-all')]")
|
||||||
|
ARTICLE_VIEW_ALL = (By.XPATH, "//div[contains(@class, 'blog-panel')]//a[contains(@class, 'view-all')]")
|
||||||
|
|
||||||
|
def __init__(self, driver):
|
||||||
|
super().__init__(driver)
|
||||||
|
|
||||||
|
@allure.step("Открытие домашней страницы")
|
||||||
|
def open_home_page(self):
|
||||||
|
self.open(UIConfig.UI_BASE_URL)
|
||||||
|
|
||||||
|
@allure.step("Проверка большого лого")
|
||||||
|
def check_home_logo(self):
|
||||||
|
text = self.get_text(self.LOGO)
|
||||||
|
assert text == "Team"
|
||||||
|
return text
|
||||||
|
|
||||||
|
@allure.step("Проверка заголовка списка команды")
|
||||||
|
def check_home_team(self):
|
||||||
|
text = self.get_text(self.TEAM_TITLE)
|
||||||
|
assert text == "MEET THE TEAM"
|
||||||
|
return text
|
||||||
|
|
||||||
|
@allure.step("Проверка заголовка списка постов")
|
||||||
|
def check_home_posts(self):
|
||||||
|
text = self.get_text(self.ARTICLE_TITLE)
|
||||||
|
assert text == "LATEST ARTICLES"
|
||||||
|
return text
|
||||||
|
|
||||||
|
@allure.step("Проверить наличие раздела 'Meet the Team'")
|
||||||
|
def is_meet_the_team_section_displayed(self):
|
||||||
|
return self.is_visible(self.TEAM_SECTION)
|
||||||
|
|
||||||
|
@allure.step("Проверить наличие раздела 'Latest Articles'")
|
||||||
|
def is_latest_articles_section_displayed(self):
|
||||||
|
return self.is_visible(self.ARTICLE_SECTION)
|
||||||
|
|
||||||
|
@allure.step("Проверить сообщение 'No team members found'")
|
||||||
|
def check_no_team_members_message(self):
|
||||||
|
message = self.get_text(self.TEAM_NOT_FOUND)
|
||||||
|
assert "No team members found" in message, \
|
||||||
|
f"Ожидалось сообщение 'No team members found', получено '{message}'"
|
||||||
|
return message
|
||||||
|
|
||||||
|
@allure.step("Проверить сообщение 'No articles found'")
|
||||||
|
def check_no_articles_message(self):
|
||||||
|
message = self.get_text(self.ARTICLE_NOT_FOUND)
|
||||||
|
assert "No articles found" in message, \
|
||||||
|
f"Ожидалось сообщение 'No articles found', получено '{message}'"
|
||||||
|
return message
|
||||||
|
|
||||||
|
@allure.step("Проверить наличие карточки автора")
|
||||||
|
def is_member_card_displayed(self):
|
||||||
|
return self.is_visible(self.AUTHOR_CARD)
|
||||||
|
|
||||||
|
@allure.step("Проверить наличие карточки статьи")
|
||||||
|
def is_article_card_displayed(self):
|
||||||
|
return self.is_visible(self.ARTICLE_CARD)
|
||||||
|
|
||||||
|
@allure.step("Проверить видимость кнопки VIEW ALL TEAM")
|
||||||
|
def is_team_view_all_displayed(self):
|
||||||
|
return self.is_visible(self.TEAM_VIEW_ALL)
|
||||||
|
|
||||||
|
@allure.step("Проверить видимость кнопки VIEW ALL TEAM")
|
||||||
|
def is_article_view_all_displayed(self):
|
||||||
|
return self.is_visible(self.ARTICLE_VIEW_ALL)
|
||||||
|
|
||||||
|
@allure.step("Проверить кликабельность кнопки VIEW ALL TEAM")
|
||||||
|
def click_view_all_team_button(self):
|
||||||
|
self.click(self.TEAM_VIEW_ALL)
|
||||||
|
|
||||||
|
@allure.step("Проверить кликабельность кнопки VIEW ALL TEAM")
|
||||||
|
def click_view_all_article_button(self):
|
||||||
|
self.click(self.ARTICLE_VIEW_ALL)
|
||||||
|
|
||||||
158
tests/ui/test_admin_page.py
Normal file
158
tests/ui/test_admin_page.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||||
|
|
||||||
|
from config.ui_config import UIConfig
|
||||||
|
from tests.api.utils.api_client import APIClient
|
||||||
|
from tests.ui.pages.admin_page import AdminPage
|
||||||
|
|
||||||
|
|
||||||
|
@allure.epic("Страница администратора")
|
||||||
|
@allure.feature("Основной функционал")
|
||||||
|
class TestAdminPage:
|
||||||
|
@allure.story("Загрузка страницы")
|
||||||
|
@allure.title("Проверка успешной загрузки страницы")
|
||||||
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
|
@pytest.mark.smoke
|
||||||
|
@pytest.mark.ui
|
||||||
|
def test_page_load_succesfull(self, driver):
|
||||||
|
admin_page = AdminPage(driver)
|
||||||
|
|
||||||
|
with allure.step('1. Открыть страницу'):
|
||||||
|
admin_page.open_admin_page()
|
||||||
|
admin_page.take_screenshot("admin_page")
|
||||||
|
|
||||||
|
@allure.story("Структура страницы")
|
||||||
|
@allure.title("Проверка отображения элементов")
|
||||||
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
|
@pytest.mark.smoke
|
||||||
|
@pytest.mark.ui
|
||||||
|
def test_page_elements_visiblity(self, driver):
|
||||||
|
admin_page = AdminPage(driver)
|
||||||
|
|
||||||
|
with allure.step('1. Открыть страницу'):
|
||||||
|
admin_page.open_admin_page()
|
||||||
|
admin_page.take_screenshot("admin_page")
|
||||||
|
|
||||||
|
with allure.step('2. Проверка наличия кнопки блока логина'):
|
||||||
|
assert admin_page.is_login_block_visible(), 'Кнопка блока логина не отобразилась'
|
||||||
|
|
||||||
|
with allure.step('3. Проверка невидимости кнопки "try it out"'):
|
||||||
|
assert not admin_page.is_login_tryitout_btn_visible(), 'Кнопка не должна быть видима'
|
||||||
|
|
||||||
|
with allure.step('4. Проверка невидимости поля ввода'):
|
||||||
|
assert not admin_page.is_login_textarea_visible(), 'Поле ввода не должно быть видимо'
|
||||||
|
|
||||||
|
with allure.step('5. Проверка невидимости кнопки выполнения запроса'):
|
||||||
|
assert not admin_page.is_login_execute_btn_visible(), 'Кнопка выполнения запроса не должна быть видимой'
|
||||||
|
|
||||||
|
@allure.story("Функциональность")
|
||||||
|
@allure.title("Проверка функциональности страницы")
|
||||||
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
|
@pytest.mark.smoke
|
||||||
|
@pytest.mark.ui
|
||||||
|
def test_page_login_process(self, driver: WebDriver):
|
||||||
|
admin_page = AdminPage(driver)
|
||||||
|
|
||||||
|
with allure.step('1. Открыть страницу'):
|
||||||
|
admin_page.open_admin_page()
|
||||||
|
admin_page.wait_for_element(admin_page.LOGIN_BLOCK)
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_BLOCK)
|
||||||
|
admin_page.take_screenshot("admin_page")
|
||||||
|
|
||||||
|
with allure.step('2. Нажать на блок логина'):
|
||||||
|
admin_page.click_login_block()
|
||||||
|
assert admin_page.is_login_tryitout_btn_visible(), 'Кнопка "try it out" должна быть видимой'
|
||||||
|
|
||||||
|
with allure.step('3. Нажать на кнопку "try it out"'):
|
||||||
|
admin_page.click_login_tryitout_btn()
|
||||||
|
assert admin_page.is_login_textarea_visible(), 'Поле ввода должно быть видимым'
|
||||||
|
assert admin_page.is_login_execute_btn_visible(), 'Кнопка выполнения запроса должна быть видимой'
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_TRYITOUT_BTN)
|
||||||
|
admin_page.take_screenshot('admin_page_login_block')
|
||||||
|
|
||||||
|
with allure.step('4. Ввод данных для входа'):
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_TEXTAREA)
|
||||||
|
admin_page.take_screenshot("admin_page_login_area_unchanged")
|
||||||
|
admin_page.enter_text_to_login_textarea(admin_page.LOGIN_DATA)
|
||||||
|
time.sleep(0.5)
|
||||||
|
admin_page.take_screenshot("admin_page_login_text_entered")
|
||||||
|
|
||||||
|
with allure.step('5. Выполнение запроса'):
|
||||||
|
admin_page.click_login_execute_btn()
|
||||||
|
time.sleep(2.0)
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_STATUS_CODE)
|
||||||
|
admin_page.take_screenshot("admin_page_login_request_executed")
|
||||||
|
|
||||||
|
with allure.step('6. Проверка статуса выполнения запроса'):
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_STATUS_CODE)
|
||||||
|
assert int(admin_page.get_login_status()) == 200
|
||||||
|
|
||||||
|
@allure.story("Добавление пользователя")
|
||||||
|
@allure.title("Проверка функции добавления пользователя")
|
||||||
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
|
@pytest.mark.smoke
|
||||||
|
@pytest.mark.ui
|
||||||
|
def test_page_user_adding(self, driver: WebDriver, auth_admin: APIClient):
|
||||||
|
admin_page = AdminPage(driver)
|
||||||
|
user_id = ''
|
||||||
|
|
||||||
|
with allure.step('1. Открыть страницу'):
|
||||||
|
admin_page.open_admin_page()
|
||||||
|
admin_page.wait_for_element(admin_page.USER_BLOCK)
|
||||||
|
admin_page.scroll_to_element(admin_page.USER_BLOCK)
|
||||||
|
admin_page.take_screenshot("admin_page")
|
||||||
|
|
||||||
|
with allure.step("2. Авторизоваться"):
|
||||||
|
admin_page.click_login_block()
|
||||||
|
admin_page.click_login_tryitout_btn()
|
||||||
|
admin_page.enter_text_to_login_textarea(admin_page.LOGIN_DATA)
|
||||||
|
time.sleep(0.5)
|
||||||
|
admin_page.click_login_execute_btn()
|
||||||
|
time.sleep(2.0)
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_STATUS_CODE)
|
||||||
|
admin_page.take_screenshot("admin_page_login_request_executed")
|
||||||
|
|
||||||
|
with allure.step('3. Нажать на блок пользователя'):
|
||||||
|
admin_page.click_user_block()
|
||||||
|
assert admin_page.is_user_tryitout_btn_visible(), 'Кнопка "try it out" должна быть видимой'
|
||||||
|
|
||||||
|
with allure.step('4. Нажать на кнопку "try it out"'):
|
||||||
|
admin_page.click_user_tryitout_btn()
|
||||||
|
assert admin_page.is_user_textarea_visible(), 'Поле ввода должно быть видимым'
|
||||||
|
assert admin_page.is_user_execute_btn_visible(), 'Кнопка выполнения запроса должна быть видимой'
|
||||||
|
admin_page.scroll_to_element(admin_page.USER_TRYITOUT_BTN)
|
||||||
|
admin_page.take_screenshot('admin_page_user_block')
|
||||||
|
|
||||||
|
with allure.step('5. Ввод данных для регистрации пользователя'):
|
||||||
|
admin_page.scroll_to_element(admin_page.USER_TEXTAREA)
|
||||||
|
admin_page.take_screenshot("admin_page_user_area_unchanged")
|
||||||
|
admin_page.enter_text_to_user_textarea(admin_page.USER_DATA)
|
||||||
|
time.sleep(0.5)
|
||||||
|
admin_page.take_screenshot("admin_page_user_text_entered")
|
||||||
|
|
||||||
|
with allure.step('6. Выполнение запроса'):
|
||||||
|
admin_page.click_user_execute_btn()
|
||||||
|
time.sleep(2.0)
|
||||||
|
admin_page.scroll_to_element(admin_page.USER_STATUS_CODE)
|
||||||
|
admin_page.take_screenshot("admin_page_user_request_executed")
|
||||||
|
|
||||||
|
with allure.step('7. Проверка статуса выполнения запроса'):
|
||||||
|
admin_page.scroll_to_element(admin_page.LOGIN_STATUS_CODE)
|
||||||
|
assert int(admin_page.get_login_status()) == 200
|
||||||
|
|
||||||
|
with allure.step('8. Сохраняем id пользователя для удаления'):
|
||||||
|
content = admin_page.get_user_response()
|
||||||
|
user_id = json.loads(content)['id']
|
||||||
|
|
||||||
|
with allure.step('9. Проверка, что пользователь добавлен на сайт'):
|
||||||
|
admin_page.open(UIConfig.get_url('team'))
|
||||||
|
time.sleep(2)
|
||||||
|
admin_page.take_screenshot('team_page_user')
|
||||||
|
|
||||||
|
with allure.step('10. Удаляем пользователя через API'):
|
||||||
|
auth_admin.delete_user(user_id)
|
||||||
|
|
||||||
224
tests/ui/test_home_page.py
Normal file
224
tests/ui/test_home_page.py
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
import pytest
|
||||||
|
import allure
|
||||||
|
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||||
|
from config.ui_config import UIConfig
|
||||||
|
import time
|
||||||
|
|
||||||
|
from tests.ui.pages.home_page import HomePage
|
||||||
|
|
||||||
|
|
||||||
|
@allure.epic("Домашняя страница")
|
||||||
|
@allure.feature("Основной функционал")
|
||||||
|
class TestTeamPage:
|
||||||
|
|
||||||
|
@allure.story("Загрузка страницы")
|
||||||
|
@allure.title("Проверка успешной загрузки страницы")
|
||||||
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
|
@pytest.mark.smoke
|
||||||
|
@pytest.mark.ui
|
||||||
|
def test_page_load_successfully(self, driver):
|
||||||
|
"""
|
||||||
|
Тест проверяет успешную загрузку страницы Team
|
||||||
|
"""
|
||||||
|
home_page = HomePage(driver)
|
||||||
|
|
||||||
|
with allure.step("1. Открыть страницу"):
|
||||||
|
home_page.open_home_page()
|
||||||
|
home_page.take_screenshot("home_page_loaded")
|
||||||
|
|
||||||
|
with allure.step("2. Проверить заголовок окна браузера"):
|
||||||
|
page_title = driver.title
|
||||||
|
allure.attach(page_title, name="page_title", attachment_type=allure.attachment_type.TEXT)
|
||||||
|
assert page_title, "Заголовок страницы пустой"
|
||||||
|
|
||||||
|
@allure.story("Контент страницы")
|
||||||
|
@allure.title("Проверка текстового содержания страницы")
|
||||||
|
@allure.severity(allure.severity_level.CRITICAL)
|
||||||
|
@pytest.mark.regression
|
||||||
|
def test_page_content_validation(self, driver):
|
||||||
|
"""
|
||||||
|
Тест проверяет корректность текстового содержания страницы
|
||||||
|
"""
|
||||||
|
home_page = HomePage(driver)
|
||||||
|
|
||||||
|
with allure.step("1. Открыть страницу"):
|
||||||
|
home_page.open_home_page()
|
||||||
|
|
||||||
|
with allure.step("2. Проверить заголовок"):
|
||||||
|
title = home_page.get_text(home_page.LOGO)
|
||||||
|
assert title == "Team", f"Неверный заголовок: {title}"
|
||||||
|
|
||||||
|
|
||||||
|
with allure.step("4. Проверить сообщения о пустом состоянии"):
|
||||||
|
team_message = home_page.check_no_team_members_message()
|
||||||
|
articles_message = home_page.check_no_articles_message()
|
||||||
|
|
||||||
|
allure.attach(
|
||||||
|
f"Сообщение о команде: {team_message}\nСообщение о статьях: {articles_message}",
|
||||||
|
name="empty_state_messages",
|
||||||
|
attachment_type=allure.attachment_type.TEXT
|
||||||
|
)
|
||||||
|
|
||||||
|
with allure.step("5. Сделать скриншот всей страницы"):
|
||||||
|
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||||
|
time.sleep(1)
|
||||||
|
home_page.take_screenshot("full_page_content")
|
||||||
|
|
||||||
|
@allure.story("Структура страницы")
|
||||||
|
@allure.title("Проверка структуры и порядка элементов")
|
||||||
|
@allure.severity(allure.severity_level.NORMAL)
|
||||||
|
@pytest.mark.ui
|
||||||
|
def test_page_structure(self, driver):
|
||||||
|
"""
|
||||||
|
Тест проверяет правильность структуры расположения элементов
|
||||||
|
"""
|
||||||
|
home_page = HomePage(driver)
|
||||||
|
|
||||||
|
with allure.step("1. Открыть страницу Team"):
|
||||||
|
home_page.open_home_page()
|
||||||
|
|
||||||
|
with allure.step("2. Проверить наличие всех разделов"):
|
||||||
|
assert home_page.is_meet_the_team_section_displayed(), \
|
||||||
|
"Раздел 'MEET THE TEAM' не отображается"
|
||||||
|
assert home_page.is_latest_articles_section_displayed(), \
|
||||||
|
"Раздел 'LATEST ARTICLES' не отображается"
|
||||||
|
|
||||||
|
with allure.step("3. Проверить кнопку VIEW ALL team"):
|
||||||
|
button = home_page.is_team_view_all_displayed()
|
||||||
|
assert button, "Кнопка не отобразилась"
|
||||||
|
|
||||||
|
with allure.step("4. Проверить кнопку VIEW ALL article"):
|
||||||
|
button = home_page.is_article_view_all_displayed()
|
||||||
|
assert button, "Кнопка не отобразилась"
|
||||||
|
|
||||||
|
@allure.story("Функциональность")
|
||||||
|
@allure.title("Проверка кликабельности элементов")
|
||||||
|
@allure.severity(allure.severity_level.NORMAL)
|
||||||
|
@pytest.mark.regression
|
||||||
|
def test_elements_interactivity(self, driver):
|
||||||
|
"""
|
||||||
|
Тест проверяет кликабельность интерактивных элементов
|
||||||
|
"""
|
||||||
|
home_page = HomePage(driver)
|
||||||
|
|
||||||
|
with allure.step("1. Открыть страницу Team"):
|
||||||
|
home_page.open_home_page()
|
||||||
|
|
||||||
|
with allure.step("2. Проверить кликабельность кнопки VIEW ALL team"):
|
||||||
|
button_team = home_page.find_element(home_page.TEAM_VIEW_ALL)
|
||||||
|
assert button_team.is_enabled(), "Кнопка VIEW ALL не активна"
|
||||||
|
|
||||||
|
# Проверяем, что это ссылка
|
||||||
|
tag_name = button_team.tag_name
|
||||||
|
assert tag_name.lower() in ['a', 'button'], \
|
||||||
|
f"Элемент не является ссылкой или кнопкой (тег: {tag_name})"
|
||||||
|
|
||||||
|
with allure.step("3. Проверить кликабельность кнопки VIEW ALL article"):
|
||||||
|
button_article = home_page.find_element(home_page.ARTICLE_VIEW_ALL)
|
||||||
|
assert button_article.is_enabled(), "Кнопка VIEW ALL не активна"
|
||||||
|
|
||||||
|
# Проверяем, что это ссылка
|
||||||
|
tag_name = button_article.tag_name
|
||||||
|
assert tag_name.lower() in ['a', 'button'], \
|
||||||
|
f"Элемент не является ссылкой или кнопкой (тег: {tag_name})"
|
||||||
|
|
||||||
|
|
||||||
|
with allure.step("4. Навести курсор на кнопку VIEW ALL team"):
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
actions = ActionChains(driver)
|
||||||
|
actions.move_to_element(button_team).perform()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Проверяем изменение стиля (опционально)
|
||||||
|
home_page.take_screenshot("team_view_all_button_hover")
|
||||||
|
|
||||||
|
with allure.step("4. Навести курсор на кнопку VIEW ALL article"):
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
actions = ActionChains(driver)
|
||||||
|
actions.move_to_element(button_article).perform()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Проверяем изменение стиля (опционально)
|
||||||
|
home_page.take_screenshot("article_view_all_button_hover")
|
||||||
|
|
||||||
|
|
||||||
|
@allure.story("Навигация")
|
||||||
|
@allure.title("Проверка поведения кнопки VIEW ALL")
|
||||||
|
@allure.severity(allure.severity_level.CRITICAL)
|
||||||
|
@pytest.mark.smoke
|
||||||
|
def test_view_all_button_functionality(self, driver: WebDriver):
|
||||||
|
"""
|
||||||
|
Тест проверяет функциональность кнопки VIEW ALL
|
||||||
|
"""
|
||||||
|
home_page = HomePage(driver)
|
||||||
|
|
||||||
|
with allure.step("1. Открыть страницу Team"):
|
||||||
|
home_page.open_home_page()
|
||||||
|
initial_url = driver.current_url
|
||||||
|
allure.attach(initial_url, name="initial_url", attachment_type=allure.attachment_type.TEXT)
|
||||||
|
|
||||||
|
with allure.step("2. Нажать кнопку VIEW ALL team"):
|
||||||
|
home_page.click_view_all_team_button()
|
||||||
|
time.sleep(2) # Ждем загрузки
|
||||||
|
|
||||||
|
new_url = driver.current_url
|
||||||
|
allure.attach(new_url, name="new_url", attachment_type=allure.attachment_type.TEXT)
|
||||||
|
|
||||||
|
with allure.step("3. Проверить изменение URL"):
|
||||||
|
# Проверяем, что URL изменился (или остался тем же, если это anchor link)
|
||||||
|
assert new_url == UIConfig.get_url("team"), "Совершен переход на неверную страницу"
|
||||||
|
|
||||||
|
with allure.step("4. Возвращаемся на главную"):
|
||||||
|
driver.back()
|
||||||
|
|
||||||
|
with allure.step("2. Нажать кнопку VIEW ALL article"):
|
||||||
|
home_page.click_view_all_article_button()
|
||||||
|
time.sleep(2) # Ждем загрузки
|
||||||
|
|
||||||
|
new_url = driver.current_url
|
||||||
|
allure.attach(new_url, name="new_url", attachment_type=allure.attachment_type.TEXT)
|
||||||
|
|
||||||
|
with allure.step("3. Проверить изменение URL"):
|
||||||
|
# Проверяем, что URL изменился (или остался тем же, если это anchor link)
|
||||||
|
assert new_url == UIConfig.get_url("blog"), "Совершен переход на неверную страницу"
|
||||||
|
|
||||||
|
@allure.story("Контент страницы")
|
||||||
|
@allure.title("Проверка появления карточек")
|
||||||
|
@allure.severity(allure.severity_level.CRITICAL)
|
||||||
|
@pytest.mark.regression
|
||||||
|
def test_cards_visiblity(self, driver, gen_data_fixture):
|
||||||
|
"""
|
||||||
|
Тест проверяет появление предложенных карточек
|
||||||
|
"""
|
||||||
|
home_page = HomePage(driver)
|
||||||
|
|
||||||
|
with allure.step("1. Открыть страницу"):
|
||||||
|
home_page.open_home_page()
|
||||||
|
|
||||||
|
with allure.step("2. Проверка существования карточки автора"):
|
||||||
|
card = home_page.is_member_card_displayed()
|
||||||
|
assert card, "Карточка пользователя не отобразилась"
|
||||||
|
|
||||||
|
with allure.step("3. Проверка реакции карточки на наведение"):
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
member_card = home_page.find_element(home_page.AUTHOR_CARD)
|
||||||
|
actions = ActionChains(driver)
|
||||||
|
actions.move_to_element(member_card).perform()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
home_page.take_screenshot("Member_card_hower_reaction")
|
||||||
|
|
||||||
|
with allure.step("4. Проверка существования карточки статьи"):
|
||||||
|
card = home_page.is_article_card_displayed()
|
||||||
|
assert card, "Карточка статьи не отобразилась"
|
||||||
|
|
||||||
|
with allure.step("5. Проверка реакции карточки на наведение"):
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
member_card = home_page.find_element(home_page.ARTICLE_CARD)
|
||||||
|
actions = ActionChains(driver)
|
||||||
|
actions.move_to_element(member_card).perform()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
home_page.take_screenshot("Member_card_hower_reaction")
|
||||||
|
|
||||||
|
|
||||||
0
tests/ui/utils/__init__.py
Normal file
0
tests/ui/utils/__init__.py
Normal file
0
tests/ui/utils/wait.py
Normal file
0
tests/ui/utils/wait.py
Normal file
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/waiters.cpython-313.pyc
Normal file
BIN
utils/__pycache__/waiters.cpython-313.pyc
Normal file
Binary file not shown.
0
utils/allure_helpers.py
Normal file
0
utils/allure_helpers.py
Normal file
398
utils/logger.py
Normal file
398
utils/logger.py
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, Union
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
# Цвета для консольного вывода (только если поддерживается)
|
||||||
|
from colorama import init, Fore, Back, Style
|
||||||
|
init(autoreset=True)
|
||||||
|
|
||||||
|
class TestLogger:
|
||||||
|
"""Кастомный логгер для тестов с поддержкой Allure и консольного вывода"""
|
||||||
|
|
||||||
|
# Уровни логирования с цветами
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
'DEBUG': Fore.CYAN,
|
||||||
|
'INFO': Fore.GREEN,
|
||||||
|
'WARNING': Fore.YELLOW,
|
||||||
|
'ERROR': Fore.RED,
|
||||||
|
'CRITICAL': Fore.RED + Back.WHITE,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Уровни логирования для Allure
|
||||||
|
ALLURE_LEVELS = {
|
||||||
|
'DEBUG': 'debug',
|
||||||
|
'INFO': 'info',
|
||||||
|
'WARNING': 'warning',
|
||||||
|
'ERROR': 'error',
|
||||||
|
'CRITICAL': 'critical',
|
||||||
|
}
|
||||||
|
|
||||||
|
_instances = {} # Кэш инстансов логгеров
|
||||||
|
|
||||||
|
def __init__(self, name: str, level: str = 'INFO',
|
||||||
|
log_to_file: bool = True,
|
||||||
|
log_to_console: bool = True,
|
||||||
|
log_to_allure: bool = True):
|
||||||
|
"""
|
||||||
|
Инициализация логгера
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя логгера (обычно __name__)
|
||||||
|
level: Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
log_to_file: Логировать в файл
|
||||||
|
log_to_console: Логировать в консоль
|
||||||
|
log_to_allure: Логировать в Allure отчет
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.log_to_file = log_to_file
|
||||||
|
self.log_to_console = log_to_console
|
||||||
|
self.log_to_allure = log_to_allure
|
||||||
|
|
||||||
|
# Создаем стандартный логгер
|
||||||
|
self.logger = logging.getLogger(name)
|
||||||
|
self.logger.setLevel(getattr(logging, level.upper()))
|
||||||
|
|
||||||
|
# Удаляем существующие обработчики
|
||||||
|
self.logger.handlers.clear()
|
||||||
|
|
||||||
|
# Форматтеры
|
||||||
|
self._setup_formatters()
|
||||||
|
|
||||||
|
# Обработчики
|
||||||
|
self._setup_handlers()
|
||||||
|
|
||||||
|
# Для хранения контекста теста
|
||||||
|
self.test_context = {}
|
||||||
|
|
||||||
|
def _setup_formatters(self):
|
||||||
|
"""Настройка форматтеров"""
|
||||||
|
# Подробный формат для файла
|
||||||
|
self.file_formatter = logging.Formatter(
|
||||||
|
fmt='%(asctime)s.%(msecs)03d | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Простой формат для консоли
|
||||||
|
self.console_formatter = logging.Formatter(
|
||||||
|
fmt='%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s',
|
||||||
|
datefmt='%H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
# JSON формат для машинной обработки
|
||||||
|
self.json_formatter = JSONFormatter()
|
||||||
|
|
||||||
|
def _setup_handlers(self):
|
||||||
|
"""Настройка обработчиков"""
|
||||||
|
# Консольный обработчик
|
||||||
|
if self.log_to_console:
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_handler.setLevel(self.logger.level)
|
||||||
|
console_handler.setFormatter(self.console_formatter)
|
||||||
|
|
||||||
|
console_handler.setFormatter(ColorFormatter())
|
||||||
|
|
||||||
|
self.logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# Файловый обработчик
|
||||||
|
if self.log_to_file:
|
||||||
|
self._setup_file_handler()
|
||||||
|
|
||||||
|
def _setup_file_handler(self):
|
||||||
|
"""Настройка файлового обработчика"""
|
||||||
|
from config.settings import settings
|
||||||
|
|
||||||
|
log_file = Path(settings.LOG_FILE)
|
||||||
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||||
|
file_handler.setLevel(self.logger.level)
|
||||||
|
file_handler.setFormatter(self.file_formatter)
|
||||||
|
self.logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# JSON лог для машинной обработки
|
||||||
|
json_log_file = log_file.parent / f"{log_file.stem}_json{log_file.suffix}"
|
||||||
|
json_handler = logging.FileHandler(json_log_file, encoding='utf-8')
|
||||||
|
json_handler.setLevel(self.logger.level)
|
||||||
|
json_handler.setFormatter(self.json_formatter)
|
||||||
|
self.logger.addHandler(json_handler)
|
||||||
|
|
||||||
|
def set_test_context(self, **kwargs):
|
||||||
|
"""Установка контекста теста (имя теста, тестовые данные и т.д.)"""
|
||||||
|
self.test_context.update(kwargs)
|
||||||
|
|
||||||
|
def clear_test_context(self):
|
||||||
|
"""Очистка контекста теста"""
|
||||||
|
self.test_context.clear()
|
||||||
|
|
||||||
|
def debug(self, message: str, **kwargs):
|
||||||
|
"""Логирование на уровне DEBUG"""
|
||||||
|
self._log('DEBUG', message, **kwargs)
|
||||||
|
|
||||||
|
def info(self, message: str, **kwargs):
|
||||||
|
"""Логирование на уровне INFO"""
|
||||||
|
self._log('INFO', message, **kwargs)
|
||||||
|
|
||||||
|
def warning(self, message: str, **kwargs):
|
||||||
|
"""Логирование на уровне WARNING"""
|
||||||
|
self._log('WARNING', message, **kwargs)
|
||||||
|
|
||||||
|
def error(self, message: str, exception: Optional[Exception] = None, **kwargs):
|
||||||
|
"""Логирование на уровне ERROR"""
|
||||||
|
if exception:
|
||||||
|
message = f"{message} | Exception: {exception} | Traceback: {traceback.format_exc()}"
|
||||||
|
self._log('ERROR', message, **kwargs)
|
||||||
|
|
||||||
|
def critical(self, message: str, **kwargs):
|
||||||
|
"""Логирование на уровне CRITICAL"""
|
||||||
|
self._log('CRITICAL', message, **kwargs)
|
||||||
|
|
||||||
|
def _log(self, level: str, message: str, **kwargs):
|
||||||
|
"""Внутренний метод логирования"""
|
||||||
|
# Добавляем контекст к сообщению
|
||||||
|
if self.test_context:
|
||||||
|
context_str = ' | '.join(f"{k}: {v}" for k, v in self.test_context.items())
|
||||||
|
message = f"{message} | Context: {context_str}"
|
||||||
|
|
||||||
|
# Добавляем дополнительные поля
|
||||||
|
if kwargs:
|
||||||
|
extra_fields = ' | '.join(f"{k}: {v}" for k, v in kwargs.items())
|
||||||
|
message = f"{message} | {extra_fields}"
|
||||||
|
|
||||||
|
# Получаем информацию о вызывающем коде
|
||||||
|
curr_frame = inspect.currentframe()
|
||||||
|
lineno = None
|
||||||
|
func_name = None
|
||||||
|
filename = None
|
||||||
|
|
||||||
|
if curr_frame and curr_frame.f_back:
|
||||||
|
frame = curr_frame.f_back.f_back
|
||||||
|
if frame:
|
||||||
|
filename = frame.f_code.co_filename
|
||||||
|
lineno = frame.f_lineno
|
||||||
|
func_name = frame.f_code.co_name
|
||||||
|
|
||||||
|
extra = {
|
||||||
|
'filename': Path(filename).name if filename else '-',
|
||||||
|
'lineno': lineno,
|
||||||
|
'func_name': func_name,
|
||||||
|
'test_context': self.test_context.copy(),
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Логируем через стандартный логгер
|
||||||
|
log_method = getattr(self.logger, level.lower())
|
||||||
|
log_method(message, extra=extra)
|
||||||
|
|
||||||
|
# Логируем в Allure
|
||||||
|
if self.log_to_allure:
|
||||||
|
self._log_to_allure(level, message)
|
||||||
|
|
||||||
|
def _log_to_allure(self, level: str, message: str):
|
||||||
|
"""Логирование в Allure отчет"""
|
||||||
|
try:
|
||||||
|
import allure
|
||||||
|
|
||||||
|
allure_level = self.ALLURE_LEVELS.get(level, 'info')
|
||||||
|
getattr(allure.step, allure_level)(message)
|
||||||
|
except ImportError:
|
||||||
|
pass # Allure не установлен
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Failed to log to Allure: {e}")
|
||||||
|
|
||||||
|
def log_api_request(self, method: str, url: str,
|
||||||
|
headers: Optional[dict[str, Any]] = None,
|
||||||
|
body: Any = None,
|
||||||
|
response: Any = None):
|
||||||
|
"""Логирование API запроса"""
|
||||||
|
log_data = {
|
||||||
|
'type': 'API_REQUEST',
|
||||||
|
'method': method,
|
||||||
|
'url': url,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
log_data['headers'] = {k: v for k, v in headers.items() if k.lower() != 'authorization'}
|
||||||
|
|
||||||
|
if body:
|
||||||
|
log_data['body'] = str(body)[:500] + ('...' if len(str(body)) > 500 else '')
|
||||||
|
|
||||||
|
if response:
|
||||||
|
log_data['response_status'] = getattr(response, 'status_code', None)
|
||||||
|
log_data['response_body'] = str(getattr(response, 'text', ''))[:500] + ('...' if len(str(getattr(response, 'text', ''))) > 500 else '')
|
||||||
|
|
||||||
|
self.debug(f"API Request: {method} {url}", **log_data)
|
||||||
|
|
||||||
|
def log_ui_action(self, action: str, element: Optional[str] = None,
|
||||||
|
value: Optional[str] = None, success: bool = True):
|
||||||
|
"""Логирование UI действий"""
|
||||||
|
log_data = {
|
||||||
|
'type': 'UI_ACTION',
|
||||||
|
'action': action,
|
||||||
|
'element': element,
|
||||||
|
'value': value,
|
||||||
|
'success': success,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
level = 'INFO' if success else 'ERROR'
|
||||||
|
message = f"UI Action: {action}"
|
||||||
|
if element:
|
||||||
|
message += f" on {element}"
|
||||||
|
if value:
|
||||||
|
message += f" with value: {value}"
|
||||||
|
|
||||||
|
self._log(level, message, **log_data)
|
||||||
|
|
||||||
|
def log_test_start(self, test_name: str, test_params: Optional[dict] = None):
|
||||||
|
"""Логирование начала теста"""
|
||||||
|
self.set_test_context(test_name=test_name)
|
||||||
|
|
||||||
|
log_data = {
|
||||||
|
'type': 'TEST_START',
|
||||||
|
'test_name': test_name,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if test_params:
|
||||||
|
log_data['parameters'] = test_params
|
||||||
|
|
||||||
|
self.info(f"🚀 Starting test: {test_name}", **log_data)
|
||||||
|
|
||||||
|
def log_test_end(self, test_name: str, status: str,
|
||||||
|
duration: float = None, error: str = None):
|
||||||
|
"""Логирование окончания теста"""
|
||||||
|
log_data = {
|
||||||
|
'type': 'TEST_END',
|
||||||
|
'test_name': test_name,
|
||||||
|
'status': status,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration is not None:
|
||||||
|
log_data['duration_seconds'] = round(duration, 2)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
log_data['error'] = error
|
||||||
|
|
||||||
|
emoji = '✅' if status == 'PASSED' else '❌' if status == 'FAILED' else '⚠️'
|
||||||
|
self.info(f"{emoji} Test {status}: {test_name} "
|
||||||
|
f"(Duration: {duration:.2f}s)" if duration else "", **log_data)
|
||||||
|
|
||||||
|
self.clear_test_context()
|
||||||
|
|
||||||
|
def log_screenshot(self, screenshot_path: str, description: str = "Screenshot"):
|
||||||
|
"""Логирование скриншота"""
|
||||||
|
log_data = {
|
||||||
|
'type': 'SCREENSHOT',
|
||||||
|
'path': screenshot_path,
|
||||||
|
'description': description,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.info(f"📸 Screenshot saved: {description} -> {screenshot_path}", **log_data)
|
||||||
|
|
||||||
|
# Прикрепляем к Allure
|
||||||
|
if self.log_to_allure:
|
||||||
|
try:
|
||||||
|
import allure
|
||||||
|
if Path(screenshot_path).exists():
|
||||||
|
with open(screenshot_path, 'rb') as f:
|
||||||
|
allure.attach(
|
||||||
|
f.read(),
|
||||||
|
name=description,
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.warning(f"Failed to attach screenshot to Allure: {e}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_logger(cls, name: Optional[str] = None, **kwargs) -> 'TestLogger':
|
||||||
|
"""Фабричный метод для получения логгера (singleton pattern)"""
|
||||||
|
if name is None:
|
||||||
|
# Получаем имя вызывающего модуля
|
||||||
|
curr_frame = inspect.currentframe()
|
||||||
|
if curr_frame:
|
||||||
|
frame = curr_frame.f_back
|
||||||
|
module = inspect.getmodule(frame)
|
||||||
|
name = module.__name__ if module else '__main__'
|
||||||
|
|
||||||
|
if name not in cls._instances and name:
|
||||||
|
cls._instances[name] = cls(name, **kwargs)
|
||||||
|
|
||||||
|
return cls._instances[name]
|
||||||
|
|
||||||
|
|
||||||
|
class ColorFormatter(logging.Formatter):
|
||||||
|
"""Форматтер с цветами для консоли"""
|
||||||
|
|
||||||
|
FORMATS = {
|
||||||
|
logging.DEBUG: Fore.CYAN + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||||
|
logging.INFO: Fore.GREEN + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||||
|
logging.WARNING: Fore.YELLOW + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||||
|
logging.ERROR: Fore.RED + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||||
|
logging.CRITICAL: Fore.RED + Back.WHITE + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||||
|
}
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
log_fmt = self.FORMATS.get(record.levelno, self.FORMATS[logging.INFO])
|
||||||
|
formatter = logging.Formatter(log_fmt, datefmt='%H:%M:%S')
|
||||||
|
return formatter.format(record)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONFormatter(logging.Formatter):
|
||||||
|
"""Форматтер для JSON логов (удобно для машинной обработки)"""
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
log_record = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'level': record.levelname,
|
||||||
|
'logger': record.name,
|
||||||
|
'message': record.getMessage(),
|
||||||
|
'module': record.module,
|
||||||
|
'function': record.funcName,
|
||||||
|
'line': record.lineno,
|
||||||
|
'thread': record.threadName,
|
||||||
|
'process': record.processName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
return json.dumps(log_record, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Добавляем дополнительные поля из extra
|
||||||
|
if hasattr(record, 'test_context'):
|
||||||
|
log_record['test_context'] = record.test_context
|
||||||
|
|
||||||
|
if hasattr(record, 'filename'):
|
||||||
|
log_record['filename'] = record.filename
|
||||||
|
|
||||||
|
# Добавляем exception info если есть
|
||||||
|
if record.exc_info:
|
||||||
|
log_record['exception'] = {
|
||||||
|
'type': record.exc_info[0].__name__ if record.exc_info[0] else 'Unknown exception type',
|
||||||
|
'message': str(record.exc_info[1]),
|
||||||
|
'traceback': self.formatException(record.exc_info),
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.dumps(log_record, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный логгер по умолчанию
|
||||||
|
def get_logger(name: Optional[str] = None, **kwargs) -> TestLogger:
|
||||||
|
"""
|
||||||
|
Утилита для получения логгера
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя логгера (обычно __name__)
|
||||||
|
**kwargs: Дополнительные параметры для TestLogger
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Экземпляр TestLogger
|
||||||
|
"""
|
||||||
|
return TestLogger.get_logger(name, **kwargs)
|
||||||
4
utils/waiters.py
Normal file
4
utils/waiters.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
|
||||||
|
|
||||||
|
waiter: WebDriverWait
|
||||||
Loading…
Add table
Add a link
Reference in a new issue