117 lines
5.1 KiB
Python
117 lines
5.1 KiB
Python
|
|
"""
|
|||
|
|
BATON-FIX-011: Проверяет, что BOT_TOKEN не попадает в httpx-логи.
|
|||
|
|
|
|||
|
|
1. logging.getLogger('httpx').level >= logging.WARNING после импорта приложения.
|
|||
|
|
2. Дочерние логгеры httpx._client и httpx._async_client также не пишут INFO.
|
|||
|
|
3. При вызове send_message ни одна запись httpx-логгера с уровнем INFO
|
|||
|
|
не содержит 'bot' или токен-подобный паттерн /bot[0-9]+:/.
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
import re
|
|||
|
|
|
|||
|
|
import httpx
|
|||
|
|
import pytest
|
|||
|
|
import respx
|
|||
|
|
from unittest.mock import patch, AsyncMock
|
|||
|
|
|
|||
|
|
# conftest.py уже устанавливает BOT_TOKEN=test-bot-token до этого импорта
|
|||
|
|
from backend import config
|
|||
|
|
from backend.telegram import send_message
|
|||
|
|
|
|||
|
|
SEND_URL = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
|
|||
|
|
BOT_TOKEN_PATTERN = re.compile(r"bot[0-9]+:")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Уровень логгера httpx
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def test_httpx_logger_level_is_warning_or_higher():
|
|||
|
|
"""logging.getLogger('httpx').level должен быть WARNING (30) или выше после импорта приложения."""
|
|||
|
|
# Импортируем main, чтобы гарантировать, что setLevel уже вызван
|
|||
|
|
import backend.main # noqa: F401
|
|||
|
|
|
|||
|
|
httpx_logger = logging.getLogger("httpx")
|
|||
|
|
assert httpx_logger.level >= logging.WARNING, (
|
|||
|
|
f"Ожидался уровень >= WARNING (30), получен {httpx_logger.level}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_httpx_logger_info_not_enabled():
|
|||
|
|
"""logging.getLogger('httpx').isEnabledFor(INFO) должен возвращать False."""
|
|||
|
|
import backend.main # noqa: F401
|
|||
|
|
|
|||
|
|
httpx_logger = logging.getLogger("httpx")
|
|||
|
|
assert not httpx_logger.isEnabledFor(logging.INFO), (
|
|||
|
|
"httpx-логгер не должен обрабатывать INFO-сообщения"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_httpx_client_logger_info_not_enabled():
|
|||
|
|
"""Дочерний логгер httpx._client не должен обрабатывать INFO."""
|
|||
|
|
import backend.main # noqa: F401
|
|||
|
|
|
|||
|
|
child_logger = logging.getLogger("httpx._client")
|
|||
|
|
assert not child_logger.isEnabledFor(logging.INFO), (
|
|||
|
|
"httpx._client не должен обрабатывать INFO-сообщения"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_httpx_async_client_logger_info_not_enabled():
|
|||
|
|
"""Дочерний логгер httpx._async_client не должен обрабатывать INFO."""
|
|||
|
|
import backend.main # noqa: F401
|
|||
|
|
|
|||
|
|
child_logger = logging.getLogger("httpx._async_client")
|
|||
|
|
assert not child_logger.isEnabledFor(logging.INFO), (
|
|||
|
|
"httpx._async_client не должен обрабатывать INFO-сообщения"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# BOT_TOKEN не появляется в httpx INFO-логах при send_message
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_send_message_no_httpx_records_at_warning_level(caplog):
|
|||
|
|
"""При вызове send_message httpx не выдаёт записей уровня WARNING и ниже с токеном.
|
|||
|
|
|
|||
|
|
Проверяет фактическое состояние логгера в продакшне (WARNING): INFO-сообщения
|
|||
|
|
с URL (включая BOT_TOKEN) не должны проходить через httpx-логгер.
|
|||
|
|
"""
|
|||
|
|
import backend.main # noqa: F401 — убеждаемся, что setLevel вызван
|
|||
|
|
|
|||
|
|
with respx.mock(assert_all_called=False) as mock:
|
|||
|
|
mock.post(SEND_URL).mock(return_value=httpx.Response(200, json={"ok": True}))
|
|||
|
|
|
|||
|
|
# Захватываем логи при реальном уровне WARNING — INFO-сообщения не должны проходить
|
|||
|
|
with caplog.at_level(logging.WARNING, logger="httpx"):
|
|||
|
|
await send_message("test message for token leak check")
|
|||
|
|
|
|||
|
|
bot_token = config.BOT_TOKEN
|
|||
|
|
httpx_records = [r for r in caplog.records if r.name.startswith("httpx")]
|
|||
|
|
for record in httpx_records:
|
|||
|
|
assert bot_token not in record.message, (
|
|||
|
|
f"BOT_TOKEN найден в httpx-логе (уровень {record.levelname}): {record.message!r}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_send_message_no_token_pattern_in_httpx_info_logs(caplog):
|
|||
|
|
"""При вызове send_message httpx INFO-логи не содержат паттерн /bot[0-9]+:/."""
|
|||
|
|
with respx.mock(assert_all_called=False) as mock:
|
|||
|
|
mock.post(SEND_URL).mock(return_value=httpx.Response(200, json={"ok": True}))
|
|||
|
|
|
|||
|
|
with caplog.at_level(logging.INFO, logger="httpx"):
|
|||
|
|
await send_message("check token pattern")
|
|||
|
|
|
|||
|
|
info_records = [
|
|||
|
|
r for r in caplog.records
|
|||
|
|
if r.name.startswith("httpx") and r.levelno <= logging.INFO
|
|||
|
|
]
|
|||
|
|
for record in info_records:
|
|||
|
|
assert not BOT_TOKEN_PATTERN.search(record.message), (
|
|||
|
|
f"Паттерн bot[0-9]+: найден в httpx INFO-логе: {record.message!r}"
|
|||
|
|
)
|