Remove pull_image + list_registries; mark Gruppen 6+7 as entfällt
DSM methods for SYNO.Docker.Image/pull and SYNO.Docker.Registry/get did not behave as expected in production testing against the NAS. Tools deregistered, modules deleted, tests removed, CLAUDE.md updated. Tool count: 19 → 17. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""Tests for modules/images.py."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -491,140 +491,3 @@ async def test_check_image_updates_for_project():
|
||||
result = await tools["check_image_updates"](project_name="myapp")
|
||||
assert "myapp" in result
|
||||
assert "nginx:1.24" in result
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# pull_image
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
PULLED_IMAGE = {
|
||||
"id": "sha256:dddd",
|
||||
"repository": "postgres",
|
||||
"tags": ["17.8"],
|
||||
"size": 80 * 1024 * 1024,
|
||||
"created": 1700000000,
|
||||
"upgradable": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pull_image_success():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "pull":
|
||||
return {}
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return {"images": [PULLED_IMAGE]}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
with patch("asyncio.sleep", new=AsyncMock()):
|
||||
result = await tools["pull_image"]("postgres:17.8")
|
||||
|
||||
assert "Pulled postgres:17.8" in result
|
||||
assert "MiB" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pull_image_no_tag_defaults_to_latest():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
found_image = {**PULLED_IMAGE, "repository": "nginx", "tags": ["latest"]}
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "pull":
|
||||
# Verify tag defaults to "latest"
|
||||
assert kwargs.get("params", {}).get("tag") == "latest"
|
||||
return {}
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return {"images": [found_image]}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
with patch("asyncio.sleep", new=AsyncMock()):
|
||||
result = await tools["pull_image"]("nginx")
|
||||
|
||||
assert "Pulled nginx:latest" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pull_image_registry_prefixed():
|
||||
"""Registry-prefixed images (e.g. ghcr.io/foo/bar:v1) split at last ':'."""
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
found_image = {
|
||||
**PULLED_IMAGE,
|
||||
"repository": "ghcr.io/open-webui/open-webui",
|
||||
"tags": ["v0.9.0"],
|
||||
}
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "pull":
|
||||
params = kwargs.get("params", {})
|
||||
assert params["repository"] == "ghcr.io/open-webui/open-webui"
|
||||
assert params["tag"] == "v0.9.0"
|
||||
return {}
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return {"images": [found_image]}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
with patch("asyncio.sleep", new=AsyncMock()):
|
||||
result = await tools["pull_image"]("ghcr.io/open-webui/open-webui:v0.9.0")
|
||||
|
||||
assert "Pulled ghcr.io/open-webui/open-webui:v0.9.0" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pull_image_timeout():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "pull":
|
||||
return {}
|
||||
# image never appears in list
|
||||
return {"images": []}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
with patch("asyncio.sleep", new=AsyncMock()):
|
||||
result = await tools["pull_image"]("nonexistent:latest")
|
||||
|
||||
assert "did not complete" in result
|
||||
assert "120" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pull_image_api_error():
|
||||
from mcp_synology_container.dsm_client import SynologyError
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.side_effect = SynologyError("pull failed", code=400)
|
||||
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
with patch("asyncio.sleep", new=AsyncMock()):
|
||||
result = await tools["pull_image"]("bad/image:tag")
|
||||
|
||||
assert "Error" in result
|
||||
assert "bad/image:tag" in result
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
"""Tests for modules/registries.py."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def make_mock_mcp():
|
||||
tools: dict = {}
|
||||
|
||||
class MockMCP:
|
||||
def tool(self):
|
||||
def decorator(fn):
|
||||
tools[fn.__name__] = fn
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
return MockMCP(), tools
|
||||
|
||||
|
||||
def make_config():
|
||||
from mcp_synology_container.config import AppConfig, ConnectionConfig
|
||||
|
||||
return AppConfig(
|
||||
schema_version=1,
|
||||
connection=ConnectionConfig(host="nas.local", port=443, https=True, verify_ssl=True),
|
||||
)
|
||||
|
||||
|
||||
SAMPLE_REGISTRIES = {
|
||||
"registries": [
|
||||
{
|
||||
"name": "Docker Hub",
|
||||
"url": "https://registry.hub.docker.com",
|
||||
"syno": True,
|
||||
"enable_registry_mirror": False,
|
||||
"enable_trust_SSC": True,
|
||||
"mirror_urls": [],
|
||||
},
|
||||
{
|
||||
"name": "GitHub Packages",
|
||||
"url": "https://ghcr.io",
|
||||
"syno": False,
|
||||
"enable_registry_mirror": False,
|
||||
"enable_trust_SSC": True,
|
||||
"mirror_urls": [],
|
||||
},
|
||||
],
|
||||
"using": "Docker Hub",
|
||||
"total": 2,
|
||||
"offset": 0,
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# list_registries
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_registries_shows_all():
|
||||
from mcp_synology_container.modules.registries import register_registries
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.return_value = SAMPLE_REGISTRIES
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_registries(mcp, make_config(), client)
|
||||
|
||||
result = await tools["list_registries"]()
|
||||
|
||||
assert "Docker Hub" in result
|
||||
assert "GitHub Packages" in result
|
||||
assert "registry.hub.docker.com" in result
|
||||
assert "ghcr.io" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_registries_marks_active():
|
||||
from mcp_synology_container.modules.registries import register_registries
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.return_value = SAMPLE_REGISTRIES
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_registries(mcp, make_config(), client)
|
||||
|
||||
result = await tools["list_registries"]()
|
||||
|
||||
# Docker Hub is the active registry
|
||||
assert "[active]" in result
|
||||
# GitHub Packages is not active — "[active]" appears only once
|
||||
assert result.count("[active]") == 1
|
||||
|
||||
pos_hub = result.index("Docker Hub")
|
||||
pos_active = result.index("[active]")
|
||||
pos_github = result.index("GitHub Packages")
|
||||
# [active] marker appears right after "Docker Hub", before "GitHub Packages"
|
||||
assert pos_hub < pos_active < pos_github
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_registries_uses_get_method():
|
||||
"""list_registries must call SYNO.Docker.Registry with method='get'."""
|
||||
from mcp_synology_container.modules.registries import register_registries
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.return_value = SAMPLE_REGISTRIES
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_registries(mcp, make_config(), client)
|
||||
|
||||
await tools["list_registries"]()
|
||||
|
||||
client.request.assert_called_once()
|
||||
call_args = client.request.call_args
|
||||
assert call_args.args[0] == "SYNO.Docker.Registry"
|
||||
assert call_args.args[1] == "get"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_registries_mirror_flag():
|
||||
from mcp_synology_container.modules.registries import register_registries
|
||||
|
||||
data = {
|
||||
"registries": [
|
||||
{
|
||||
"name": "Mirror Registry",
|
||||
"url": "https://mirror.example.com",
|
||||
"syno": False,
|
||||
"enable_registry_mirror": True,
|
||||
"mirror_urls": ["https://mirror.example.com"],
|
||||
}
|
||||
],
|
||||
"using": "",
|
||||
}
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.return_value = data
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_registries(mcp, make_config(), client)
|
||||
|
||||
result = await tools["list_registries"]()
|
||||
|
||||
assert "[mirror enabled]" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_registries_empty():
|
||||
from mcp_synology_container.modules.registries import register_registries
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.return_value = {"registries": [], "using": ""}
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_registries(mcp, make_config(), client)
|
||||
|
||||
result = await tools["list_registries"]()
|
||||
|
||||
assert "No registries" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_registries_api_error():
|
||||
from mcp_synology_container.dsm_client import SynologyError
|
||||
from mcp_synology_container.modules.registries import register_registries
|
||||
|
||||
client = AsyncMock()
|
||||
client.request.side_effect = SynologyError("Permission denied", code=105)
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_registries(mcp, make_config(), client)
|
||||
|
||||
result = await tools["list_registries"]()
|
||||
|
||||
assert "Error" in result
|
||||
assert "Permission denied" in result
|
||||
Reference in New Issue
Block a user