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]