forked from HomeLab/unraid-mcp
feat: harden API safety and expand command docs with full test coverage
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
"""Tests for unraid_users tool."""
|
||||
"""Tests for unraid_users tool.
|
||||
|
||||
NOTE: Unraid GraphQL API only supports the me() query.
|
||||
User management operations (list, add, delete, cloud, remote_access, origins) are NOT available in the API.
|
||||
"""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
@@ -20,112 +24,54 @@ def _make_tool():
|
||||
|
||||
|
||||
class TestUsersValidation:
|
||||
async def test_delete_requires_confirm(self, _mock_graphql: AsyncMock) -> None:
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="destructive"):
|
||||
await tool_fn(action="delete", user_id="u:1")
|
||||
"""Test validation for invalid actions."""
|
||||
|
||||
async def test_get_requires_user_id(self, _mock_graphql: AsyncMock) -> None:
|
||||
async def test_invalid_action_rejected(self, _mock_graphql: AsyncMock) -> None:
|
||||
"""Test that non-existent actions are rejected with clear error."""
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="user_id"):
|
||||
await tool_fn(action="get")
|
||||
with pytest.raises(ToolError, match="Invalid action"):
|
||||
await tool_fn(action="list")
|
||||
|
||||
async def test_add_requires_name_and_password(self, _mock_graphql: AsyncMock) -> None:
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="requires name and password"):
|
||||
with pytest.raises(ToolError, match="Invalid action"):
|
||||
await tool_fn(action="add")
|
||||
|
||||
async def test_delete_requires_user_id(self, _mock_graphql: AsyncMock) -> None:
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="user_id"):
|
||||
await tool_fn(action="delete", confirm=True)
|
||||
with pytest.raises(ToolError, match="Invalid action"):
|
||||
await tool_fn(action="delete")
|
||||
|
||||
with pytest.raises(ToolError, match="Invalid action"):
|
||||
await tool_fn(action="cloud")
|
||||
|
||||
|
||||
class TestUsersActions:
|
||||
"""Test the single supported action: me."""
|
||||
|
||||
async def test_me(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"me": {"id": "u:1", "name": "root", "description": "", "roles": ["ADMIN"]}}
|
||||
"""Test querying current authenticated user."""
|
||||
_mock_graphql.return_value = {
|
||||
"me": {"id": "u:1", "name": "root", "description": "", "roles": ["ADMIN"]}
|
||||
}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="me")
|
||||
assert result["name"] == "root"
|
||||
assert result["roles"] == ["ADMIN"]
|
||||
_mock_graphql.assert_called_once()
|
||||
|
||||
async def test_list(self, _mock_graphql: AsyncMock) -> None:
|
||||
async def test_me_default_action(self, _mock_graphql: AsyncMock) -> None:
|
||||
"""Test that 'me' is the default action."""
|
||||
_mock_graphql.return_value = {
|
||||
"users": [{"id": "u:1", "name": "root"}, {"id": "u:2", "name": "guest"}]
|
||||
"me": {"id": "u:1", "name": "root", "description": "", "roles": ["ADMIN"]}
|
||||
}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="list")
|
||||
assert len(result["users"]) == 2
|
||||
|
||||
async def test_get(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"user": {"id": "u:1", "name": "root", "description": "", "roles": ["ADMIN"]}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="get", user_id="u:1")
|
||||
result = await tool_fn()
|
||||
assert result["name"] == "root"
|
||||
|
||||
async def test_add(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"addUser": {"id": "u:3", "name": "newuser", "description": "", "roles": ["USER"]}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="add", name="newuser", password="pass123")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_add_with_role(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"addUser": {"id": "u:3", "name": "admin2", "description": "", "roles": ["ADMIN"]}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="add", name="admin2", password="pass123", role="admin")
|
||||
assert result["success"] is True
|
||||
call_args = _mock_graphql.call_args
|
||||
assert call_args[0][1]["input"]["role"] == "ADMIN"
|
||||
|
||||
async def test_delete(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"deleteUser": {"id": "u:2", "name": "guest"}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="delete", user_id="u:2", confirm=True)
|
||||
assert result["success"] is True
|
||||
call_args = _mock_graphql.call_args
|
||||
assert call_args[0][1]["input"]["id"] == "u:2"
|
||||
|
||||
async def test_cloud(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"cloud": {"status": "connected", "apiKey": "***"}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="cloud")
|
||||
assert result["status"] == "connected"
|
||||
|
||||
async def test_remote_access(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"remoteAccess": {"enabled": True, "url": "https://example.com"}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="remote_access")
|
||||
assert result["enabled"] is True
|
||||
|
||||
async def test_origins(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"allowedOrigins": ["http://localhost", "https://example.com"]}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="origins")
|
||||
assert len(result["origins"]) == 2
|
||||
|
||||
|
||||
class TestUsersNoneHandling:
|
||||
"""Verify actions return empty dict (not TypeError) when API returns None."""
|
||||
|
||||
async def test_me_returns_none(self, _mock_graphql: AsyncMock) -> None:
|
||||
"""Test that me returns empty dict when API returns None."""
|
||||
_mock_graphql.return_value = {"me": None}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="me")
|
||||
assert result == {}
|
||||
|
||||
async def test_get_returns_none(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"user": None}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="get", user_id="u:1")
|
||||
assert result == {}
|
||||
|
||||
async def test_cloud_returns_none(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"cloud": None}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="cloud")
|
||||
assert result == {}
|
||||
|
||||
async def test_remote_access_returns_none(self, _mock_graphql: AsyncMock) -> None:
|
||||
_mock_graphql.return_value = {"remoteAccess": None}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="remote_access")
|
||||
assert result == {}
|
||||
|
||||
Reference in New Issue
Block a user