mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 04:29:17 -07:00
Resolves review threads: - PRRT_kwDOO6Hdxs50fewG (setup.py): non-eliciting clients now return True from elicit_reset_confirmation so they can reconfigure without being blocked - PRRT_kwDOO6Hdxs50fewM (test-tools.sh): add notification/recalculate smoke test - PRRT_kwDOO6Hdxs50fewP (test-tools.sh): add system/array smoke test - PRRT_kwDOO6Hdxs50fewT (resources.py): surface manager error state instead of reporting 'connecting' for permanently failed subscriptions - PRRT_kwDOO6Hdxs50feAj (resources.py): use is not None check for empty cached dicts - PRRT_kwDOO6Hdxs50fewY (integration tests): remove duplicate snapshot-registration tests already covered in test_resources.py - PRRT_kwDOO6Hdxs50fewe (test_resources.py): replace brittle import-detail test with behavior tests for connecting/error states - PRRT_kwDOO6Hdxs50fewh (test_customization.py): strengthen public_theme assertion - PRRT_kwDOO6Hdxs50fewk (test_customization.py): strengthen theme assertion - PRRT_kwDOO6Hdxs50fewo (__init__.py): correct subaction count ~88 -> ~107 - PRRT_kwDOO6Hdxs50fewx (test_oidc.py): assert providers list value directly - PRRT_kwDOO6Hdxs50fewz (unraid.py): remove unreachable raise after vm handler - PRRT_kwDOO6Hdxs50few2 (unraid.py): remove unreachable raise after docker handler - PRRT_kwDOO6Hdxs50fev8 (CLAUDE.md): replace legacy 15-tool table with unified unraid action/subaction table - PRRT_kwDOO6Hdxs50fev_ (test_oidc.py): assert providers + defaultAllowedOrigins - PRRT_kwDOO6Hdxs50feAz (CLAUDE.md): update tool categories to unified API shape - PRRT_kwDOO6Hdxs50feBE (CLAUDE.md/setup.py): update unraid_health refs to unraid(action=health, subaction=setup)
528 lines
18 KiB
Python
528 lines
18 KiB
Python
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from unraid_mcp.core.exceptions import CredentialsNotConfiguredError, ToolError
|
|
|
|
|
|
def test_credentials_not_configured_error_exists():
|
|
err = CredentialsNotConfiguredError()
|
|
assert str(err) == "Unraid credentials are not configured."
|
|
|
|
|
|
def test_credentials_not_configured_error_is_exception():
|
|
"""CredentialsNotConfiguredError must be catchable as a plain Exception."""
|
|
with pytest.raises(Exception):
|
|
raise CredentialsNotConfiguredError()
|
|
|
|
|
|
def test_credentials_not_configured_error_is_not_tool_error():
|
|
"""CredentialsNotConfiguredError must NOT be a ToolError — it bypasses MCP protocol error handling."""
|
|
assert not issubclass(CredentialsNotConfiguredError, ToolError)
|
|
|
|
|
|
def test_settings_is_configured_true():
|
|
from unraid_mcp.config import settings
|
|
|
|
with (
|
|
patch.object(settings, "UNRAID_API_URL", "https://example.com"),
|
|
patch.object(settings, "UNRAID_API_KEY", "key123"),
|
|
):
|
|
assert settings.is_configured() is True
|
|
|
|
|
|
def test_settings_is_configured_false_when_missing():
|
|
from unraid_mcp.config import settings
|
|
|
|
with (
|
|
patch.object(settings, "UNRAID_API_URL", None),
|
|
patch.object(settings, "UNRAID_API_KEY", None),
|
|
):
|
|
assert settings.is_configured() is False
|
|
|
|
|
|
def test_settings_apply_runtime_config_updates_module_globals():
|
|
import os
|
|
|
|
from unraid_mcp.config import settings
|
|
|
|
original_url = settings.UNRAID_API_URL
|
|
original_key = settings.UNRAID_API_KEY
|
|
original_env_url = os.environ.get("UNRAID_API_URL")
|
|
original_env_key = os.environ.get("UNRAID_API_KEY")
|
|
try:
|
|
settings.apply_runtime_config("https://newurl.com/graphql", "newkey")
|
|
assert settings.UNRAID_API_URL == "https://newurl.com/graphql"
|
|
assert settings.UNRAID_API_KEY == "newkey"
|
|
assert os.environ["UNRAID_API_URL"] == "https://newurl.com/graphql"
|
|
assert os.environ["UNRAID_API_KEY"] == "newkey"
|
|
finally:
|
|
# Reset module globals
|
|
settings.UNRAID_API_URL = original_url
|
|
settings.UNRAID_API_KEY = original_key
|
|
# Reset os.environ
|
|
if original_env_url is None:
|
|
os.environ.pop("UNRAID_API_URL", None)
|
|
else:
|
|
os.environ["UNRAID_API_URL"] = original_env_url
|
|
if original_env_key is None:
|
|
os.environ.pop("UNRAID_API_KEY", None)
|
|
else:
|
|
os.environ["UNRAID_API_KEY"] = original_env_key
|
|
|
|
|
|
def test_run_server_does_not_exit_when_creds_missing(monkeypatch):
|
|
"""Server should not sys.exit(1) when credentials are absent."""
|
|
import unraid_mcp.config.settings as settings_mod
|
|
|
|
monkeypatch.setattr(settings_mod, "UNRAID_API_URL", None)
|
|
monkeypatch.setattr(settings_mod, "UNRAID_API_KEY", None)
|
|
|
|
from unraid_mcp import server as server_mod
|
|
|
|
with (
|
|
patch.object(server_mod, "mcp") as mock_mcp,
|
|
patch("unraid_mcp.server.logger") as mock_logger,
|
|
):
|
|
mock_mcp.run.side_effect = SystemExit(0)
|
|
try:
|
|
server_mod.run_server()
|
|
except SystemExit as e:
|
|
assert e.code == 0, f"Unexpected sys.exit({e.code}) — server crashed on missing creds"
|
|
mock_logger.warning.assert_called()
|
|
warning_msgs = [call[0][0] for call in mock_logger.warning.call_args_list]
|
|
assert any("elicitation" in msg for msg in warning_msgs), (
|
|
f"Expected a warning containing 'elicitation', got: {warning_msgs}"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_and_configure_writes_env_file(tmp_path):
|
|
"""elicit_and_configure writes a .env file and calls apply_runtime_config."""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from unraid_mcp.core.setup import elicit_and_configure
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "accept"
|
|
mock_result.data = MagicMock()
|
|
mock_result.data.api_url = "https://myunraid.example.com/graphql"
|
|
mock_result.data.api_key = "abc123secret"
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
creds_dir = tmp_path / "creds"
|
|
creds_file = creds_dir / ".env"
|
|
|
|
with (
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_DIR", creds_dir),
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_ENV_PATH", creds_file),
|
|
patch("unraid_mcp.core.setup.PROJECT_ROOT", tmp_path),
|
|
patch("unraid_mcp.core.setup.apply_runtime_config") as mock_apply,
|
|
):
|
|
result = await elicit_and_configure(mock_ctx)
|
|
|
|
assert result is True
|
|
assert creds_file.exists()
|
|
content = creds_file.read_text()
|
|
assert "UNRAID_API_URL=https://myunraid.example.com/graphql" in content
|
|
assert "UNRAID_API_KEY=abc123secret" in content
|
|
mock_apply.assert_called_once_with("https://myunraid.example.com/graphql", "abc123secret")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_and_configure_returns_false_on_decline():
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_and_configure
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "decline"
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
result = await elicit_and_configure(mock_ctx)
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_and_configure_returns_false_on_cancel():
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_and_configure
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "cancel"
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
result = await elicit_and_configure(mock_ctx)
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_make_graphql_request_raises_sentinel_when_unconfigured():
|
|
"""make_graphql_request raises CredentialsNotConfiguredError (not ToolError) when
|
|
credentials are absent, so callers can trigger elicitation."""
|
|
from unraid_mcp.config import settings as settings_mod
|
|
from unraid_mcp.core.client import make_graphql_request
|
|
from unraid_mcp.core.exceptions import CredentialsNotConfiguredError
|
|
|
|
original_url = settings_mod.UNRAID_API_URL
|
|
original_key = settings_mod.UNRAID_API_KEY
|
|
try:
|
|
settings_mod.UNRAID_API_URL = None
|
|
settings_mod.UNRAID_API_KEY = None
|
|
with pytest.raises(CredentialsNotConfiguredError):
|
|
await make_graphql_request("{ __typename }")
|
|
finally:
|
|
settings_mod.UNRAID_API_URL = original_url
|
|
settings_mod.UNRAID_API_KEY = original_key
|
|
|
|
|
|
import os # noqa: E402 — needed for reload-based tests below
|
|
|
|
|
|
def test_credentials_dir_defaults_to_home_unraid_mcp():
|
|
"""CREDENTIALS_DIR defaults to ~/.unraid-mcp when env var is not set."""
|
|
import importlib
|
|
|
|
import unraid_mcp.config.settings as s
|
|
|
|
os.environ.pop("UNRAID_CREDENTIALS_DIR", None)
|
|
try:
|
|
with patch.dict(os.environ, {}, clear=False):
|
|
os.environ.pop("UNRAID_CREDENTIALS_DIR", None)
|
|
importlib.reload(s)
|
|
assert Path.home() / ".unraid-mcp" == s.CREDENTIALS_DIR
|
|
finally:
|
|
importlib.reload(s) # Restore module state
|
|
|
|
|
|
def test_credentials_dir_env_var_override():
|
|
"""UNRAID_CREDENTIALS_DIR env var overrides the default."""
|
|
import importlib
|
|
|
|
import unraid_mcp.config.settings as s
|
|
|
|
custom = "/tmp/custom-creds"
|
|
try:
|
|
with patch.dict(os.environ, {"UNRAID_CREDENTIALS_DIR": custom}):
|
|
importlib.reload(s)
|
|
assert Path(custom) == s.CREDENTIALS_DIR
|
|
finally:
|
|
# Reload without the custom env var to restore original state
|
|
os.environ.pop("UNRAID_CREDENTIALS_DIR", None)
|
|
importlib.reload(s)
|
|
|
|
|
|
def test_credentials_env_path_is_dot_env_inside_credentials_dir():
|
|
import unraid_mcp.config.settings as s
|
|
|
|
assert s.CREDENTIALS_ENV_PATH == s.CREDENTIALS_DIR / ".env"
|
|
|
|
|
|
import stat # noqa: E402
|
|
|
|
|
|
def test_write_env_creates_credentials_dir_with_700_permissions(tmp_path):
|
|
"""_write_env creates CREDENTIALS_DIR with mode 700 (owner-only)."""
|
|
from unraid_mcp.core.setup import _write_env
|
|
|
|
creds_dir = tmp_path / "creds"
|
|
creds_file = creds_dir / ".env"
|
|
|
|
with (
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_DIR", creds_dir),
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_ENV_PATH", creds_file),
|
|
):
|
|
_write_env("https://example.com", "mykey")
|
|
|
|
assert creds_dir.exists()
|
|
dir_mode = stat.S_IMODE(creds_dir.stat().st_mode)
|
|
assert dir_mode == 0o700, f"Expected 0o700, got {oct(dir_mode)}"
|
|
|
|
|
|
def test_write_env_sets_file_permissions_600(tmp_path):
|
|
"""_write_env sets .env file permissions to 600 (owner read/write only)."""
|
|
from unraid_mcp.core.setup import _write_env
|
|
|
|
creds_dir = tmp_path / "creds"
|
|
creds_file = creds_dir / ".env"
|
|
|
|
with (
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_DIR", creds_dir),
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_ENV_PATH", creds_file),
|
|
):
|
|
_write_env("https://example.com", "mykey")
|
|
|
|
file_mode = stat.S_IMODE(creds_file.stat().st_mode)
|
|
assert file_mode == 0o600, f"Expected 0o600, got {oct(file_mode)}"
|
|
|
|
|
|
def test_write_env_seeds_from_env_example_on_first_run(tmp_path):
|
|
"""_write_env copies .env.example structure and replaces credentials in-place."""
|
|
from unraid_mcp.core.setup import _write_env
|
|
|
|
creds_dir = tmp_path / "creds"
|
|
creds_file = creds_dir / ".env"
|
|
# Create a fake .env.example
|
|
example = tmp_path / ".env.example"
|
|
example.write_text(
|
|
"# Example config\nFOO=bar\nUNRAID_API_URL=placeholder\nUNRAID_API_KEY=placeholder\n"
|
|
)
|
|
|
|
with (
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_DIR", creds_dir),
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_ENV_PATH", creds_file),
|
|
patch("unraid_mcp.core.setup.PROJECT_ROOT", tmp_path),
|
|
):
|
|
_write_env("https://real.url", "realkey")
|
|
|
|
content = creds_file.read_text()
|
|
assert "UNRAID_API_URL=https://real.url" in content
|
|
assert "UNRAID_API_KEY=realkey" in content
|
|
assert "# Example config" in content # comment preserved
|
|
assert "FOO=bar" in content # other vars preserved
|
|
assert "placeholder" not in content # old credential values replaced
|
|
# Credentials should be at their original position (after comments), not prepended before them
|
|
lines = content.splitlines()
|
|
url_idx = next(i for i, line in enumerate(lines) if line.startswith("UNRAID_API_URL="))
|
|
comment_idx = next(i for i, line in enumerate(lines) if line.startswith("# Example config"))
|
|
assert comment_idx < url_idx # Comment comes before credentials
|
|
|
|
|
|
def test_write_env_first_run_no_example_file(tmp_path):
|
|
"""_write_env works on first run when .env.example does not exist."""
|
|
from unraid_mcp.core.setup import _write_env
|
|
|
|
creds_dir = tmp_path / "creds"
|
|
creds_file = creds_dir / ".env"
|
|
# tmp_path has no .env.example
|
|
|
|
with (
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_DIR", creds_dir),
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_ENV_PATH", creds_file),
|
|
patch("unraid_mcp.core.setup.PROJECT_ROOT", tmp_path),
|
|
):
|
|
_write_env("https://myserver.com", "mykey123")
|
|
|
|
assert creds_file.exists()
|
|
content = creds_file.read_text()
|
|
assert "UNRAID_API_URL=https://myserver.com" in content
|
|
assert "UNRAID_API_KEY=mykey123" in content
|
|
|
|
|
|
def test_write_env_updates_existing_credentials_in_place(tmp_path):
|
|
"""_write_env updates credentials without destroying other vars."""
|
|
from unraid_mcp.core.setup import _write_env
|
|
|
|
creds_dir = tmp_path / "creds"
|
|
creds_dir.mkdir(mode=0o700)
|
|
creds_file = creds_dir / ".env"
|
|
creds_file.write_text(
|
|
"UNRAID_API_URL=https://old.url\nUNRAID_API_KEY=oldkey\nUNRAID_VERIFY_SSL=false\n"
|
|
)
|
|
|
|
with (
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_DIR", creds_dir),
|
|
patch("unraid_mcp.core.setup.CREDENTIALS_ENV_PATH", creds_file),
|
|
patch("unraid_mcp.core.setup.PROJECT_ROOT", tmp_path),
|
|
):
|
|
_write_env("https://new.url", "newkey")
|
|
|
|
content = creds_file.read_text()
|
|
assert "UNRAID_API_URL=https://new.url" in content
|
|
assert "UNRAID_API_KEY=newkey" in content
|
|
assert "UNRAID_VERIFY_SSL=false" in content # preserved
|
|
assert "old" not in content
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_and_configure_returns_false_when_client_not_supported():
|
|
"""elicit_and_configure returns False when client raises NotImplementedError."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_and_configure
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_ctx.elicit = AsyncMock(side_effect=NotImplementedError("elicitation not supported"))
|
|
|
|
result = await elicit_and_configure(mock_ctx)
|
|
assert result is False
|
|
|
|
|
|
def test_tool_error_handler_converts_credentials_not_configured_to_tool_error():
|
|
"""tool_error_handler wraps CredentialsNotConfiguredError in a ToolError."""
|
|
import logging
|
|
|
|
from unraid_mcp.core.exceptions import (
|
|
CredentialsNotConfiguredError,
|
|
ToolError,
|
|
tool_error_handler,
|
|
)
|
|
|
|
_log = logging.getLogger("test")
|
|
with pytest.raises(ToolError), tool_error_handler("docker", "list", _log):
|
|
raise CredentialsNotConfiguredError()
|
|
|
|
|
|
def test_tool_error_handler_credentials_error_message_includes_path():
|
|
"""ToolError from CredentialsNotConfiguredError includes the credentials path."""
|
|
import logging
|
|
|
|
from unraid_mcp.config.settings import CREDENTIALS_ENV_PATH
|
|
from unraid_mcp.core.exceptions import (
|
|
CredentialsNotConfiguredError,
|
|
ToolError,
|
|
tool_error_handler,
|
|
)
|
|
|
|
_log = logging.getLogger("test")
|
|
with pytest.raises(ToolError) as exc_info, tool_error_handler("docker", "list", _log):
|
|
raise CredentialsNotConfiguredError()
|
|
|
|
assert str(CREDENTIALS_ENV_PATH) in str(exc_info.value)
|
|
assert "setup" in str(exc_info.value).lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# elicit_reset_confirmation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_returns_false_when_ctx_none():
|
|
"""Returns False immediately when no MCP context is available."""
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
result = await elicit_reset_confirmation(None, "https://example.com")
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_returns_true_when_user_confirms():
|
|
"""Returns True when the user accepts and answers True."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "accept"
|
|
mock_result.data = True
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
result = await elicit_reset_confirmation(mock_ctx, "https://example.com")
|
|
assert result is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_returns_false_when_user_answers_false():
|
|
"""Returns False when the user accepts but answers False (does not want to reset)."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "accept"
|
|
mock_result.data = False
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
result = await elicit_reset_confirmation(mock_ctx, "https://example.com")
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_returns_false_when_declined():
|
|
"""Returns False when the user declines via action (dismisses the prompt)."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "decline"
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
result = await elicit_reset_confirmation(mock_ctx, "https://example.com")
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_returns_false_when_cancelled():
|
|
"""Returns False when the user cancels the prompt."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "cancel"
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
result = await elicit_reset_confirmation(mock_ctx, "https://example.com")
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_returns_true_when_not_implemented():
|
|
"""Returns True (proceed with reset) when the MCP client does not support elicitation.
|
|
|
|
Non-interactive clients (stdio, CI) must not be permanently blocked from
|
|
reconfiguring credentials just because they can't ask the user a yes/no question.
|
|
"""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_ctx.elicit = AsyncMock(side_effect=NotImplementedError("elicitation not supported"))
|
|
|
|
result = await elicit_reset_confirmation(mock_ctx, "https://example.com")
|
|
assert result is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_elicit_reset_confirmation_includes_current_url_in_prompt():
|
|
"""The elicitation message includes the current URL so the user knows what they're replacing."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from unraid_mcp.core.setup import elicit_reset_confirmation
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.action = "decline"
|
|
mock_ctx.elicit = AsyncMock(return_value=mock_result)
|
|
|
|
await elicit_reset_confirmation(mock_ctx, "https://my-unraid.example.com:31337")
|
|
|
|
call_kwargs = mock_ctx.elicit.call_args
|
|
message = call_kwargs.kwargs.get("message") or call_kwargs.args[0]
|
|
assert "https://my-unraid.example.com:31337" in message
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_credentials_not_configured_surfaces_as_tool_error_with_path():
|
|
"""CredentialsNotConfiguredError from a tool becomes ToolError with the credentials path."""
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from tests.conftest import make_tool_fn
|
|
from unraid_mcp.config.settings import CREDENTIALS_ENV_PATH
|
|
from unraid_mcp.core.exceptions import CredentialsNotConfiguredError, ToolError
|
|
|
|
tool_fn = make_tool_fn("unraid_mcp.tools.unraid", "register_unraid_tool", "unraid")
|
|
|
|
with (
|
|
patch(
|
|
"unraid_mcp.tools.unraid.make_graphql_request",
|
|
new=AsyncMock(side_effect=CredentialsNotConfiguredError()),
|
|
),
|
|
pytest.raises(ToolError) as exc_info,
|
|
):
|
|
await tool_fn(action="user", subaction="me")
|
|
|
|
assert str(CREDENTIALS_ENV_PATH) in str(exc_info.value)
|