This commit is contained in:
2026-01-25 18:50:18 +03:00
parent 1dceab6479
commit 34b0345983
3 changed files with 292 additions and 184 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: help build up down logs clean restart status deploy backup restore update clean-monitoring monitoring check-deps check-bot-deps check-anonBot-deps auth-setup auth-add-user auth-reset .PHONY: help build up down logs clean restart status deploy backup restore update clean-monitoring monitoring check-deps check-bot-deps check-anonBot-deps auth-setup auth-add-user auth-reset format-check format format-diff import-check import-fix lint-check code-quality
help: ## Показать справку help: ## Показать справку
@echo "🏗️ Production Infrastructure - Доступные команды:" @echo "🏗️ Production Infrastructure - Доступные команды:"
@@ -329,3 +329,66 @@ auth-reset: ## Сбросить пароль для пользователя (ma
auth-list: ## Показать список пользователей мониторинга auth-list: ## Показать список пользователей мониторинга
@echo "👥 Monitoring users:" @echo "👥 Monitoring users:"
@sudo cat /etc/nginx/passwords/monitoring.htpasswd 2>/dev/null | cut -d: -f1 || echo "❌ No users found" @sudo cat /etc/nginx/passwords/monitoring.htpasswd 2>/dev/null | cut -d: -f1 || echo "❌ No users found"
# ========================================
# Code Quality & Formatting
# ========================================
format-check: ## Проверить форматирование кода (Black)
@echo "🔍 Checking code formatting with Black..."
@if [ -f .venv/bin/python ]; then \
.venv/bin/python -m black --check . || (echo "❌ Code formatting issues found. Run 'make format' to fix." && exit 1); \
else \
python3 -m black --check . || (echo "❌ Code formatting issues found. Run 'make format' to fix." && exit 1); \
fi
@echo "✅ Code formatting is correct!"
format: ## Автоматически исправить форматирование кода (Black)
@echo "🎨 Formatting code with Black..."
@if [ -f .venv/bin/python ]; then \
.venv/bin/python -m black .; \
else \
python3 -m black .; \
fi
@echo "✅ Code formatted!"
format-diff: ## Показать что будет изменено Black (без применения)
@echo "📋 Showing Black diff (no changes applied)..."
@if [ -f .venv/bin/python ]; then \
.venv/bin/python -m black --diff .; \
else \
python3 -m black --diff .; \
fi
import-check: ## Проверить сортировку импортов (isort)
@echo "🔍 Checking import sorting with isort..."
@if [ -f .venv/bin/python ]; then \
.venv/bin/python -m isort --check-only . || (echo "❌ Import sorting issues found. Run 'make import-fix' to fix." && exit 1); \
else \
python3 -m isort --check-only . || (echo "❌ Import sorting issues found. Run 'make import-fix' to fix." && exit 1); \
fi
@echo "✅ Import sorting is correct!"
import-fix: ## Автоматически исправить сортировку импортов (isort)
@echo "📦 Fixing import sorting with isort..."
@if [ -f .venv/bin/python ]; then \
.venv/bin/python -m isort .; \
else \
python3 -m isort .; \
fi
@echo "✅ Imports sorted!"
lint-check: ## Проверить код линтером (flake8) - только критические ошибки
@echo "🔍 Running flake8 linter (critical errors only)..."
@if [ -f .venv/bin/python ]; then \
.venv/bin/python -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=".venv,venv,__pycache__,.git,*.pyc" || true; \
else \
python3 -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=".venv,venv,__pycache__,.git,*.pyc" || true; \
fi
@echo "✅ Linting check completed (non-critical warnings in dependencies ignored)!"
code-quality: format-check import-check lint-check ## Проверить качество кода (все проверки)
@echo ""
@echo "✅ All code quality checks passed!"
@echo ""
@echo " Note: F821/F822/F824 warnings in bots/ are non-critical and ignored in CI"

View File

