import json from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from fastapi import FastAPI, status from app.config import settings from app.services.payment_service import PaymentService # ensure backup directory exists before importing the unified app to avoid side effects during module import _backup_dir = Path('data/backups') _backup_dir.mkdir(parents=True, exist_ok=True) from app.webserver.unified_app import create_unified_app @pytest.mark.anyio async def test_unified_app_health_reports_features(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: bot = AsyncMock() dispatcher = SimpleNamespace(feed_update=AsyncMock()) payment_service = AsyncMock(spec=PaymentService) miniapp_static_dir = tmp_path / 'miniapp' miniapp_static_dir.mkdir() monkeypatch.setattr(settings, 'WEB_API_ENABLED', True, raising=False) monkeypatch.setattr(settings, 'TRIBUTE_ENABLED', True, raising=False) monkeypatch.setattr(settings, 'WEBHOOK_URL', 'https://hooks.example.com', raising=False) monkeypatch.setattr(settings, 'WEBHOOK_PATH', '/telegram-webhook', raising=False) monkeypatch.setattr(settings, 'WEBHOOK_SECRET_TOKEN', 'super-secret', raising=False) monkeypatch.setattr(settings, 'BOT_RUN_MODE', 'webhook', raising=False) monkeypatch.setattr(settings, 'WEBHOOK_MAX_QUEUE_SIZE', 8, raising=False) monkeypatch.setattr(settings, 'WEBHOOK_WORKERS', 1, raising=False) monkeypatch.setattr(settings, 'WEBHOOK_ENQUEUE_TIMEOUT', 0.0, raising=False) monkeypatch.setattr(settings, 'WEBHOOK_WORKER_SHUTDOWN_TIMEOUT', 1.0, raising=False) monkeypatch.setattr(settings, 'MINIAPP_STATIC_PATH', str(miniapp_static_dir), raising=False) app = create_unified_app( bot, dispatcher, # type: ignore[arg-type] payment_service, enable_telegram_webhook=True, ) health_route = next( route for route in app.routes if getattr(route, 'endpoint', None) and getattr(route.endpoint, '__name__', '') == 'unified_health' ) assert getattr(health_route, 'path', None) == '/health/unified' await app.router.startup() try: response = await health_route.endpoint() # type: ignore[func-returns-value] finally: await app.router.shutdown() payload = json.loads(response.body.decode('utf-8')) # type: ignore[attr-defined] assert payload['status'] == 'ok' assert payload['web_api_enabled'] is True assert payload['bot_run_mode'] == 'webhook' assert payload['telegram_webhook']['enabled'] is True assert payload['telegram_webhook']['running'] is True assert payload['telegram_webhook']['path'] == '/telegram-webhook' assert payload['telegram_webhook']['secret_configured'] is True assert payload['payment_webhooks']['enabled'] is True assert payload['payment_webhooks']['providers']['tribute'] is True assert payload['miniapp_static']['mounted'] is True assert payload['miniapp_static']['path'].endswith('miniapp') def _build_unified_app(monkeypatch: pytest.MonkeyPatch, docs_enabled: bool) -> FastAPI: bot = AsyncMock() dispatcher = SimpleNamespace(feed_update=AsyncMock()) payment_service = AsyncMock(spec=PaymentService) monkeypatch.setattr(settings, 'WEB_API_ENABLED', False, raising=False) monkeypatch.setattr(settings, 'WEB_API_DOCS_ENABLED', docs_enabled, raising=False) monkeypatch.setattr(settings, 'MINIAPP_STATIC_PATH', 'miniapp', raising=False) return create_unified_app( bot, dispatcher, # type: ignore[arg-type] payment_service, enable_telegram_webhook=False, ) @pytest.mark.anyio async def test_unified_app_health_path_without_admin(monkeypatch: pytest.MonkeyPatch) -> None: bot = AsyncMock() dispatcher = SimpleNamespace(feed_update=AsyncMock()) payment_service = AsyncMock(spec=PaymentService) monkeypatch.setattr(settings, 'WEB_API_ENABLED', False, raising=False) monkeypatch.setattr(settings, 'MINIAPP_STATIC_PATH', 'miniapp', raising=False) app = create_unified_app( bot, dispatcher, # type: ignore[arg-type] payment_service, enable_telegram_webhook=False, ) health_route = next( route for route in app.routes if getattr(route, 'endpoint', None) and getattr(route.endpoint, '__name__', '') == 'unified_health' ) assert getattr(health_route, 'path', None) == '/health' def test_unified_app_docs_disabled(monkeypatch: pytest.MonkeyPatch) -> None: app = _build_unified_app(monkeypatch, docs_enabled=False) assert app.docs_url is None assert app.redoc_url is None assert app.openapi_url is None registered_paths = {getattr(route, 'path', None) for route in app.routes} assert '/doc' not in registered_paths @pytest.mark.anyio async def test_unified_app_docs_enabled_with_alias(monkeypatch: pytest.MonkeyPatch) -> None: app = _build_unified_app(monkeypatch, docs_enabled=True) assert app.docs_url == '/docs' assert app.openapi_url == '/openapi.json' alias_route = next( (route for route in app.routes if getattr(route, 'path', None) == '/doc'), None, ) assert alias_route is not None assert getattr(alias_route, 'include_in_schema', True) is False redoc_route = next( (route for route in app.routes if getattr(route, 'path', None) == '/redoc'), None, ) assert redoc_route is not None assert getattr(redoc_route, 'include_in_schema', True) is False response = await alias_route.endpoint() # type: ignore[func-returns-value] assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT assert response.headers['location'] == '/docs' redoc_response = await redoc_route.endpoint() # type: ignore[func-returns-value] assert b'ReDoc' in redoc_response.body # type: ignore[attr-defined]