From 9795660e1ff4a9faec9ed6efe67ae1cf3c8895cb Mon Sep 17 00:00:00 2001 From: KamilM1205 Date: Mon, 19 Jan 2026 23:32:11 +0400 Subject: [PATCH] Initial commit --- .gitignore | 6 + Dockerfile | 0 config/__init__.py | 0 config/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 157 bytes config/__pycache__/api_config.cpython-313.pyc | Bin 0 -> 5142 bytes .../__pycache__/environment.cpython-313.pyc | Bin 0 -> 7177 bytes .../session_config.cpython-313.pyc | Bin 0 -> 2190 bytes config/__pycache__/settings.cpython-313.pyc | Bin 0 -> 7099 bytes config/__pycache__/ui_config.cpython-313.pyc | Bin 0 -> 4547 bytes config/api_config.py | 76 ++++ config/environment.py | 152 +++++++ config/session_config.py | 42 ++ config/settings.py | 168 ++++++++ config/ui_config.py | 90 ++++ requirements.txt | 7 + scripts/run_tests.sh | 33 ++ tests/api/__init__.py | 0 tests/api/conftest.py | 64 +++ tests/api/test_posts.py | 206 +++++++++ tests/api/test_users.py | 133 ++++++ tests/api/utils/api_client.py | 213 ++++++++++ tests/conftest.py | 106 +++++ tests/data/api_posts.json | 38 ++ tests/data/api_user_auth.json | 13 + tests/data/api_users.json | 132 ++++++ tests/fixtures/__init__.py | 0 tests/fixtures/data_fixtures.py | 24 ++ tests/pytest.ini | 15 + tests/ui/__init__.py | 0 tests/ui/conftest.py | 130 ++++++ tests/ui/pages/__init__.py | 0 tests/ui/pages/admin_page.py | 134 ++++++ tests/ui/pages/base_page.py | 99 +++++ tests/ui/pages/home_page.py | 92 ++++ tests/ui/test_admin_page.py | 158 +++++++ tests/ui/test_home_page.py | 224 ++++++++++ tests/ui/utils/__init__.py | 0 tests/ui/utils/wait.py | 0 utils/__pycache__/logger.cpython-313.pyc | Bin 0 -> 19973 bytes utils/__pycache__/waiters.cpython-313.pyc | Bin 0 -> 270 bytes utils/allure_helpers.py | 0 utils/logger.py | 398 ++++++++++++++++++ utils/waiters.py | 4 + 43 files changed, 2757 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 config/__init__.py create mode 100644 config/__pycache__/__init__.cpython-313.pyc create mode 100644 config/__pycache__/api_config.cpython-313.pyc create mode 100644 config/__pycache__/environment.cpython-313.pyc create mode 100644 config/__pycache__/session_config.cpython-313.pyc create mode 100644 config/__pycache__/settings.cpython-313.pyc create mode 100644 config/__pycache__/ui_config.cpython-313.pyc create mode 100644 config/api_config.py create mode 100644 config/environment.py create mode 100644 config/session_config.py create mode 100644 config/settings.py create mode 100644 config/ui_config.py create mode 100644 requirements.txt create mode 100755 scripts/run_tests.sh create mode 100644 tests/api/__init__.py create mode 100644 tests/api/conftest.py create mode 100644 tests/api/test_posts.py create mode 100644 tests/api/test_users.py create mode 100644 tests/api/utils/api_client.py create mode 100644 tests/conftest.py create mode 100644 tests/data/api_posts.json create mode 100644 tests/data/api_user_auth.json create mode 100644 tests/data/api_users.json create mode 100644 tests/fixtures/__init__.py create mode 100644 tests/fixtures/data_fixtures.py create mode 100644 tests/pytest.ini create mode 100644 tests/ui/__init__.py create mode 100644 tests/ui/conftest.py create mode 100644 tests/ui/pages/__init__.py create mode 100644 tests/ui/pages/admin_page.py create mode 100644 tests/ui/pages/base_page.py create mode 100644 tests/ui/pages/home_page.py create mode 100644 tests/ui/test_admin_page.py create mode 100644 tests/ui/test_home_page.py create mode 100644 tests/ui/utils/__init__.py create mode 100644 tests/ui/utils/wait.py create mode 100644 utils/__pycache__/logger.cpython-313.pyc create mode 100644 utils/__pycache__/waiters.cpython-313.pyc create mode 100644 utils/allure_helpers.py create mode 100644 utils/logger.py create mode 100644 utils/waiters.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99e615d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pytest_cache/ +allure-report/ +allure-results/ +logs/ +reports/ +screenshots/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__pycache__/__init__.cpython-313.pyc b/config/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d83ce32783ae287c963d6d6ba3ef4e2ef25c984b GIT binary patch literal 157 zcmey&%ge>Uz`!u=T2v;8eheaDm=VhO%)-FHFqI*lL6gyMB|{Mt0|Ucnkkl<3{fzwF zRQ=r2l4AYRoZ?dblGNgoV*R9?{PcJT6GH0g=_lvsrDdk;$H!;pWtPOp>lIYq;;_lh ePbtkwwJTy}U|;}QRt#c%WM*V!EMjJ0U;qFLO(u~5 literal 0 HcmV?d00001 diff --git a/config/__pycache__/api_config.cpython-313.pyc b/config/__pycache__/api_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60dc3bf4aa39a74c5ee4e855d296c339cc025ffd GIT binary patch literal 5142 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 literal 0 HcmV?d00001 diff --git a/config/__pycache__/settings.cpython-313.pyc b/config/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5150494b093bdb83c53493d441ccaa6834c23c5a GIT binary patch literal 7099 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 KN2HMh8~^~PjbUW~ literal 0 HcmV?d00001 diff --git a/config/__pycache__/ui_config.cpython-313.pyc b/config/__pycache__/ui_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4b55fc305845a5766e87b53ba057514da8fdbfa GIT binary patch literal 4547 zcmey&%ge>Uz`!t3H#c)5KLf*K5C?`?pp4HJ3=9lY8G;#t8NC_27>gLan2MMbKzt@| zW-pc^76k?g1|^1IW+{eX7E>mOt|HbL)*`laR!z2-AOTILTP!Y_$tAa#9rG$R8E*-? z=9Og@<>%$5=9T#5C#IwpC4-c~Fe{Yt`4%(C>KKM1#$bjbreMY*=3u5GmKfF`9;gZ= z3giSN2e9K*#Tv{8cP0l;RqVkWFjYaqNY+4^rcksuD(Wy#P*CB1ozPajFtUQYC~_l^BvLVVtVO zkyMG`R3#BC3HPHYPE}G!DM1XUD(PSuxLM*jRmmcmC4p0w9FprLajKFJR)E_mg;SMc zuo7IAG)`5@!74CSIMt{ItHIO+tLrfp$;8M8YsAO}YsSc#umo#KF=Vm8)4P0(Y_MjG zLX2FDq6rfyqna=UYon-8!lFV4MTK&(Zjnm5o~HgSj!;kM{JgZx^p~LGrc{Z6f#Jg3 z3mYyrUTnJ9aADttofmdp*r5>Wsc^CB!d8&zg?$$`-Qo`QjCXPjc8w1W@~INdC@Cqh z($~+)PfpCq$S*FjGBq?ezQq;l86V>5>*^mG@=}h0f#DX9Q;>gnuxn7fpQEqqE%vm` zqSUnfidzCOVGmbF7a!N);E-FqFrjcymk^K426hI9TYNAv4_8ljkC03Y76t}{ULQw4 z_fSW7*IP`fdAEdMO5H+ze1e^WTwVR#ZV5Yk1o`{A#=~rh_j3)uB?J|8@eFo!@^OuK z4+y;_2o>}5j}LbAb8+&IxFrS^f+=%x4T}%<@VzDA<{9Mb<{tq!^cJtDZ-9@dvu8+r zxT9ytOHko{i`O**N$i$jfTO!>ypO-53p{viK*7To>>T775CRjtB?fVMu!nz0aJ;`? zyqlw^PiTHt48css%rLd7jG+ucAUnbG2qKg@hy%fcu|U<40YfO03!@pME&~ID3WFv~sShZ9 z&%dzm!tRUB7uzpvy|DMfjtg5AQu9&@@-y>F6fQO@T-bkM--RvUq`u+8UQlwsunUwC zKoT1+HeA?nVb6uF(A=@9iZwLIM?t+xMqME{46!mOr`9-;jC7LX^n3Hpgi$LLVi!Hl6 zu_(Rx77r)}N>XzRauQ2Yi$R%GK|w*`maTq9er~FMZfQxeerZl|seVanaY->a3gaP6 z2&t#14{?irX=XfxrB_gSi#^MK|*?Z#l(u~ zH4|$Vcw83O?clk`FMUTyW=83Phy`93^t>*}dS4duxxnM|fM2M;zO#OT%ymVRONu5J z740^ZT~xHY$ZvOn!|pREPLiQ94q`K~GcYiK(#7XEMokp-cmK-h!B} z$yTa|J=I^>cVYj{Ph05tOnoHec9&VJnEG$#RP$BQ-H4C$+fv7HfHCN=e2o zwv5!w^o){Q965=3>7|M3skhjYGm7$aQ#Dy`@xT#U!0L!#LK|IPy}kSQt%22G|?P&IY| zvv36$pL;L1T-b48^TlRR>H;Ua9SRrrUD$SE$Av8yK;`U?3p+uHE;F|vCo?&->6#ZMG{I2}W=P6hm(;r`sRxh!kDROmTpt)%Ik~_AU!={z zz~HB;dW$0-Twlh=-{Ojo&&^LM%}I@qzr_=B4K4 z$H(7dkB?8uPmYf-0!7&^0g$Ux^UC7O5{oiH`3PJ~6oDHPtU3ARsYOMge1SE?fO<+e zGYYm)E0PC=p8|*gHU5fpK$(U+IVZ8WI5)K z3z=LJGT9J&!6o{#P)vjO4MC;rg2tBwjW_rlP`WJW+TitpjX^~9y0GabVbcwH2Qn`U z2V55pyC@uXSvaD>=Z3h(b#co};+7Yzd`_fX77xC_B634S^}2}JB@wd?^(R;^i}*J9 z-Vjy0E^2;B)ck^l#|f3oqW%qjST#y&U6-`JBx!xYCg4I)_+`n63oK$E*chaBu1ncm zlCrsA8+0Kg;<8la1s3reC~CtlONL)y5xXIwd0oQll7!U-YrhKtA(tgWFR+N-kkz{` zYkx`B{(?iqg{Zj8vhf#Kq(HT+CWgyDurY|L!QIYuS;V`+7uh#Z5fod3E=vT1Y(e&- z(q&QK2EPw%408IIR^Jd)zbIE8wSZ+o@CxMx5exY~GqAIAft7q_VrPYBHBe&m z(`3KJR#I7znU`J!PnNfYpe>wK#OMY%OM#Q#Elzk72rYTu;;@0_RJ)=`1_lOD0a6Ug l79W@y85!?0$lPTx_|6i{$jJ4P0YrYtVPfDFZR7-}ZvZsWGnoJY literal 0 HcmV?d00001 diff --git a/config/api_config.py b/config/api_config.py new file mode 100644 index 0000000..6f85543 --- /dev/null +++ b/config/api_config.py @@ -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 diff --git a/config/environment.py b/config/environment.py new file mode 100644 index 0000000..cde5b38 --- /dev/null +++ b/config/environment.py @@ -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() diff --git a/config/session_config.py b/config/session_config.py new file mode 100644 index 0000000..f821148 --- /dev/null +++ b/config/session_config.py @@ -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 diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..b14b6fd --- /dev/null +++ b/config/settings.py @@ -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 diff --git a/config/ui_config.py b/config/ui_config.py new file mode 100644 index 0000000..7091d44 --- /dev/null +++ b/config/ui_config.py @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e00d456 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +selenium==3.141.0 +pytest==7.4.3 +urllib3<=2.0 +allure-pytest +dotenv +validators +colorama diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh new file mode 100755 index 0000000..08b90ea --- /dev/null +++ b/scripts/run_tests.sh @@ -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 diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/api/conftest.py b/tests/api/conftest.py new file mode 100644 index 0000000..64ac6c8 --- /dev/null +++ b/tests/api/conftest.py @@ -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) + diff --git a/tests/api/test_posts.py b/tests/api/test_posts.py new file mode 100644 index 0000000..ffddf14 --- /dev/null +++ b/tests/api/test_posts.py @@ -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 diff --git a/tests/api/test_users.py b/tests/api/test_users.py new file mode 100644 index 0000000..fff2d99 --- /dev/null +++ b/tests/api/test_users.py @@ -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}" diff --git a/tests/api/utils/api_client.py b/tests/api/utils/api_client.py new file mode 100644 index 0000000..54e99a7 --- /dev/null +++ b/tests/api/utils/api_client.py @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..089513f --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/data/api_posts.json b/tests/data/api_posts.json new file mode 100644 index 0000000..90f1b59 --- /dev/null +++ b/tests/data/api_posts.json @@ -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: пишем первое мобильное приложение" + } +] + diff --git a/tests/data/api_user_auth.json b/tests/data/api_user_auth.json new file mode 100644 index 0000000..126b948 --- /dev/null +++ b/tests/data/api_user_auth.json @@ -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" + } diff --git a/tests/data/api_users.json b/tests/data/api_users.json new file mode 100644 index 0000000..07cf48d --- /dev/null +++ b/tests/data/api_users.json @@ -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" + } +] diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/data_fixtures.py b/tests/fixtures/data_fixtures.py new file mode 100644 index 0000000..fd50f3a --- /dev/null +++ b/tests/fixtures/data_fixtures.py @@ -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') diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..5c587a5 --- /dev/null +++ b/tests/pytest.ini @@ -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 diff --git a/tests/ui/__init__.py b/tests/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ui/conftest.py b/tests/ui/conftest.py new file mode 100644 index 0000000..b358efe --- /dev/null +++ b/tests/ui/conftest.py @@ -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']) diff --git a/tests/ui/pages/__init__.py b/tests/ui/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ui/pages/admin_page.py b/tests/ui/pages/admin_page.py new file mode 100644 index 0000000..f1c4608 --- /dev/null +++ b/tests/ui/pages/admin_page.py @@ -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) diff --git a/tests/ui/pages/base_page.py b/tests/ui/pages/base_page.py new file mode 100644 index 0000000..71bffcf --- /dev/null +++ b/tests/ui/pages/base_page.py @@ -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) diff --git a/tests/ui/pages/home_page.py b/tests/ui/pages/home_page.py new file mode 100644 index 0000000..f19737c --- /dev/null +++ b/tests/ui/pages/home_page.py @@ -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) + diff --git a/tests/ui/test_admin_page.py b/tests/ui/test_admin_page.py new file mode 100644 index 0000000..b8d0462 --- /dev/null +++ b/tests/ui/test_admin_page.py @@ -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) + diff --git a/tests/ui/test_home_page.py b/tests/ui/test_home_page.py new file mode 100644 index 0000000..9e24e08 --- /dev/null +++ b/tests/ui/test_home_page.py @@ -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") + + diff --git a/tests/ui/utils/__init__.py b/tests/ui/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ui/utils/wait.py b/tests/ui/utils/wait.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__pycache__/logger.cpython-313.pyc b/utils/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92a1fbcd514a7095fecf792301da6034b3e26509 GIT binary patch literal 19973 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 literal 0 HcmV?d00001 diff --git a/utils/allure_helpers.py b/utils/allure_helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..946dc08 --- /dev/null +++ b/utils/logger.py @@ -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) diff --git a/utils/waiters.py b/utils/waiters.py new file mode 100644 index 0000000..0e45ad1 --- /dev/null +++ b/utils/waiters.py @@ -0,0 +1,4 @@ +from selenium.webdriver.support.ui import WebDriverWait + + +waiter: WebDriverWait