diff --git a/tests/test_server.py b/tests/test_server.py index 4d4a90c..d9e2732 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,7 +11,12 @@ from cordra_mcp.client import ( CordraNotFoundError, DigitalObject, ) -from cordra_mcp.server import get_cordra_design, get_cordra_object, search_objects +from cordra_mcp.server import ( + count_objects, + get_cordra_design, + get_cordra_object, + search_objects, +) @pytest.fixture @@ -32,16 +37,16 @@ def sample_digital_object(): "name": "profile_photo", "filename": "john_doe_profile.jpg", "size": 125440, - "mediaType": "image/jpeg" + "mediaType": "image/jpeg", } - ] + ], ) class TestGetCordraObject: """Test the get_cordra_object resource handler.""" - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_object_success(self, mock_client, sample_digital_object): """Test successful object retrieval.""" mock_client.get_object = AsyncMock(return_value=sample_digital_object) @@ -61,7 +66,7 @@ class TestGetCordraObject: # Verify the client was called with the correct object ID mock_client.get_object.assert_called_once_with("people/john-doe-123") - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_object_not_found(self, mock_client): """Test object not found exception.""" mock_client.get_object = AsyncMock( @@ -74,7 +79,7 @@ class TestGetCordraObject: assert "Object not found: people/nonexistent" in str(exc_info.value) mock_client.get_object.assert_called_once_with("people/nonexistent") - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_object_client_error(self, mock_client): """Test general client error handling.""" mock_client.get_object = AsyncMock( @@ -88,7 +93,7 @@ class TestGetCordraObject: assert "Connection failed" in str(exc_info.value) mock_client.get_object.assert_called_once_with("people/john-doe-123") - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_object_id_construction(self, mock_client, sample_digital_object): """Test that object ID is correctly constructed from prefix and suffix.""" mock_client.get_object = AsyncMock(return_value=sample_digital_object) @@ -104,7 +109,7 @@ class TestGetCordraObject: await get_cordra_object(prefix, suffix) mock_client.get_object.assert_called_with(expected_id) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_json_formatting(self, mock_client, sample_digital_object): """Test that the returned JSON is properly formatted.""" mock_client.get_object = AsyncMock(return_value=sample_digital_object) @@ -126,7 +131,7 @@ class TestGetCordraObject: assert "acl" in parsed_result assert "payloads" in parsed_result - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_minimal_object(self, mock_client): """Test handling of object with minimal data.""" minimal_object = DigitalObject( @@ -135,7 +140,7 @@ class TestGetCordraObject: content={"id": "test/minimal"}, metadata=None, acl=None, - payloads=None + payloads=None, ) mock_client.get_object = AsyncMock(return_value=minimal_object) @@ -153,17 +158,18 @@ class TestGetCordraObject: class TestSchemaResourceFunctions: """Test the schema resource functions.""" - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_create_schema_resource_success(self, mock_client): """Test successful schema resource creation.""" mock_schema = DigitalObject( id="test/user-schema", type="Schema", - content={"name": "User", "type": "object", "properties": {}} + content={"name": "User", "type": "object", "properties": {}}, ) mock_client.get_schema = AsyncMock(return_value=mock_schema) from cordra_mcp.server import create_schema_resource + result = await create_schema_resource("User") # Verify the result is valid JSON @@ -175,88 +181,101 @@ class TestSchemaResourceFunctions: # Verify the client was called with correct schema name mock_client.get_schema.assert_called_once_with("User") - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_create_schema_resource_not_found(self, mock_client): """Test schema resource creation with schema not found.""" - mock_client.get_schema = AsyncMock(side_effect=CordraNotFoundError("Schema not found")) + mock_client.get_schema = AsyncMock( + side_effect=CordraNotFoundError("Schema not found") + ) from cordra_mcp.server import create_schema_resource + with pytest.raises(RuntimeError) as exc_info: await create_schema_resource("NonExistent") assert "Schema not found: NonExistent" in str(exc_info.value) mock_client.get_schema.assert_called_once_with("NonExistent") - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_register_schema_resources_success(self, mock_client): """Test successful schema resource registration.""" mock_search_result = { "results": [ {"content": {"name": "User"}, "id": "test/user-schema"}, {"content": {"name": "Project"}, "id": "test/project-schema"}, - {"content": {"name": "Document"}, "id": "test/doc-schema"} + {"content": {"name": "Document"}, "id": "test/doc-schema"}, ], "total_size": 3, "page_num": 0, - "page_size": 20 + "page_size": 20, } mock_client.find = AsyncMock(return_value=mock_search_result) # Mock the mcp.add_resource method - with patch('cordra_mcp.server.mcp') as mock_mcp: + with patch("cordra_mcp.server.mcp") as mock_mcp: from cordra_mcp.server import register_schema_resources + await register_schema_resources() # Verify the client was called with correct query - mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0) + mock_client.find.assert_called_once_with( + "type:Schema", page_size=20, page_num=0 + ) # Verify add_resource was called for each schema assert mock_mcp.add_resource.call_count == 3 - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_register_schema_resources_missing_name(self, mock_client): """Test schema resource registration with objects missing name field.""" mock_search_result = { "results": [ {"content": {"name": "User"}, "id": "test/user-schema"}, {"content": {}, "id": "test/no-name-schema"}, # Missing name field - {"content": {"name": "Project"}, "id": "test/project-schema"} + {"content": {"name": "Project"}, "id": "test/project-schema"}, ], "total_size": 3, "page_num": 0, - "page_size": 20 + "page_size": 20, } mock_client.find = AsyncMock(return_value=mock_search_result) - with patch('cordra_mcp.server.mcp') as mock_mcp: + with patch("cordra_mcp.server.mcp") as mock_mcp: from cordra_mcp.server import register_schema_resources + await register_schema_resources() # Only 2 schemas should be registered (those with name field) assert mock_mcp.add_resource.call_count == 2 - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_register_schema_resources_client_error(self, mock_client): """Test schema resource registration with client error.""" mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed")) # Should not raise an exception, just log a warning from cordra_mcp.server import register_schema_resources + await register_schema_resources() # Should complete without raising - mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0) + mock_client.find.assert_called_once_with( + "type:Schema", page_size=20, page_num=0 + ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_register_schema_resources_pagination(self, mock_client): """Test schema resource registration with pagination.""" # Mock multiple pages of results # First page with full 20 results (simulating more schemas) - first_page_schemas = [{"content": {"name": f"Schema{i}"}, "id": f"test/schema{i}"} for i in range(20)] + first_page_schemas = [ + {"content": {"name": f"Schema{i}"}, "id": f"test/schema{i}"} + for i in range(20) + ] first_page = { "results": first_page_schemas, "total_size": 25, "page_num": 0, - "page_size": 20 + "page_size": 20, } # Second page with fewer results (indicating last page) @@ -266,14 +285,15 @@ class TestSchemaResourceFunctions: ], "total_size": 25, "page_num": 1, - "page_size": 20 + "page_size": 20, } # Return first page, then second page (with fewer results indicating last page) mock_client.find = AsyncMock(side_effect=[first_page, second_page]) - with patch('cordra_mcp.server.mcp') as mock_mcp: + with patch("cordra_mcp.server.mcp") as mock_mcp: from cordra_mcp.server import register_schema_resources + await register_schema_resources() # Verify pagination calls @@ -288,17 +308,25 @@ class TestSchemaResourceFunctions: class TestSearchObjects: """Test the search_objects tool.""" - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_success(self, mock_client): """Test successful object search.""" mock_search_result = { "results": [ - {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, - {"id": "people/jane-smith", "type": "Person", "content": {"name": "Jane Smith"}}, + { + "id": "people/john-doe", + "type": "Person", + "content": {"name": "John Doe"}, + }, + { + "id": "people/jane-smith", + "type": "Person", + "content": {"name": "Jane Smith"}, + }, ], "total_size": 2, "page_num": 0, - "page_size": 1000 + "page_size": 1000, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -316,16 +344,20 @@ class TestSearchObjects: "name:John", object_type=None, page_size=25, page_num=0 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_with_type_filter(self, mock_client): """Test object search with type filter.""" mock_search_result = { "results": [ - {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, + { + "id": "people/john-doe", + "type": "Person", + "content": {"name": "John Doe"}, + }, ], "total_size": 1, "page_num": 0, - "page_size": 1000 + "page_size": 1000, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -341,39 +373,49 @@ class TestSearchObjects: "name:John", object_type="Person", page_size=25, page_num=0 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_with_limit(self, mock_client): """Test object search with custom limit.""" mock_search_result = { "results": [ - {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, + { + "id": "people/john-doe", + "type": "Person", + "content": {"name": "John Doe"}, + }, ], "total_size": 1, "page_num": 0, - "page_size": 50 + "page_size": 50, } mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("name:John", limit=50) - # Verify the result is valid JSON + # Verify the result is valid JSON with new format parsed_result = json.loads(result) assert parsed_result["results"] == ["people/john-doe"] assert parsed_result["page_size"] == 50 # Verify the client was called with custom limit - mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=50, page_num=0) + mock_client.find.assert_called_once_with( + "name:John", object_type=None, page_size=50, page_num=0 + ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_with_all_parameters(self, mock_client): """Test object search with all parameters.""" mock_search_result = { "results": [ - {"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}}, + { + "id": "documents/report-123", + "type": "Document", + "content": {"title": "Report"}, + }, ], "total_size": 1, "page_num": 0, - "page_size": 25 + "page_size": 25, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -389,14 +431,14 @@ class TestSearchObjects: "title:Report", object_type="Document", page_size=25, page_num=0 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_empty_results(self, mock_client): """Test object search with no results.""" mock_search_result = { "results": [], "total_size": 0, "page_num": 0, - "page_size": 1000 + "page_size": 1000, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -411,7 +453,7 @@ class TestSearchObjects: "nonexistent:data", object_type=None, page_size=25, page_num=0 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_client_error(self, mock_client): """Test object search with client error.""" mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed")) @@ -424,7 +466,7 @@ class TestSearchObjects: "test:query", object_type=None, page_size=25, page_num=0 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_value_error(self, mock_client): """Test object search with value error.""" mock_client.find = AsyncMock(side_effect=ValueError("Invalid query")) @@ -437,7 +479,7 @@ class TestSearchObjects: "invalid:query", object_type=None, page_size=25, page_num=0 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_json_formatting(self, mock_client): """Test that search results are properly formatted as JSON.""" mock_search_result = { @@ -446,7 +488,7 @@ class TestSearchObjects: ], "total_size": 1, "page_num": 0, - "page_size": 1000 + "page_size": 1000, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -454,7 +496,7 @@ class TestSearchObjects: # Verify it's valid JSON with proper indentation parsed_result = json.loads(result) - assert isinstance(parsed_result, list) + assert isinstance(parsed_result, dict) # Check that the result contains indentation (pretty-printed) assert " " in result # Should have 2-space indentation @@ -463,16 +505,20 @@ class TestSearchObjects: assert parsed_result["results"] == ["test/object"] assert parsed_result["total_count"] == 1 - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_with_page_num(self, mock_client): """Test object search with page number parameter.""" mock_search_result = { "results": [ - {"id": "documents/doc-21", "type": "Document", "content": {"title": "Page 2 Doc"}}, + { + "id": "documents/doc-21", + "type": "Document", + "content": {"title": "Page 2 Doc"}, + }, ], "total_size": 50, "page_num": 1, - "page_size": 20 + "page_size": 20, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -483,7 +529,7 @@ class TestSearchObjects: "type:Document", object_type=None, page_size=25, page_num=1 ) - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_search_objects_with_all_pagination_params(self, mock_client): """Test object search with all pagination parameters.""" mock_search_result = { @@ -496,7 +542,7 @@ class TestSearchObjects: ], "total_size": 100, "page_num": 5, - "page_size": 10 + "page_size": 10, } mock_client.find = AsyncMock(return_value=mock_search_result) @@ -511,7 +557,7 @@ class TestSearchObjects: class TestGetCordraDesign: """Test the get_cordra_design resource handler.""" - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_design_success(self, mock_client): """Test successful design object retrieval.""" mock_design = DigitalObject( @@ -520,9 +566,9 @@ class TestGetCordraDesign: content={ "types": {"User": {}, "Project": {}}, "workflows": {}, - "systemConfig": {"serverName": "test-cordra"} + "systemConfig": {"serverName": "test-cordra"}, }, - metadata={"created": "2023-01-01", "modified": "2023-06-15"} + metadata={"created": "2023-01-01", "modified": "2023-06-15"}, ) mock_client.get_design = AsyncMock(return_value=mock_design) @@ -539,7 +585,7 @@ class TestGetCordraDesign: # Verify the client was called mock_client.get_design.assert_called_once() - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_design_not_found(self, mock_client): """Test design object not found exception.""" mock_client.get_design = AsyncMock( @@ -552,7 +598,7 @@ class TestGetCordraDesign: assert "Design object not found" in str(exc_info.value) mock_client.get_design.assert_called_once() - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_design_authentication_error(self, mock_client): """Test design object authentication error.""" mock_client.get_design = AsyncMock( @@ -565,7 +611,7 @@ class TestGetCordraDesign: assert "Authentication failed" in str(exc_info.value) mock_client.get_design.assert_called_once() - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_design_client_error(self, mock_client): """Test design object general client error.""" mock_client.get_design = AsyncMock( @@ -579,14 +625,14 @@ class TestGetCordraDesign: assert "Connection failed" in str(exc_info.value) mock_client.get_design.assert_called_once() - @patch('cordra_mcp.server.cordra_client') + @patch("cordra_mcp.server.cordra_client") async def test_get_design_json_formatting(self, mock_client): """Test that the design object is properly formatted as JSON.""" mock_design = DigitalObject( id="design", type="CordraDesign", content={"data": "value"}, - metadata={"created": "2023-01-01"} + metadata={"created": "2023-01-01"}, ) mock_client.get_design = AsyncMock(return_value=mock_design)