Fix delete_image: POST with images JSON array (DevTools-confirmed format)
DSM Container Manager rejects name+tag and sha256 id params (error 114).
Browser DevTools capture shows the correct call is:
POST SYNO.Docker.Image / delete / version=1
images=[{"repository":"nouchka/sqlite3","tags":["latest"]}]
Changes:
- Add DsmClient.post_request() for form-encoded POST requests
- delete_image now calls post_request with version=1 and the images
JSON array built from the resolved repository name and tag
- Remove unused docker error-code dict from _error_message()
- Tests mock post_request and assert the images param content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -203,17 +203,18 @@ async def test_delete_image_preview():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_image_confirmed():
|
||||
import json
|
||||
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
client.post_request = AsyncMock(return_value={})
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return SAMPLE_IMAGES
|
||||
if api == "SYNO.Docker.Container":
|
||||
return {"containers": []}
|
||||
if api == "SYNO.Docker.Image" and method == "delete":
|
||||
return {}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
@@ -225,12 +226,11 @@ async def test_delete_image_confirmed():
|
||||
assert "redis:7" in result
|
||||
assert "freed" in result
|
||||
|
||||
# Delete must use the sha256 id, not name+tag
|
||||
delete_call = next(c for c in client.request.call_args_list if c.args[1] == "delete")
|
||||
params = delete_call.kwargs.get("params") or {}
|
||||
assert params.get("id") == "sha256:cccc"
|
||||
assert "name" not in params
|
||||
assert "tag" not in params
|
||||
# post_request must be called with images JSON param, not name/tag/id
|
||||
client.post_request.assert_called_once()
|
||||
params = client.post_request.call_args.kwargs.get("params", {})
|
||||
images = json.loads(params["images"])
|
||||
assert images == [{"repository": "redis", "tags": ["7"]}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -238,6 +238,7 @@ async def test_delete_image_not_found():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
client.post_request = AsyncMock(return_value={})
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
@@ -250,6 +251,7 @@ async def test_delete_image_not_found():
|
||||
|
||||
result = await tools["delete_image"](image_id="nonexistent:latest", confirmed=True)
|
||||
assert "not found" in result
|
||||
client.post_request.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -257,6 +259,7 @@ async def test_delete_image_in_use_blocked():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
client.post_request = AsyncMock(return_value={})
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
@@ -272,21 +275,23 @@ async def test_delete_image_in_use_blocked():
|
||||
result = await tools["delete_image"](image_id="nginx:1.24", confirmed=True)
|
||||
assert "Cannot delete" in result
|
||||
assert "my-nginx" in result
|
||||
client.post_request.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_image_by_hash():
|
||||
import json
|
||||
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
client.post_request = AsyncMock(return_value={})
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return SAMPLE_IMAGES
|
||||
if api == "SYNO.Docker.Container":
|
||||
return {"containers": []}
|
||||
if api == "SYNO.Docker.Image" and method == "delete":
|
||||
return {}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
@@ -297,10 +302,18 @@ async def test_delete_image_by_hash():
|
||||
assert "Deleted" in result
|
||||
assert "redis" in result
|
||||
|
||||
# Verify images param uses the resolved name+tag (not the hash)
|
||||
call_kwargs = client.post_request.call_args
|
||||
params = call_kwargs.kwargs.get("params") or {}
|
||||
images = json.loads(params.get("images", "[]"))
|
||||
assert images == [{"repository": "redis", "tags": ["7"]}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_image_registry_prefixed_name():
|
||||
"""Registry-prefixed image names (e.g. ghcr.io/foo/bar:v1) must split at last ':'."""
|
||||
import json
|
||||
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
registry_images = {
|
||||
@@ -317,14 +330,13 @@ async def test_delete_image_registry_prefixed_name():
|
||||
}
|
||||
|
||||
client = AsyncMock()
|
||||
client.post_request = AsyncMock(return_value={})
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return registry_images
|
||||
if api == "SYNO.Docker.Container":
|
||||
return {"containers": []}
|
||||
if api == "SYNO.Docker.Image" and method == "delete":
|
||||
return {}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
@@ -337,12 +349,11 @@ async def test_delete_image_registry_prefixed_name():
|
||||
assert "Deleted" in result
|
||||
assert "open-webui" in result
|
||||
|
||||
# Verify delete was called with the sha256 ID, not name+tag
|
||||
delete_call = next(c for c in client.request.call_args_list if c.args[1] == "delete")
|
||||
params = delete_call.kwargs.get("params") or {}
|
||||
assert params.get("id") == "sha256:dddd"
|
||||
assert "name" not in params
|
||||
assert "tag" not in params
|
||||
# images param must use full registry-prefixed repository name
|
||||
call_kwargs = client.post_request.call_args
|
||||
params = call_kwargs.kwargs.get("params") or {}
|
||||
images = json.loads(params.get("images", "[]"))
|
||||
assert images == [{"repository": "ghcr.io/open-webui/open-webui", "tags": ["v0.8.10"]}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -351,14 +362,13 @@ async def test_delete_image_api_error():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = AsyncMock()
|
||||
client.post_request = AsyncMock(side_effect=SynologyError("delete failed", code=114))
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return SAMPLE_IMAGES
|
||||
if api == "SYNO.Docker.Container":
|
||||
return {"containers": []}
|
||||
if api == "SYNO.Docker.Image" and method == "delete":
|
||||
raise SynologyError("delete failed", code=1)
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
@@ -367,6 +377,7 @@ async def test_delete_image_api_error():
|
||||
|
||||
result = await tools["delete_image"](image_id="redis:7", confirmed=True)
|
||||
assert "Error" in result
|
||||
assert "114" in result
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user