"""Tests for MCP server management endpoints."""
import pytest
import uuid
from unittest.mock import AsyncMock, patch
from fastapi.testclient import TestClient
from preloop.models.models.mcp_server import MCPServer
@pytest.fixture(autouse=True)
def mock_event_bus_connect():
"""Auto-mock the event bus connect method to avoid NATS connection attempts."""
with patch(
"preloop.sync.services.event_bus.EventBus.connect", new_callable=AsyncMock
) as mock_connect:
yield mock_connect
@pytest.fixture(autouse=True)
def mock_mcp_client():
"""Auto-mock MCPClient to avoid actual connection attempts."""
with patch("preloop.services.mcp_client_pool.MCPClient") as mock_client_class:
mock_instance = AsyncMock()
mock_instance.connect = AsyncMock()
mock_instance.close = AsyncMock()
mock_client_class.return_value = mock_instance
yield mock_instance
@pytest.fixture(autouse=True)
def mock_tool_scan():
"""Auto-mock tool scanning to avoid actual MCP server connections."""
with patch(
"preloop.api.endpoints.mcp_servers.scan_mcp_server_tools",
new_callable=AsyncMock,
) as mock_scan:
mock_scan.return_value = []
yield mock_scan
def test_list_mcp_servers_empty(client: TestClient, db_session):
"""Test listing MCP servers when none exist."""
response = client.get("/api/v1/mcp-servers")
assert response.status_code == 200
assert response.json() == []
def test_list_mcp_servers_with_data(client: TestClient, db_session, test_user):
"""Test listing MCP servers with existing data.
This test verifies that:
1. MCP servers are properly retrieved from the database
2. UUID fields (id, account_id) are correctly serialized to strings
3. The response matches the MCPServerResponse schema
"""
server = MCPServer(
name="Test MCP Server",
url="http://localhost:8080/mcp",
transport="http-streaming",
auth_type="none",
account_id=test_user.account_id,
status="active",
)
db_session.add(server)
db_session.commit()
db_session.refresh(server)
response = client.get("/api/v1/mcp-servers")
assert response.status_code == 200
response_json = response.json()
assert len(response_json) == 1
server_data = response_json[0]
assert server_data["name"] == "Test MCP Server"
assert server_data["url"] == "http://localhost:8080/mcp"
# Critical: Verify UUID fields are serialized to strings, not UUID objects
assert isinstance(server_data["id"], str)
assert isinstance(server_data["account_id"], str)
assert server_data["id"] == str(server.id)
assert server_data["account_id"] == str(server.account_id)
def test_create_mcp_server_success(client: TestClient, db_session, test_user):
"""Test creating an MCP server successfully.
This test specifically verifies the bug fix:
- account_id should be serialized to string in the response
- The schema should accept UUID from the model and serialize it
"""
server_data = {
"name": "New MCP Server",
"url": "http://localhost:8080/mcp",
"transport": "http-streaming",
"auth_type": "none",
}
response = client.post("/api/v1/mcp-servers", json=server_data)
assert response.status_code == 201
response_json = response.json()
assert response_json["name"] == "New MCP Server"
assert response_json["url"] == "http://localhost:8080/mcp"
assert response_json["status"] == "active"
# Critical: Verify UUID serialization (this is the bug we're testing for)
assert isinstance(response_json["id"], str)
assert isinstance(response_json["account_id"], str)
# Verify the UUID can be parsed
try:
uuid.UUID(response_json["id"])
uuid.UUID(response_json["account_id"])
except ValueError:
pytest.fail("id or account_id is not a valid UUID string")
# Verify it was actually saved to the database
server_id = uuid.UUID(response_json["id"])
db_server = db_session.query(MCPServer).filter(MCPServer.id == server_id).first()
assert db_server is not None
assert db_server.name == "New MCP Server"
assert db_server.account_id == test_user.account_id
def test_create_mcp_server_duplicate_name(client: TestClient, db_session, test_user):
"""Test creating an MCP server with a duplicate name."""
# Create first server
server = MCPServer(
name="Duplicate Server",
url="http://localhost:8080/mcp",
transport="http-streaming",
auth_type="none",
account_id=test_user.account_id,
status="active",
)
db_session.add(server)
db_session.commit()
# Try to create another with the same name
server_data = {
"name": "Duplicate Server",
"url": "http://localhost:9090/mcp",
"transport": "http-streaming",
"auth_type": "none",
}
response = client.post("/api/v1/mcp-servers", json=server_data)
assert response.status_code == 400
assert "already exists" in response.json()["detail"]
def test_create_mcp_server_with_connection_error(
client: TestClient, db_session, test_user, mock_mcp_client
):
"""Test creating an MCP server when connection validation fails.
The server should still be created but with error status.
"""
# Make the mock client raise an error on connect
mock_mcp_client.connect.side_effect = Exception("Connection refused")
server_data = {
"name": "Failing Server",
"url": "http://unreachable:8080/mcp",
"transport": "http-streaming",
"auth_type": "none",
}
response = client.post("/api/v1/mcp-servers", json=server_data)
assert response.status_code == 201
response_json = response.json()
assert response_json["name"] == "Failing Server"
assert response_json["status"] == "error"
assert response_json["last_error"] is not None
assert "Connection refused" in response_json["last_error"]
# Still verify UUID serialization works even with error status
assert isinstance(response_json["id"], str)
assert isinstance(response_json["account_id"], str)
def test_get_mcp_server_success(client: TestClient, db_session, test_user):
"""Test getting a single MCP server by ID."""
server = MCPServer(
name="Test Server",
url="http://localhost:8080/mcp",
transport="http-streaming",
auth_type="none",
account_id=test_user.account_id,
status="active",
)
db_session.add(server)
db_session.commit()
db_session.refresh(server)
response = client.get(f"/api/v1/mcp-servers/{server.id}")
assert response.status_code == 200
response_json = response.json()
assert response_json["name"] == "Test Server"
assert isinstance(response_json["id"], str)
assert isinstance(response_json["account_id"], str)
def test_get_mcp_server_not_found(client: TestClient, db_session, test_user):
"""Test getting a non-existent MCP server."""
fake_id = uuid.uuid4()
response = client.get(f"/api/v1/mcp-servers/{fake_id}")
assert response.status_code == 404
def test_update_mcp_server_success(client: TestClient, db_session, test_user):
"""Test updating an MCP server."""
server = MCPServer(
name="Original Name",
url="http://localhost:8080/mcp",
transport="http-streaming",
auth_type="none",
account_id=test_user.account_id,
status="active",
)
db_session.add(server)
db_session.commit()
db_session.refresh(server)
update_data = {
"name": "Updated Name",
"status": "disabled",
}
response = client.put(f"/api/v1/mcp-servers/{server.id}", json=update_data)
assert response.status_code == 200
response_json = response.json()
assert response_json["name"] == "Updated Name"
assert response_json["status"] == "disabled"
# Verify UUID serialization on update
assert isinstance(response_json["id"], str)
assert isinstance(response_json["account_id"], str)
def test_delete_mcp_server_success(client: TestClient, db_session, test_user):
"""Test deleting an MCP server."""
server = MCPServer(
name="Server to Delete",
url="http://localhost:8080/mcp",
transport="http-streaming",
auth_type="none",
account_id=test_user.account_id,
status="active",
)
db_session.add(server)
db_session.commit()
db_session.refresh(server)
response = client.delete(f"/api/v1/mcp-servers/{server.id}")
assert response.status_code == 200
# Verify the server is deleted from database
deleted = db_session.query(MCPServer).filter(MCPServer.id == server.id).first()
assert deleted is None
def test_delete_mcp_server_not_found(client: TestClient, db_session, test_user):
"""Test deleting a non-existent MCP server."""
fake_id = uuid.uuid4()
response = client.delete(f"/api/v1/mcp-servers/{fake_id}")
assert response.status_code == 404
def test_schema_serialization_with_real_model(db_session, test_user):
"""Direct unit test for schema serialization with real database model.
This test verifies the fix at the schema level:
- MCPServer model has account_id as UUID
- MCPServerResponse schema should accept UUID and serialize to string
"""
from preloop.models.schemas.mcp_server import MCPServerResponse
# Create a real MCPServer model instance with UUID account_id
server = MCPServer(
name="Schema Test Server",
url="http://localhost:8080/mcp",
transport="http-streaming",
auth_type="none",
account_id=test_user.account_id, # This is a UUID
status="active",
)
db_session.add(server)
db_session.commit()
db_session.refresh(server)
# Verify the model has UUID types
assert isinstance(server.id, uuid.UUID)
assert isinstance(server.account_id, uuid.UUID)
# This should not raise a ValidationError anymore
response = MCPServerResponse.model_validate(server)
# Verify the response has serialized UUIDs to strings
response_dict = response.model_dump()
assert isinstance(response_dict["id"], str)
assert isinstance(response_dict["account_id"], str)
assert response_dict["id"] == str(server.id)
assert response_dict["account_id"] == str(server.account_id)