@@ -3,12 +3,12 @@
Тесты для конфигурации Prometheus Тесты для конфигурации Prometheus
""" """
import pytest
import yaml
import sys
import os import os
import sys
from pathlib import Path from pathlib import Path
import pytest
import yaml
class TestPrometheusConfig: class TestPrometheusConfig:
@@ -17,7 +17,12 @@ class TestPrometheusConfig:
@pytest.fixture @pytest.fixture
def prometheus_config_path(self): def prometheus_config_path(self):
"""Путь к файлу конфигурации Prometheus""" """Путь к файлу конфигурации Prometheus"""
return Path(__file__).parent.parent.parent / 'infra' / 'prometheus' / 'prometheus.yml' return (
Path(__file__).parent.parent.parent
/ "infra"
/ "prometheus"
/ "prometheus.yml"
)
@pytest.fixture @pytest.fixture
def prometheus_config(self, prometheus_config_path): def prometheus_config(self, prometheus_config_path):
@@ -25,100 +30,117 @@ class TestPrometheusConfig:
if not prometheus_config_path.exists(): if not prometheus_config_path.exists():
pytest.skip(f"Prometheus config file not found: {prometheus_config_path}") pytest.skip(f"Prometheus config file not found: {prometheus_config_path}")
with open(prometheus_config_path, 'r', encoding='utf-8') as f: with open(prometheus_config_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) return yaml.safe_load(f)
def test_config_file_exists(self, prometheus_config_path): def test_config_file_exists(self, prometheus_config_path):
"""Тест существования файла конфигурации""" """Тест существования файла конфигурации"""
assert prometheus_config_path.exists(), f"Prometheus config file not found: {prometheus_config_path}" assert (
prometheus_config_path.exists()
), f"Prometheus config file not found: {prometheus_config_path}"
def test_config_is_valid_yaml(self, prometheus_config): def test_config_is_valid_yaml(self, prometheus_config):
"""Тест валидности YAML конфигурации""" """Тест валидности YAML конфигурации"""
assert isinstance(prometheus_config, dict), "Config should be a valid YAML dictionary" assert isinstance(
prometheus_config, dict
), "Config should be a valid YAML dictionary"
def test_global_section(self, prometheus_config): def test_global_section(self, prometheus_config):
"""Тест глобальной секции конфигурации""" """Тест глобальной секции конфигурации"""
assert 'global' in prometheus_config, "Config should have global section" assert "global" in prometheus_config, "Config should have global section"
global_config = prometheus_config['global'] global_config = prometheus_config["global"]
assert 'scrape_interval' in global_config, "Global section should have scrape_interval" assert (
assert 'evaluation_interval' in global_config, "Global section should have evaluation_interval" "scrape_interval" in global_config
), "Global section should have scrape_interval"
assert (
"evaluation_interval" in global_config
), "Global section should have evaluation_interval"
# Проверяем значения интервалов # Проверяем значения интервалов
assert global_config['scrape_interval'] == '15s', "Default scrape_interval should be 15s" assert (
assert global_config['evaluation_interval'] == '15s', "Default evaluation_interval should be 15s" global_config["scrape_interval"] == "15s"
), "Default scrape_interval should be 15s"
assert (
global_config["evaluation_interval"] == "15s"
), "Default evaluation_interval should be 15s"
def test_scrape_configs_section(self, prometheus_config): def test_scrape_configs_section(self, prometheus_config):
"""Тест секции scrape_configs""" """Тест секции scrape_configs"""
assert 'scrape_configs' in prometheus_config, "Config should have scrape_configs section" assert (
"scrape_configs" in prometheus_config
), "Config should have scrape_configs section"
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
assert isinstance(scrape_configs, list), "scrape_configs should be a list" assert isinstance(scrape_configs, list), "scrape_configs should be a list"
assert len(scrape_configs) >= 1, "Should have at least one scrape config" assert len(scrape_configs) >= 1, "Should have at least one scrape config"
def test_prometheus_job(self, prometheus_config): def test_prometheus_job(self, prometheus_config):
"""Тест job для самого Prometheus""" """Тест job для самого Prometheus"""
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
# Ищем job для prometheus # Ищем job для prometheus
prometheus_job = None prometheus_job = None
for job in scrape_configs: for job in scrape_configs:
if job.get('job_name') == 'prometheus': if job.get("job_name") == "prometheus":
prometheus_job = job prometheus_job = job
break break
assert prometheus_job is not None, "Should have prometheus job" assert prometheus_job is not None, "Should have prometheus job"
assert 'static_configs' in prometheus_job, "Prometheus job should have static_configs" assert (
"static_configs" in prometheus_job
), "Prometheus job should have static_configs"
static_configs = prometheus_job['static_configs'] static_configs = prometheus_job["static_configs"]
assert isinstance(static_configs, list), "static_configs should be a list" assert isinstance(static_configs, list), "static_configs should be a list"
assert len(static_configs) > 0, "Should have at least one static config" assert len(static_configs) > 0, "Should have at least one static config"
# Проверяем targets # Проверяем targets
targets = static_configs[0].get('targets', []) targets = static_configs[0].get("targets", [])
assert 'localhost:9090' in targets, "Prometheus should scrape localhost:9090" assert "localhost:9090" in targets, "Prometheus should scrape localhost:9090"
def test_telegram_bot_job(self, prometheus_config): def test_telegram_bot_job(self, prometheus_config):
"""Тест job для telegram-helper-bot""" """Тест job для telegram-helper-bot"""
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
# Ищем job для telegram-helper-bot # Ищем job для telegram-helper-bot
bot_job = None bot_job = None
for job in scrape_configs: for job in scrape_configs:
if job.get('job_name') == 'telegram-helper-bot': if job.get("job_name") == "telegram-helper-bot":
bot_job = job bot_job = job
break break
assert bot_job is not None, "Should have telegram-helper-bot job" assert bot_job is not None, "Should have telegram-helper-bot job"
# Проверяем основные параметры # Проверяем основные параметры
assert 'static_configs' in bot_job, "Bot job should have static_configs" assert "static_configs" in bot_job, "Bot job should have static_configs"
assert 'metrics_path' in bot_job, "Bot job should have metrics_path" assert "metrics_path" in bot_job, "Bot job should have metrics_path"
assert 'scrape_interval' in bot_job, "Bot job should have scrape_interval" assert "scrape_interval" in bot_job, "Bot job should have scrape_interval"
assert 'scrape_timeout' in bot_job, "Bot job should have scrape_timeout" assert "scrape_timeout" in bot_job, "Bot job should have scrape_timeout"
assert 'honor_labels' in bot_job, "Bot job should have honor_labels" assert "honor_labels" in bot_job, "Bot job should have honor_labels"
# Проверяем значения # Проверяем значения
assert bot_job['metrics_path'] == '/metrics', "Metrics path should be /metrics" assert bot_job["metrics_path"] == "/metrics", "Metrics path should be /metrics"
assert bot_job['scrape_interval'] == '15s', "Scrape interval should be 15s" assert bot_job["scrape_interval"] == "15s", "Scrape interval should be 15s"
assert bot_job['scrape_timeout'] == '10s', "Scrape timeout should be 10s" assert bot_job["scrape_timeout"] == "10s", "Scrape timeout should be 10s"
assert bot_job['honor_labels'] is True, "honor_labels should be True" assert bot_job["honor_labels"] is True, "honor_labels should be True"
# Проверяем static_configs # Проверяем static_configs
static_configs = bot_job['static_configs'] static_configs = bot_job["static_configs"]
assert len(static_configs) > 0, "Should have at least one static config" assert len(static_configs) > 0, "Should have at least one static config"
# Проверяем targets # Проверяем targets
targets = static_configs[0].get('targets', []) targets = static_configs[0].get("targets", [])
assert 'bots_telegram_bot:8080' in targets, "Should scrape bots_telegram_bot:8080" assert (
"bots_telegram_bot:8080" in targets
), "Should scrape bots_telegram_bot:8080"
# Проверяем labels # Проверяем labels
labels = static_configs[0].get('labels', {}) labels = static_configs[0].get("labels", {})
expected_labels = { expected_labels = {
'bot_name': 'telegram-helper-bot', "bot_name": "telegram-helper-bot",
'environment': 'production', "environment": "production",
'service': 'telegram-bot' "service": "telegram-bot",
} }
for key, value in expected_labels.items(): for key, value in expected_labels.items():
@@ -127,114 +149,144 @@ class TestPrometheusConfig:
def test_alerting_section(self, prometheus_config): def test_alerting_section(self, prometheus_config):
"""Тест секции alerting""" """Тест секции alerting"""
assert 'alerting' in prometheus_config, "Config should have alerting section" assert "alerting" in prometheus_config, "Config should have alerting section"
alerting_config = prometheus_config['alerting'] alerting_config = prometheus_config["alerting"]
assert 'alertmanagers' in alerting_config, "Alerting section should have alertmanagers" assert (
"alertmanagers" in alerting_config
), "Alerting section should have alertmanagers"
alertmanagers = alerting_config['alertmanagers'] alertmanagers = alerting_config["alertmanagers"]
assert isinstance(alertmanagers, list), "alertmanagers should be a list" assert isinstance(alertmanagers, list), "alertmanagers should be a list"
# Проверяем, что alertmanager настроен правильно # Проверяем, что alertmanager настроен правильно
if len(alertmanagers) > 0: if len(alertmanagers) > 0:
for am in alertmanagers: for am in alertmanagers:
if 'static_configs' in am: if "static_configs" in am:
static_configs = am['static_configs'] static_configs = am["static_configs"]
assert isinstance(static_configs, list), "static_configs should be a list" assert isinstance(
static_configs, list
), "static_configs should be a list"
for sc in static_configs: for sc in static_configs:
if 'targets' in sc: if "targets" in sc:
targets = sc['targets'] targets = sc["targets"]
# targets может быть None если все строки закомментированы # targets может быть None если все строки закомментированы
if targets is not None: if targets is not None:
assert isinstance(targets, list), "targets should be a list" assert isinstance(
targets, list
), "targets should be a list"
# Проверяем, что targets не пустые и имеют правильный формат # Проверяем, что targets не пустые и имеют правильный формат
for target in targets: for target in targets:
assert isinstance(target, str), f"Target should be a string: {target}" assert isinstance(
target, str
), f"Target should be a string: {target}"
# Если target не закомментирован, проверяем формат # Если target не закомментирован, проверяем формат
if not target.startswith('#'): if not target.startswith("#"):
assert ':' in target, f"Target should have port: {target}" assert (
":" in target
), f"Target should have port: {target}"
def test_rule_files_section(self, prometheus_config): def test_rule_files_section(self, prometheus_config):
"""Тест секции rule_files""" """Тест секции rule_files"""
assert 'rule_files' in prometheus_config, "Config should have rule_files section" assert (
"rule_files" in prometheus_config
), "Config should have rule_files section"
rule_files = prometheus_config['rule_files'] rule_files = prometheus_config["rule_files"]
# rule_files может быть None если все строки закомментированы # rule_files может быть None если все строки закомментированы
if rule_files is not None: if rule_files is not None:
assert isinstance(rule_files, list), "rule_files should be a list" assert isinstance(rule_files, list), "rule_files should be a list"
# Проверяем, что rule files имеют правильный формат # Проверяем, что rule files имеют правильный формат
for rule_file in rule_files: for rule_file in rule_files:
assert isinstance(rule_file, str), f"Rule file should be a string: {rule_file}" assert isinstance(
rule_file, str
), f"Rule file should be a string: {rule_file}"
# Если rule file не закомментирован, проверяем, что это валидный путь # Если rule file не закомментирован, проверяем, что это валидный путь
if not rule_file.startswith('#'): if not rule_file.startswith("#"):
assert rule_file.endswith('.yml') or rule_file.endswith('.yaml'), \ assert rule_file.endswith(".yml") or rule_file.endswith(
f"Rule file should have .yml or .yaml extension: {rule_file}" ".yaml"
), f"Rule file should have .yml or .yaml extension: {rule_file}"
def test_config_structure_consistency(self, prometheus_config): def test_config_structure_consistency(self, prometheus_config):
"""Тест консистентности структуры конфигурации""" """Тест консистентности структуры конфигурации"""
# Проверяем, что все job'ы имеют одинаковую структуру # Проверяем, что все job'ы имеют одинаковую структуру
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
required_fields = ['job_name', 'static_configs'] required_fields = ["job_name", "static_configs"]
optional_fields = ['metrics_path', 'scrape_interval', 'scrape_timeout', 'honor_labels'] optional_fields = [
"metrics_path",
"scrape_interval",
"scrape_timeout",
"honor_labels",
]
for job in scrape_configs: for job in scrape_configs:
# Проверяем обязательные поля # Проверяем обязательные поля
for field in required_fields: for field in required_fields:
assert field in job, f"Job {job.get('job_name', 'unknown')} missing required field: {field}" assert (
field in job
), f"Job {job.get('job_name', 'unknown')} missing required field: {field}"
# Проверяем, что static_configs содержит targets # Проверяем, что static_configs содержит targets
static_configs = job['static_configs'] static_configs = job["static_configs"]
assert isinstance(static_configs, list), f"Job {job.get('job_name', 'unknown')} static_configs should be list" assert isinstance(
static_configs, list
), f"Job {job.get('job_name', 'unknown')} static_configs should be list"
for static_config in static_configs: for static_config in static_configs:
assert 'targets' in static_config, f"Static config should have targets" assert "targets" in static_config, f"Static config should have targets"
targets = static_config['targets'] targets = static_config["targets"]
assert isinstance(targets, list), "Targets should be a list" assert isinstance(targets, list), "Targets should be a list"
assert len(targets) > 0, "Targets should not be empty" assert len(targets) > 0, "Targets should not be empty"
def test_port_configurations(self, prometheus_config): def test_port_configurations(self, prometheus_config):
"""Тест конфигурации портов""" """Тест конфигурации портов"""
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
# Проверяем, что порты корректно настроены # Проверяем, что порты корректно настроены
for job in scrape_configs: for job in scrape_configs:
static_configs = job['static_configs'] static_configs = job["static_configs"]
for static_config in static_configs: for static_config in static_configs:
targets = static_config['targets'] targets = static_config["targets"]
for target in targets: for target in targets:
if ':' in target: if ":" in target:
host, port = target.split(':', 1) host, port = target.split(":", 1)
# Проверяем, что порт это число # Проверяем, что порт это число
try: try:
port_num = int(port) port_num = int(port)
assert 1 <= port_num <= 65535, f"Port {port_num} out of range" assert (
1 <= port_num <= 65535
), f"Port {port_num} out of range"
except ValueError: except ValueError:
# Это может быть Docker service name без порта # Это может быть Docker service name без порта
pass pass
def test_environment_labels(self, prometheus_config): def test_environment_labels(self, prometheus_config):
"""Тест labels окружения""" """Тест labels окружения"""
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
# Проверяем, что production окружение правильно помечено # Проверяем, что production окружение правильно помечено
for job in scrape_configs: for job in scrape_configs:
if job.get('job_name') == 'telegram-helper-bot': if job.get("job_name") == "telegram-helper-bot":
static_configs = job['static_configs'] static_configs = job["static_configs"]
for static_config in static_configs: for static_config in static_configs:
labels = static_config.get('labels', {}) labels = static_config.get("labels", {})
if 'environment' in labels: if "environment" in labels:
assert labels['environment'] == 'production', "Environment should be production" assert (
labels["environment"] == "production"
), "Environment should be production"
def test_metrics_path_consistency(self, prometheus_config): def test_metrics_path_consistency(self, prometheus_config):
"""Тест консистентности paths для метрик""" """Тест консистентности paths для метрик"""
scrape_configs = prometheus_config['scrape_configs'] scrape_configs = prometheus_config["scrape_configs"]
# Проверяем, что все job'ы используют /metrics # Проверяем, что все job'ы используют /metrics
for job in scrape_configs: for job in scrape_configs:
if 'metrics_path' in job: if "metrics_path" in job:
assert job['metrics_path'] == '/metrics', f"Job {job.get('job_name', 'unknown')} should use /metrics path" assert (
job["metrics_path"] == "/metrics"
), f"Job {job.get('job_name', 'unknown')} should use /metrics path"
class TestPrometheusConfigValidation: class TestPrometheusConfigValidation:
@@ -244,73 +296,58 @@ class TestPrometheusConfigValidation:
def sample_valid_config(self): def sample_valid_config(self):
"""Пример валидной конфигурации""" """Пример валидной конфигурации"""
return { return {
'global': { "global": {"scrape_interval": "15s", "evaluation_interval": "15s"},
'scrape_interval': '15s', "scrape_configs": [
'evaluation_interval': '15s'
},
'scrape_configs': [
{ {
'job_name': 'test', "job_name": "test",
'static_configs': [ "static_configs": [{"targets": ["localhost:9090"]}],
{
'targets': ['localhost:9090']
}
]
} }
] ],
} }
def test_minimal_valid_config(self, sample_valid_config): def test_minimal_valid_config(self, sample_valid_config):
"""Тест минимальной валидной конфигурации""" """Тест минимальной валидной конфигурации"""
# Проверяем, что конфигурация содержит все необходимые поля # Проверяем, что конфигурация содержит все необходимые поля
assert 'global' in sample_valid_config assert "global" in sample_valid_config
assert 'scrape_configs' in sample_valid_config assert "scrape_configs" in sample_valid_config
global_config = sample_valid_config['global'] global_config = sample_valid_config["global"]
assert 'scrape_interval' in global_config assert "scrape_interval" in global_config
assert 'evaluation_interval' in global_config assert "evaluation_interval" in global_config
scrape_configs = sample_valid_config['scrape_configs'] scrape_configs = sample_valid_config["scrape_configs"]
assert len(scrape_configs) > 0 assert len(scrape_configs) > 0
for job in scrape_configs: for job in scrape_configs:
assert 'job_name' in job assert "job_name" in job
assert 'static_configs' in job assert "static_configs" in job
static_configs = job['static_configs'] static_configs = job["static_configs"]
assert len(static_configs) > 0 assert len(static_configs) > 0
for static_config in static_configs: for static_config in static_configs:
assert 'targets' in static_config assert "targets" in static_config
targets = static_config['targets'] targets = static_config["targets"]
assert len(targets) > 0 assert len(targets) > 0
def test_config_without_required_fields(self): def test_config_without_required_fields(self):
"""Тест конфигурации без обязательных полей""" """Тест конфигурации без обязательных полей"""
# Конфигурация без global секции # Конфигурация без global секции
config_without_global = { config_without_global = {"scrape_configs": []}
'scrape_configs': []
}
# Конфигурация без scrape_configs # Конфигурация без scrape_configs
config_without_scrape = { config_without_scrape = {"global": {"scrape_interval": "15s"}}
'global': {
'scrape_interval': '15s'
}
}
# Конфигурация с пустыми scrape_configs # Конфигурация с пустыми scrape_configs
config_empty_scrape = { config_empty_scrape = {
'global': { "global": {"scrape_interval": "15s"},
'scrape_interval': '15s' "scrape_configs": [],
},
'scrape_configs': []
} }
# Все эти конфигурации должны быть невалидными # Все эти конфигурации должны быть невалидными
assert 'global' not in config_without_global assert "global" not in config_without_global
assert 'scrape_configs' not in config_without_scrape assert "scrape_configs" not in config_without_scrape
assert len(config_empty_scrape['scrape_configs']) == 0 assert len(config_empty_scrape["scrape_configs"]) == 0
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -3,27 +3,35 @@
Тест конфигурации pytest Тест конфигурации pytest
""" """
import pytest
import os import os
import sys import sys
import pytest
def test_pytest_config_loaded(): def test_pytest_config_loaded():
"""Проверяем, что конфигурация pytest загружена""" """Проверяем, что конфигурация pytest загружена"""
# Проверяем, что мы находимся в корневой директории проекта # Проверяем, что мы находимся в корневой директории проекта
assert os.path.exists('pytest.ini'), "pytest.ini должен существовать в корне проекта" assert os.path.exists(
"pytest.ini"
), "pytest.ini должен существовать в корне проекта"
# Проверяем, что директория tests существует # Проверяем, что директория tests существует
assert os.path.exists('tests'), "Директория tests должна существовать" assert os.path.exists("tests"), "Директория tests должна существовать"
assert os.path.exists('tests/infra'), "Директория tests/infra должна существовать" assert os.path.exists("tests/infra"), "Директория tests/infra должна существовать"
assert os.path.exists('tests/bot'), "Директория tests/bot должна существовать" assert os.path.exists("tests/bot"), "Директория tests/bot должна существовать"
def test_test_structure(): def test_test_structure():
"""Проверяем структуру тестов""" """Проверяем структуру тестов"""
# Проверяем наличие __init__.py файлов # Проверяем наличие __init__.py файлов
assert os.path.exists('tests/__init__.py'), "tests/__init__.py должен существовать" assert os.path.exists("tests/__init__.py"), "tests/__init__.py должен существовать"
assert os.path.exists('tests/infra/__init__.py'), "tests/infra/__init__.py должен существовать" assert os.path.exists(
assert os.path.exists('tests/bot/__init__.py'), "tests/bot/__init__.py должен существовать" "tests/infra/__init__.py"
), "tests/infra/__init__.py должен существовать"
assert os.path.exists(
"tests/bot/__init__.py"
), "tests/bot/__init__.py должен существовать"
if __name__ == "__main__": if __name__ == "__main__":