""" Тесты для helper_bot.server_prometheus: MetricsServer, start_metrics_server, stop_metrics_server. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest from aiohttp import web from helper_bot.server_prometheus import ( MetricsServer, start_metrics_server, stop_metrics_server, ) @pytest.mark.unit @pytest.mark.asyncio class TestMetricsServer: """Тесты для класса MetricsServer.""" def test_init_sets_host_port_and_routes(self): """При инициализации задаются host, port и маршруты /metrics, /health.""" server = MetricsServer(host="127.0.0.1", port=9090) assert server.host == "127.0.0.1" assert server.port == 9090 assert server.runner is None assert server.site is None paths = [] for res in server.app.router.resources(): info = res.get_info() path = info.get("path") or info.get("formatter") if path: paths.append(path) assert "/metrics" in paths assert "/health" in paths @patch("helper_bot.server_prometheus.metrics") async def test_metrics_handler_success_returns_prometheus_content( self, mock_metrics_module ): """metrics_handler при успехе возвращает 200 и данные метрик.""" mock_metrics_module.get_metrics.return_value = ( b"# TYPE bot_commands_total counter" ) server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.metrics_handler(request) assert response.status == 200 assert response.body == b"# TYPE bot_commands_total counter" assert "text/plain" in response.content_type mock_metrics_module.get_metrics.assert_called_once() @patch("helper_bot.server_prometheus.metrics", None) async def test_metrics_handler_when_metrics_none_returns_500(self): """metrics_handler при недоступности metrics возвращает 500.""" server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.metrics_handler(request) assert response.status == 500 assert "Metrics not available" in response.text @patch("helper_bot.server_prometheus.metrics") async def test_metrics_handler_on_exception_returns_500(self, mock_metrics_module): """metrics_handler при исключении в get_metrics возвращает 500.""" mock_metrics_module.get_metrics.side_effect = RuntimeError("metrics error") server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.metrics_handler(request) assert response.status == 500 assert "Error generating metrics" in response.text @patch("helper_bot.server_prometheus.metrics") async def test_health_handler_success_returns_ok(self, mock_metrics_module): """health_handler при успехе возвращает 200 OK.""" mock_metrics_module.get_metrics.return_value = b"some_metrics_data" server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.health_handler(request) assert response.status == 200 assert response.text == "OK" @patch("helper_bot.server_prometheus.metrics", None) async def test_health_handler_when_metrics_none_returns_503(self): """health_handler при недоступности metrics возвращает 503.""" server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.health_handler(request) assert response.status == 503 assert "Metrics not available" in response.text @patch("helper_bot.server_prometheus.metrics") async def test_health_handler_empty_metrics_returns_503(self, mock_metrics_module): """health_handler при пустых метриках возвращает 503.""" mock_metrics_module.get_metrics.return_value = b"" server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.health_handler(request) assert response.status == 503 assert "Empty metrics" in response.text @patch("helper_bot.server_prometheus.metrics") async def test_health_handler_get_metrics_raises_returns_503( self, mock_metrics_module ): """health_handler при исключении get_metrics возвращает 503.""" mock_metrics_module.get_metrics.side_effect = ValueError("gen failed") server = MetricsServer(host="0.0.0.0", port=8080) request = MagicMock(spec=web.Request) response = await server.health_handler(request) assert response.status == 503 assert "Metrics generation failed" in response.text @patch("helper_bot.server_prometheus.web.AppRunner") @patch("helper_bot.server_prometheus.web.TCPSite") async def test_start_creates_runner_and_site( self, mock_tcp_site_cls, mock_app_runner_cls ): """start() создаёт AppRunner и TCPSite и запускает сервер.""" mock_runner = MagicMock() mock_runner.setup = AsyncMock() mock_app_runner_cls.return_value = mock_runner mock_site = MagicMock() mock_site.start = AsyncMock() mock_tcp_site_cls.return_value = mock_site server = MetricsServer(host="0.0.0.0", port=19998) await server.start() mock_app_runner_cls.assert_called_once_with(server.app) mock_runner.setup.assert_awaited_once() mock_tcp_site_cls.assert_called_once_with(mock_runner, "0.0.0.0", 19998) mock_site.start.assert_awaited_once() assert server.runner is mock_runner assert server.site is mock_site async def test_stop_stops_site_and_cleans_runner(self): """stop() останавливает site и очищает runner.""" server = MetricsServer(host="0.0.0.0", port=8080) server.site = MagicMock() server.site.stop = AsyncMock() server.runner = MagicMock() server.runner.cleanup = AsyncMock() await server.stop() server.site.stop.assert_awaited_once() server.runner.cleanup.assert_awaited_once() async def test_stop_when_site_none_does_not_raise(self): """stop() при site=None не падает.""" server = MetricsServer(host="0.0.0.0", port=8080) server.site = None server.runner = None await server.stop() @patch.object(MetricsServer, "start", new_callable=AsyncMock) @patch.object(MetricsServer, "stop", new_callable=AsyncMock) async def test_context_manager_enters_and_exits(self, mock_stop, mock_start): """Использование как async context manager вызывает start и stop.""" mock_start.return_value = None server = MetricsServer(host="0.0.0.0", port=8080) async with server: pass mock_start.assert_awaited_once() mock_stop.assert_awaited_once() @patch.object(MetricsServer, "start", new_callable=AsyncMock) @patch.object(MetricsServer, "stop", new_callable=AsyncMock) async def test_context_manager_exit_calls_stop_on_exception( self, mock_stop, mock_start ): """При исключении внутри контекста stop всё равно вызывается.""" mock_start.return_value = None server = MetricsServer(host="0.0.0.0", port=8080) with pytest.raises(ValueError): async with server: raise ValueError("test") mock_stop.assert_awaited_once() @pytest.mark.unit @pytest.mark.asyncio class TestStartStopMetricsServer: """Тесты для start_metrics_server и stop_metrics_server.""" @patch("helper_bot.server_prometheus.MetricsServer") async def test_start_metrics_server_creates_and_starts_server( self, mock_server_cls ): """start_metrics_server создаёт MetricsServer и вызывает start().""" mock_instance = MagicMock() mock_instance.start = AsyncMock() mock_server_cls.return_value = mock_instance result = await start_metrics_server("0.0.0.0", 8080) mock_server_cls.assert_called_once_with("0.0.0.0", 8080) mock_instance.start.assert_awaited_once() assert result is mock_instance @patch("helper_bot.server_prometheus.MetricsServer") async def test_stop_metrics_server_when_running_stops_and_clears_global( self, mock_server_cls ): """stop_metrics_server при запущенном сервере останавливает его и обнуляет глобальную переменную.""" import helper_bot.server_prometheus as mod mock_instance = MagicMock() mock_instance.stop = AsyncMock() old_server = mod.metrics_server mod.metrics_server = mock_instance try: await stop_metrics_server() mock_instance.stop.assert_awaited_once() assert mod.metrics_server is None finally: mod.metrics_server = old_server async def test_stop_metrics_server_when_none_does_not_raise(self): """stop_metrics_server при metrics_server=None не падает.""" import helper_bot.server_prometheus as mod old_server = mod.metrics_server mod.metrics_server = None try: await stop_metrics_server() assert mod.metrics_server is None finally: mod.metrics_server = old_server