Skip to content

Commit af2a7fd

Browse files
committed
✨ MCP Service Containerization Integration #1368
[Specification Details] 1. Add test cases.
1 parent a94243b commit af2a7fd

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

test/backend/app/test_remote_mcp_app.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,5 +1478,122 @@ def test_get_container_logs_exception(self, mock_container_manager_class, mock_g
14781478
assert "Failed to get container logs" in data["detail"]
14791479

14801480

1481+
# ---------------------------------------------------------------------------
1482+
# Additional test cases for upload_mcp_image validation
1483+
# ---------------------------------------------------------------------------
1484+
1485+
1486+
class TestUploadMCPImageValidationAdditional:
1487+
"""Additional test cases for upload_mcp_image endpoint validation"""
1488+
1489+
@patch('apps.remote_mcp_app.get_current_user_id')
1490+
def test_upload_mcp_image_invalid_port_range(self, mock_get_user_id):
1491+
"""Test upload with invalid port range (covers lines 429-430)"""
1492+
mock_get_user_id.return_value = ("user123", "tenant456")
1493+
1494+
# Test port <= 0
1495+
file_content = b"fake tar content"
1496+
response = client.post(
1497+
"/mcp/upload-image",
1498+
data={"port": 0}, # Invalid port
1499+
files={"file": ("test.tar", file_content,
1500+
"application/octet-stream")},
1501+
headers={"Authorization": "Bearer test_token"}
1502+
)
1503+
assert response.status_code == HTTPStatus.BAD_REQUEST
1504+
data = response.json()
1505+
assert "Port must be between 1 and 65535" in data["detail"]
1506+
1507+
# Test port > 65535
1508+
response = client.post(
1509+
"/mcp/upload-image",
1510+
data={"port": 70000}, # Invalid port
1511+
files={"file": ("test.tar", file_content,
1512+
"application/octet-stream")},
1513+
headers={"Authorization": "Bearer test_token"}
1514+
)
1515+
assert response.status_code == HTTPStatus.BAD_REQUEST
1516+
data = response.json()
1517+
assert "Port must be between 1 and 65535" in data["detail"]
1518+
1519+
@patch('apps.remote_mcp_app.get_current_user_id')
1520+
@patch('apps.remote_mcp_app.MCPContainerManager')
1521+
@patch('apps.remote_mcp_app.check_mcp_name_exists', return_value=False)
1522+
def test_upload_mcp_image_env_vars_not_dict(self, mock_check_name, mock_container_manager_class, mock_get_user_id):
1523+
"""Test upload with environment variables that are not a JSON object (covers lines 459-460)"""
1524+
mock_get_user_id.return_value = ("user123", "tenant456")
1525+
1526+
mock_container_manager = MagicMock()
1527+
mock_container_manager_class.return_value = mock_container_manager
1528+
1529+
file_content = b"fake tar content"
1530+
1531+
# Test with array instead of object
1532+
response = client.post(
1533+
"/mcp/upload-image",
1534+
data={
1535+
"port": 5020,
1536+
"env_vars": '["VAR1", "VAR2"]' # Array instead of object
1537+
},
1538+
files={"file": ("test.tar", file_content,
1539+
"application/octet-stream")},
1540+
headers={"Authorization": "Bearer test_token"}
1541+
)
1542+
assert response.status_code == HTTPStatus.BAD_REQUEST
1543+
data = response.json()
1544+
assert "Invalid environment variables format" in data["detail"]
1545+
assert "Environment variables must be a JSON object" in data["detail"]
1546+
1547+
@patch('apps.remote_mcp_app.get_current_user_id')
1548+
@patch('apps.remote_mcp_app.MCPContainerManager')
1549+
@patch('apps.remote_mcp_app.add_remote_mcp_server_list')
1550+
@patch('apps.remote_mcp_app.check_mcp_name_exists', return_value=False)
1551+
@patch('tempfile.NamedTemporaryFile')
1552+
@patch('os.unlink', side_effect=OSError("Permission denied"))
1553+
@patch('apps.remote_mcp_app.logger')
1554+
def test_upload_mcp_image_temp_file_cleanup_warning(self, mock_logger, mock_unlink, mock_temp_file, mock_check_name, mock_add_server, mock_container_manager_class, mock_get_user_id):
1555+
"""Test upload with temporary file cleanup failure (covers lines 536-537)"""
1556+
mock_get_user_id.return_value = ("user123", "tenant456")
1557+
1558+
# Mock tempfile.NamedTemporaryFile
1559+
mock_temp_file_obj = MagicMock()
1560+
mock_temp_file_obj.__enter__.return_value = mock_temp_file_obj
1561+
mock_temp_file_obj.__exit__.return_value = None
1562+
mock_temp_file_obj.name = "/tmp/test.tar"
1563+
mock_temp_file.return_value = mock_temp_file_obj
1564+
1565+
mock_container_manager = MagicMock()
1566+
mock_container_manager_class.return_value = mock_container_manager
1567+
mock_container_manager.start_mcp_container_from_tar = AsyncMock(return_value={
1568+
"container_id": "container-123",
1569+
"mcp_url": "http://localhost:5020/mcp",
1570+
"host_port": "5020",
1571+
"status": "started",
1572+
"container_name": "test-image-user1234"
1573+
})
1574+
1575+
mock_add_server.return_value = None
1576+
1577+
file_content = b"fake tar content"
1578+
1579+
response = client.post(
1580+
"/mcp/upload-image",
1581+
data={"port": 5020},
1582+
files={"file": ("test.tar", file_content,
1583+
"application/octet-stream")},
1584+
headers={"Authorization": "Bearer test_token"}
1585+
)
1586+
1587+
# Should still succeed despite cleanup failure
1588+
assert response.status_code == HTTPStatus.OK
1589+
data = response.json()
1590+
assert data["status"] == "success"
1591+
1592+
# Verify warning was logged
1593+
mock_logger.warning.assert_called_once()
1594+
warning_call_args = mock_logger.warning.call_args[0][0]
1595+
assert "Failed to clean up temporary file /tmp/test.tar" in warning_call_args
1596+
1597+
14811598
if __name__ == "__main__":
14821599
pytest.main([__file__])

test/backend/services/test_mcp_container_service.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import sys
77
import os
8+
import tempfile
89
from unittest.mock import patch, MagicMock, AsyncMock
910
import pytest
1011

@@ -473,5 +474,208 @@ def test_get_container_logs_exception(self, mock_manager):
473474
assert "Error retrieving logs" in logs
474475

475476

477+
# ---------------------------------------------------------------------------
478+
# Test load_image_from_tar_file
479+
# ---------------------------------------------------------------------------
480+
481+
482+
class TestLoadImageFromTarFile:
483+
"""Test load_image_from_tar_file method"""
484+
485+
@pytest.fixture
486+
def mock_manager(self):
487+
"""Create MCPContainerManager instance with mocked client"""
488+
with patch('services.mcp_container_service.create_container_client_from_config'), \
489+
patch('services.mcp_container_service.DockerContainerConfig'):
490+
manager = MCPContainerManager()
491+
manager.client = MagicMock()
492+
return manager
493+
494+
@pytest.mark.asyncio
495+
async def test_load_image_from_tar_file_success_with_tags(self, mock_manager):
496+
"""Test successful loading of image with tags"""
497+
# Create a temporary file
498+
with tempfile.NamedTemporaryFile(delete=False, suffix='.tar') as temp_file:
499+
temp_file.write(b"fake tar content")
500+
temp_file_path = temp_file.name
501+
502+
try:
503+
mock_image = MagicMock()
504+
mock_image.tags = ["test-image:latest", "test-image:v1.0"]
505+
mock_image.id = "sha256:1234567890abcdef"
506+
507+
mock_manager.client.client.images.load.return_value = [mock_image]
508+
509+
result = await mock_manager.load_image_from_tar_file(temp_file_path)
510+
511+
assert result == "test-image:latest"
512+
mock_manager.client.client.images.load.assert_called_once()
513+
finally:
514+
# Clean up
515+
try:
516+
os.unlink(temp_file_path)
517+
except:
518+
pass
519+
520+
@pytest.mark.asyncio
521+
async def test_load_image_from_tar_file_success_without_tags(self, mock_manager):
522+
"""Test successful loading of image without tags"""
523+
# Create a temporary file
524+
with tempfile.NamedTemporaryFile(delete=False, suffix='.tar') as temp_file:
525+
temp_file.write(b"fake tar content")
526+
temp_file_path = temp_file.name
527+
528+
try:
529+
mock_image = MagicMock()
530+
mock_image.tags = []
531+
mock_image.id = "sha256:1234567890abcdef"
532+
533+
mock_manager.client.client.images.load.return_value = [mock_image]
534+
535+
result = await mock_manager.load_image_from_tar_file(temp_file_path)
536+
537+
assert result == "sha256:1234567890abcdef"
538+
mock_manager.client.client.images.load.assert_called_once()
539+
finally:
540+
# Clean up
541+
try:
542+
os.unlink(temp_file_path)
543+
except:
544+
pass
545+
546+
@pytest.mark.asyncio
547+
async def test_load_image_from_tar_file_empty_images(self, mock_manager):
548+
"""Test loading when no images are found in tar file (covers lines 69-70)"""
549+
# Create a temporary file
550+
with tempfile.NamedTemporaryFile(delete=False, suffix='.tar') as temp_file:
551+
temp_file.write(b"fake tar content")
552+
temp_file_path = temp_file.name
553+
554+
try:
555+
mock_manager.client.client.images.load.return_value = []
556+
557+
with pytest.raises(MCPContainerError, match="No images found in tar file"):
558+
await mock_manager.load_image_from_tar_file(temp_file_path)
559+
finally:
560+
# Clean up
561+
try:
562+
os.unlink(temp_file_path)
563+
except:
564+
pass
565+
566+
@pytest.mark.asyncio
567+
async def test_load_image_from_tar_file_exception(self, mock_manager):
568+
"""Test loading when exception occurs (covers lines 80-82)"""
569+
# Create a temporary file
570+
with tempfile.NamedTemporaryFile(delete=False, suffix='.tar') as temp_file:
571+
temp_file.write(b"fake tar content")
572+
temp_file_path = temp_file.name
573+
574+
try:
575+
mock_manager.client.client.images.load.side_effect = Exception(
576+
"File not found")
577+
578+
with pytest.raises(MCPContainerError, match="Failed to load image from tar file: File not found"):
579+
await mock_manager.load_image_from_tar_file(temp_file_path)
580+
finally:
581+
# Clean up
582+
try:
583+
os.unlink(temp_file_path)
584+
except:
585+
pass
586+
587+
588+
# ---------------------------------------------------------------------------
589+
# Test start_mcp_container_from_tar
590+
# ---------------------------------------------------------------------------
591+
592+
593+
class TestStartMCPContainerFromTar:
594+
"""Test start_mcp_container_from_tar method"""
595+
596+
@pytest.fixture
597+
def mock_manager(self):
598+
"""Create MCPContainerManager instance with mocked client"""
599+
with patch('services.mcp_container_service.create_container_client_from_config'), \
600+
patch('services.mcp_container_service.DockerContainerConfig'):
601+
manager = MCPContainerManager()
602+
manager.client = MagicMock()
603+
return manager
604+
605+
@pytest.mark.asyncio
606+
async def test_start_mcp_container_from_tar_success(self, mock_manager):
607+
"""Test successful starting of MCP container from tar file"""
608+
# Mock load_image_from_tar_file
609+
mock_manager.load_image_from_tar_file = AsyncMock(
610+
return_value="loaded-image:latest")
611+
612+
# Mock start_mcp_container
613+
mock_manager.start_mcp_container = AsyncMock(return_value={
614+
"container_id": "container-123",
615+
"mcp_url": "http://localhost:5020/mcp",
616+
"host_port": "5020",
617+
"status": "started",
618+
"container_name": "test-service-user1234"
619+
})
620+
621+
result = await mock_manager.start_mcp_container_from_tar(
622+
tar_file_path="/path/to/image.tar",
623+
service_name="test-service",
624+
tenant_id="tenant123",
625+
user_id="user12345",
626+
env_vars={"NODE_ENV": "production"},
627+
host_port=5020,
628+
full_command=["npx", "-y", "test-mcp"]
629+
)
630+
631+
assert result["container_id"] == "container-123"
632+
assert result["mcp_url"] == "http://localhost:5020/mcp"
633+
mock_manager.load_image_from_tar_file.assert_called_once_with(
634+
"/path/to/image.tar")
635+
mock_manager.start_mcp_container.assert_called_once_with(
636+
service_name="test-service",
637+
tenant_id="tenant123",
638+
user_id="user12345",
639+
env_vars={"NODE_ENV": "production"},
640+
host_port=5020,
641+
image="loaded-image:latest",
642+
full_command=["npx", "-y", "test-mcp"]
643+
)
644+
645+
@pytest.mark.asyncio
646+
async def test_start_mcp_container_from_tar_load_image_error(self, mock_manager):
647+
"""Test starting container when load_image_from_tar_file fails (covers lines 178-181)"""
648+
# Mock load_image_from_tar_file to raise error
649+
mock_manager.load_image_from_tar_file = AsyncMock(
650+
side_effect=MCPContainerError("Failed to load image"))
651+
652+
with pytest.raises(MCPContainerError, match="Failed to start container from tar file: Failed to load image"):
653+
await mock_manager.start_mcp_container_from_tar(
654+
tar_file_path="/path/to/image.tar",
655+
service_name="test-service",
656+
tenant_id="tenant123",
657+
user_id="user12345"
658+
)
659+
660+
@pytest.mark.asyncio
661+
async def test_start_mcp_container_from_tar_start_container_error(self, mock_manager):
662+
"""Test starting container when start_mcp_container fails (covers lines 178-181)"""
663+
# Mock load_image_from_tar_file to succeed
664+
mock_manager.load_image_from_tar_file = AsyncMock(
665+
return_value="loaded-image:latest")
666+
667+
# Mock start_mcp_container to raise error
668+
mock_manager.start_mcp_container = AsyncMock(
669+
side_effect=MCPContainerError("Container startup failed"))
670+
671+
with pytest.raises(MCPContainerError, match="Failed to start container from tar file: Container startup failed"):
672+
await mock_manager.start_mcp_container_from_tar(
673+
tar_file_path="/path/to/image.tar",
674+
service_name="test-service",
675+
tenant_id="tenant123",
676+
user_id="user12345"
677+
)
678+
679+
476680
if __name__ == "__main__":
477681
pytest.main([__file__])

0 commit comments

Comments
 (0)