feat(elicitation): raise CredentialsNotConfiguredError in client when creds absent

make_graphql_request now reads credentials from the settings module at call
time (via a local import) instead of relying on module-level names captured at
import time. When either credential is missing it raises CredentialsNotConfiguredError
(not ToolError), allowing callers to trigger elicitation rather than surfacing a
generic error to the MCP client.

Updated tests/test_client.py and tests/http_layer/test_request_construction.py
to patch unraid_mcp.config.settings.* instead of the now-removed client-module
attrs, and to expect CredentialsNotConfiguredError on missing credentials.
This commit is contained in:
Jacob Magar
2026-03-14 03:55:57 -04:00
parent 02e61b4290
commit 8a986a84c2
4 changed files with 50 additions and 30 deletions

View File

@@ -17,7 +17,7 @@ import respx
from tests.conftest import make_tool_fn
from unraid_mcp.core.client import DEFAULT_TIMEOUT, DISK_TIMEOUT, make_graphql_request
from unraid_mcp.core.exceptions import ToolError
from unraid_mcp.core.exceptions import CredentialsNotConfiguredError, ToolError
# ---------------------------------------------------------------------------
@@ -32,8 +32,8 @@ API_KEY = "test-api-key-12345"
def _patch_config():
"""Patch API URL and key for all tests in this module."""
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", API_URL),
patch("unraid_mcp.core.client.UNRAID_API_KEY", API_KEY),
patch("unraid_mcp.config.settings.UNRAID_API_URL", API_URL),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", API_KEY),
):
yield
@@ -1288,8 +1288,8 @@ class TestCrossCuttingConcerns:
async def test_missing_api_url_raises_before_http_call(self) -> None:
route = respx.post(API_URL).mock(return_value=_graphql_response({}))
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", ""),
pytest.raises(ToolError, match="UNRAID_API_URL not configured"),
patch("unraid_mcp.config.settings.UNRAID_API_URL", ""),
pytest.raises(CredentialsNotConfiguredError),
):
await make_graphql_request("query { online }")
assert not route.called
@@ -1298,8 +1298,8 @@ class TestCrossCuttingConcerns:
async def test_missing_api_key_raises_before_http_call(self) -> None:
route = respx.post(API_URL).mock(return_value=_graphql_response({}))
with (
patch("unraid_mcp.core.client.UNRAID_API_KEY", ""),
pytest.raises(ToolError, match="UNRAID_API_KEY not configured"),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", ""),
pytest.raises(CredentialsNotConfiguredError),
):
await make_graphql_request("query { online }")
assert not route.called

View File

@@ -16,7 +16,7 @@ from unraid_mcp.core.client import (
make_graphql_request,
redact_sensitive,
)
from unraid_mcp.core.exceptions import ToolError
from unraid_mcp.core.exceptions import CredentialsNotConfiguredError, ToolError
# ---------------------------------------------------------------------------
@@ -173,8 +173,8 @@ class TestMakeGraphQLRequestSuccess:
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
patch("unraid_mcp.config.settings.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", "test-key"),
):
yield
@@ -258,22 +258,22 @@ class TestMakeGraphQLRequestErrors:
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
patch("unraid_mcp.config.settings.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", "test-key"),
):
yield
async def test_missing_api_url(self) -> None:
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", ""),
pytest.raises(ToolError, match="UNRAID_API_URL not configured"),
patch("unraid_mcp.config.settings.UNRAID_API_URL", ""),
pytest.raises(CredentialsNotConfiguredError),
):
await make_graphql_request("{ info }")
async def test_missing_api_key(self) -> None:
with (
patch("unraid_mcp.core.client.UNRAID_API_KEY", ""),
pytest.raises(ToolError, match="UNRAID_API_KEY not configured"),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", ""),
pytest.raises(CredentialsNotConfiguredError),
):
await make_graphql_request("{ info }")
@@ -377,8 +377,8 @@ class TestGraphQLErrorHandling:
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
patch("unraid_mcp.config.settings.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", "test-key"),
):
yield
@@ -641,8 +641,8 @@ class TestRateLimitRetry:
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
patch("unraid_mcp.config.settings.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.config.settings.UNRAID_API_KEY", "test-key"),
patch("unraid_mcp.core.client.asyncio.sleep", new_callable=AsyncMock),
):
yield

View File

@@ -154,3 +154,23 @@ async def test_elicit_and_configure_returns_false_on_cancel():
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