free-claude-code/tests/api/test_app_lifespan_and_errors.py
Alishahryar1 48b085950a
Some checks are pending
CI / checks (push) Waiting to run
Warn on inherited auth token
2026-04-24 00:42:33 -07:00

370 lines
12 KiB
Python

import importlib
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi.testclient import TestClient
def test_warn_if_process_auth_token_logs_warning():
api_app_mod = importlib.import_module("api.app")
settings = SimpleNamespace(uses_process_anthropic_auth_token=lambda: True)
with patch.object(api_app_mod.logger, "warning") as warning:
api_app_mod._warn_if_process_auth_token(settings)
warning.assert_called_once()
assert "ANTHROPIC_AUTH_TOKEN" in warning.call_args.args[0]
def test_warn_if_process_auth_token_skips_explicit_dotenv_config():
api_app_mod = importlib.import_module("api.app")
settings = SimpleNamespace(uses_process_anthropic_auth_token=lambda: False)
with patch.object(api_app_mod.logger, "warning") as warning:
api_app_mod._warn_if_process_auth_token(settings)
warning.assert_not_called()
def test_create_app_provider_error_handler_returns_anthropic_format():
from api.app import create_app
from providers.exceptions import AuthenticationError
app = create_app()
@app.get("/raise_provider")
async def _raise_provider():
raise AuthenticationError("bad key")
api_app_mod = importlib.import_module("api.app")
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token=None,
allowed_telegram_user_id=None,
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir="",
claude_workspace="./agent_workspace",
host="127.0.0.1",
port=8082,
log_file="server.log",
)
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=AsyncMock()),
):
with TestClient(app) as client:
resp = client.get("/raise_provider")
assert resp.status_code == 401
body = resp.json()
assert body["type"] == "error"
assert body["error"]["type"] == "authentication_error"
def test_create_app_general_exception_handler_returns_500():
from api.app import create_app
app = create_app()
@app.get("/raise_general")
async def _raise_general():
raise RuntimeError("boom")
api_app_mod = importlib.import_module("api.app")
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token=None,
allowed_telegram_user_id=None,
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir="",
claude_workspace="./agent_workspace",
host="127.0.0.1",
port=8082,
log_file="server.log",
)
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=AsyncMock()),
):
with TestClient(app, raise_server_exceptions=False) as client:
resp = client.get("/raise_general")
assert resp.status_code == 500
body = resp.json()
assert body["type"] == "error"
assert body["error"]["type"] == "api_error"
@pytest.mark.parametrize(
"messaging_enabled", [True, False], ids=["with_platform", "no_platform"]
)
def test_app_lifespan_sets_state_and_cleans_up(tmp_path, messaging_enabled):
from api.app import create_app
app = create_app()
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token="token" if messaging_enabled else None,
allowed_telegram_user_id="123",
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir=str(tmp_path / "workspace"),
claude_workspace=str(tmp_path / "data"),
host="127.0.0.1",
port=8082,
log_file=str(tmp_path / "server.log"),
)
fake_platform = MagicMock()
fake_platform.name = "fake"
fake_platform.on_message = MagicMock()
fake_platform.start = AsyncMock()
fake_platform.stop = AsyncMock()
session_store = MagicMock()
session_store.get_all_trees.return_value = [{"t": 1}] if messaging_enabled else []
session_store.get_node_mapping.return_value = {"n": "t"}
session_store.sync_from_tree_data = MagicMock()
fake_queue = MagicMock()
fake_queue.cleanup_stale_nodes.return_value = 1
fake_queue.to_dict.return_value = {
"trees": [{"t": 1}],
"node_to_tree": {"n": "t"},
}
cli_manager = MagicMock()
cli_manager.stop_all = AsyncMock()
api_app_mod = importlib.import_module("api.app")
cleanup_provider = AsyncMock()
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
patch(
"messaging.platforms.factory.create_messaging_platform",
return_value=fake_platform if messaging_enabled else None,
) as create_platform,
patch("messaging.session.SessionStore", return_value=session_store),
patch("cli.manager.CLISessionManager", return_value=cli_manager),
patch(
"messaging.trees.queue_manager.TreeQueueManager.from_dict",
return_value=fake_queue,
),
TestClient(app),
):
pass
if messaging_enabled:
create_platform.assert_called_once()
fake_platform.on_message.assert_called_once()
fake_platform.start.assert_awaited_once()
fake_platform.stop.assert_awaited_once()
cli_manager.stop_all.assert_awaited_once()
assert getattr(app.state, "message_handler", None) is not None
session_store.sync_from_tree_data.assert_called_once_with(
[{"t": 1}],
{"n": "t"},
)
else:
fake_platform.start.assert_not_awaited()
fake_platform.stop.assert_not_awaited()
cli_manager.stop_all.assert_not_awaited()
assert getattr(app.state, "messaging_platform", "missing") is None
cleanup_provider.assert_awaited_once()
def test_app_lifespan_cleanup_continues_if_platform_stop_raises(tmp_path):
from api.app import create_app
app = create_app()
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token="token",
allowed_telegram_user_id="123",
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir=str(tmp_path / "workspace"),
claude_workspace=str(tmp_path / "data"),
host="127.0.0.1",
port=8082,
log_file=str(tmp_path / "server.log"),
)
fake_platform = MagicMock()
fake_platform.name = "fake"
fake_platform.on_message = MagicMock()
fake_platform.start = AsyncMock()
fake_platform.stop = AsyncMock(side_effect=RuntimeError("stop failed"))
session_store = MagicMock()
session_store.get_all_trees.return_value = []
session_store.get_node_mapping.return_value = {}
session_store.sync_from_tree_data = MagicMock()
cli_manager = MagicMock()
cli_manager.stop_all = AsyncMock()
api_app_mod = importlib.import_module("api.app")
cleanup_provider = AsyncMock()
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
patch(
"messaging.platforms.factory.create_messaging_platform",
return_value=fake_platform,
),
patch("messaging.session.SessionStore", return_value=session_store),
patch("cli.manager.CLISessionManager", return_value=cli_manager),
TestClient(app),
):
pass
fake_platform.stop.assert_awaited_once()
cli_manager.stop_all.assert_awaited_once()
cleanup_provider.assert_awaited_once()
def test_app_lifespan_messaging_import_error_no_crash(tmp_path, caplog):
"""Messaging import failure logs warning and continues without crash."""
from api.app import create_app
app = create_app()
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token="token",
allowed_telegram_user_id="123",
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir=str(tmp_path / "workspace"),
claude_workspace=str(tmp_path / "data"),
host="127.0.0.1",
port=8082,
log_file=str(tmp_path / "server.log"),
)
api_app_mod = importlib.import_module("api.app")
cleanup_provider = AsyncMock()
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
patch(
"messaging.platforms.factory.create_messaging_platform",
side_effect=ImportError("discord not installed"),
),
TestClient(app),
):
pass
assert getattr(app.state, "messaging_platform", None) is None
cleanup_provider.assert_awaited_once()
def test_app_lifespan_platform_start_exception_cleanup_still_runs(tmp_path):
"""Exception during platform.start() logs error, cleanup still runs."""
from api.app import create_app
app = create_app()
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token="token",
allowed_telegram_user_id="123",
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir=str(tmp_path / "workspace"),
claude_workspace=str(tmp_path / "data"),
host="127.0.0.1",
port=8082,
log_file=str(tmp_path / "server.log"),
)
fake_platform = MagicMock()
fake_platform.name = "fake"
fake_platform.on_message = MagicMock()
fake_platform.start = AsyncMock(side_effect=RuntimeError("start failed"))
fake_platform.stop = AsyncMock()
session_store = MagicMock()
session_store.get_all_trees.return_value = []
session_store.get_node_mapping.return_value = {}
session_store.sync_from_tree_data = MagicMock()
cli_manager = MagicMock()
cli_manager.stop_all = AsyncMock()
api_app_mod = importlib.import_module("api.app")
cleanup_provider = AsyncMock()
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
patch(
"messaging.platforms.factory.create_messaging_platform",
return_value=fake_platform,
),
patch("messaging.session.SessionStore", return_value=session_store),
patch("cli.manager.CLISessionManager", return_value=cli_manager),
TestClient(app),
):
pass
cleanup_provider.assert_awaited_once()
def test_app_lifespan_flush_pending_save_exception_warning_only(tmp_path):
"""Session store flush exception on shutdown is logged as warning, no crash."""
from api.app import create_app
app = create_app()
settings = SimpleNamespace(
messaging_platform="telegram",
telegram_bot_token="token",
allowed_telegram_user_id="123",
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir=str(tmp_path / "workspace"),
claude_workspace=str(tmp_path / "data"),
host="127.0.0.1",
port=8082,
log_file=str(tmp_path / "server.log"),
)
fake_platform = MagicMock()
fake_platform.name = "fake"
fake_platform.on_message = MagicMock()
fake_platform.start = AsyncMock()
fake_platform.stop = AsyncMock()
session_store = MagicMock()
session_store.get_all_trees.return_value = []
session_store.get_node_mapping.return_value = {}
session_store.sync_from_tree_data = MagicMock()
session_store.flush_pending_save = MagicMock(side_effect=OSError("disk full"))
cli_manager = MagicMock()
cli_manager.stop_all = AsyncMock()
api_app_mod = importlib.import_module("api.app")
cleanup_provider = AsyncMock()
with (
patch.object(api_app_mod, "get_settings", return_value=settings),
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
patch(
"messaging.platforms.factory.create_messaging_platform",
return_value=fake_platform,
),
patch("messaging.session.SessionStore", return_value=session_store),
patch("cli.manager.CLISessionManager", return_value=cli_manager),
TestClient(app),
):
pass
session_store.flush_pending_save.assert_called_once()
cleanup_provider.assert_awaited_once()