mirror of
https://github.com/dnlbauer/cordra-mcp.git
synced 2026-09-10 13:45:30 +00:00
Merge pull request #1 from dnlbauer/fix-schema-pagination
Fix schema pagination and enhance search functionality
This commit is contained in:
@@ -24,7 +24,11 @@ ensuring safe exploration without risk of data modification or corruption.
|
||||
|
||||
### Tools
|
||||
|
||||
- `search_objects` - Search for digital objects using a query string.
|
||||
- `search_objects` - Search for digital objects using a query string with pagination support.
|
||||
- `query` - Lucene/Solr compatible search query
|
||||
- `type` - Optional filter by object type
|
||||
- `limit` - Number of results per page (default: 1)
|
||||
- `page_num` - Page number to retrieve, 0-based (default: 0)
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -35,7 +39,6 @@ The MCP server can be configured using environment variables with the `CORDRA_`
|
||||
- `CORDRA_PASSWORD` - Password for authentication (optional)
|
||||
- `CORDRA_VERIFY_SSL` - SSL certificate verification (default: `true`)
|
||||
- `CORDRA_TIMEOUT` - Request timeout in seconds (default: `30`)
|
||||
- `CORDRA_MAX_SEARCH_RESULTS` - Maximum search results (default: `1000`)
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -129,16 +129,21 @@ class CordraClient:
|
||||
f"Failed to retrieve object {object_id}: {e}"
|
||||
) from e
|
||||
|
||||
async def find(self, query: str, object_type: str | None = None, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
"""Find objects using a Cordra query.
|
||||
async def find(self, query: str, object_type: str | None = None, page_size: int = 20, page_num: int = 0) -> dict[str, Any]:
|
||||
"""Find objects using a Cordra query with pagination support.
|
||||
|
||||
Args:
|
||||
query: The query string to search for objects
|
||||
object_type: Optional filter by object type
|
||||
limit: Optional limit on number of results
|
||||
page_size: Number of results per page (if None, no limit)
|
||||
page_num: Page number to retrieve (0-based, default: 0)
|
||||
|
||||
Returns:
|
||||
List of objects matching the query as dictionaries
|
||||
Dict containing:
|
||||
- results: List of objects matching the query as dictionaries
|
||||
- total_size: Total number of results available
|
||||
- page_num: Current page number
|
||||
- page_size: Number of results per page
|
||||
|
||||
Raises:
|
||||
ValueError: If query is empty
|
||||
@@ -151,11 +156,11 @@ class CordraClient:
|
||||
final_query = f"type:{object_type} AND ({query})"
|
||||
|
||||
url = f"{self.config.base_url}/search"
|
||||
params = {"query": final_query}
|
||||
|
||||
# Add pageSize if limit is specified
|
||||
if limit is not None:
|
||||
params["pageSize"] = str(limit)
|
||||
params = {
|
||||
"query": final_query,
|
||||
"pageSize": str(page_size),
|
||||
"pageNum": str(page_num),
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
||||
@@ -167,11 +172,12 @@ class CordraClient:
|
||||
|
||||
search_result = response.json()
|
||||
|
||||
# Extract the results array from the response
|
||||
if isinstance(search_result, dict) and "results" in search_result:
|
||||
return search_result["results"] # type: ignore
|
||||
else:
|
||||
return []
|
||||
return {
|
||||
"results": search_result["results"],
|
||||
"total_size": search_result["size"],
|
||||
"page_num": search_result["pageNum"],
|
||||
"page_size": search_result["pageSize"]
|
||||
}
|
||||
|
||||
except requests.RequestException as e:
|
||||
raise CordraClientError(
|
||||
@@ -196,7 +202,8 @@ class CordraClient:
|
||||
query = f"type:Schema AND /name:{schema_name}"
|
||||
|
||||
try:
|
||||
schemas = await self.find(query)
|
||||
search_result = await self.find(query)
|
||||
schemas = search_result["results"]
|
||||
|
||||
if not schemas:
|
||||
raise CordraNotFoundError(f"Schema '{schema_name}' not found")
|
||||
|
||||
@@ -22,9 +22,6 @@ class CordraConfig(BaseSettings):
|
||||
password: str | None = Field(
|
||||
default=None, description="Password for Cordra authentication"
|
||||
)
|
||||
max_search_results: int = Field(
|
||||
default=1000, description="Maximum number of search results to return"
|
||||
)
|
||||
verify_ssl: bool = Field(
|
||||
default=True, description="Whether to verify SSL certificates"
|
||||
)
|
||||
|
||||
@@ -35,14 +35,20 @@ Examples:
|
||||
- /author:smith - Find objects by author Smith
|
||||
- /name:John AND type:Person - Complex queries
|
||||
|
||||
Pagination:
|
||||
- Results are paginated with 0-based page numbering
|
||||
- Use 'limit' to control page size (default: 1)
|
||||
- Use 'page_num' to specify which page to retrieve (default: 0)
|
||||
|
||||
Returns a JSON list of matching objects with their full metadata."""
|
||||
)
|
||||
async def search_objects(
|
||||
query: str,
|
||||
type: str | None = None,
|
||||
limit: int | None = None,
|
||||
limit: int = 1,
|
||||
page_num: int = 0,
|
||||
) -> str:
|
||||
"""Search for digital objects in the Cordra repository.
|
||||
"""Search for digital objects in the Cordra repository with pagination support.
|
||||
|
||||
Args:
|
||||
query: The search query string (Lucene/Solr compatible). Examples:
|
||||
@@ -50,14 +56,15 @@ async def search_objects(
|
||||
- "/author:smith" - Find objects by author Smith
|
||||
- "/name:John AND type:Person" - Complex queries
|
||||
type: Optional filter by object type (e.g., "Person", "Document", "Project")
|
||||
limit: Optional limit on number of results (default: config max_search_results)
|
||||
limit: Page size - number of results per page (default: 1)
|
||||
page_num: Page number to retrieve, 0-based (default: 0 for first page)
|
||||
|
||||
Returns:
|
||||
JSON string containing list of matching objects with their full metadata
|
||||
"""
|
||||
try:
|
||||
effective_limit = limit if limit is not None else config.max_search_results
|
||||
results = await cordra_client.find(query, object_type=type, limit=effective_limit)
|
||||
search_result = await cordra_client.find(query, object_type=type, page_size=limit, page_num=page_num)
|
||||
results = search_result["results"]
|
||||
return json.dumps(results, indent=2)
|
||||
|
||||
except ValueError as e:
|
||||
@@ -155,10 +162,23 @@ async def create_schema_resource(schema_name: str) -> str:
|
||||
async def register_schema_resources() -> None:
|
||||
"""Register individual schema resources dynamically."""
|
||||
try:
|
||||
# Get all available schemas
|
||||
schemas = await cordra_client.find("type:Schema")
|
||||
# Get all available schemas using pagination
|
||||
all_schemas = []
|
||||
page_num = 0
|
||||
page_size = 20
|
||||
|
||||
for schema in schemas:
|
||||
while True:
|
||||
search_result = await cordra_client.find("type:Schema", page_size=page_size, page_num=page_num)
|
||||
schemas = search_result["results"]
|
||||
all_schemas.extend(schemas)
|
||||
|
||||
# Check if we've retrieved all schemas
|
||||
if len(schemas) < page_size:
|
||||
break
|
||||
|
||||
page_num += 1
|
||||
|
||||
for schema in all_schemas:
|
||||
schema_name = schema.get("content", {}).get("name")
|
||||
if not schema_name:
|
||||
logger.warning("Schema without a name found, skipping.")
|
||||
@@ -180,7 +200,7 @@ async def register_schema_resources() -> None:
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"Registered {len(schemas)} schema resources")
|
||||
logger.info(f"Registered {len(all_schemas)} schema resources")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to register schema resources: {e}")
|
||||
|
||||
@@ -170,6 +170,8 @@ class TestCordraClient:
|
||||
{"name": "Document", "identifier": "test/doc-schema"},
|
||||
],
|
||||
"size": 3,
|
||||
"pageNum": 0,
|
||||
"pageSize": 20,
|
||||
}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
@@ -178,21 +180,25 @@ class TestCordraClient:
|
||||
|
||||
result = await client.find("type:Schema")
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0]["name"] == "User"
|
||||
assert result[1]["name"] == "Project"
|
||||
assert result[2]["name"] == "Document"
|
||||
assert isinstance(result, dict)
|
||||
assert len(result["results"]) == 3
|
||||
assert result["results"][0]["name"] == "User"
|
||||
assert result["results"][1]["name"] == "Project"
|
||||
assert result["results"][2]["name"] == "Document"
|
||||
assert result["total_size"] == 3
|
||||
assert result["page_num"] == 0
|
||||
assert result["page_size"] == 20
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:Schema"},
|
||||
params={"query": "type:Schema", "pageSize": "20", "pageNum": "0"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_empty_results(self, mock_get, client):
|
||||
"""Test find with empty results."""
|
||||
mock_response_data = {"results": [], "size": 0}
|
||||
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_response_data
|
||||
@@ -200,26 +206,17 @@ class TestCordraClient:
|
||||
|
||||
result = await client.find("type:NonExistent")
|
||||
|
||||
assert result == []
|
||||
assert isinstance(result, dict)
|
||||
assert result["results"] == []
|
||||
assert result["total_size"] == 0
|
||||
assert result["page_num"] == 0
|
||||
assert result["page_size"] == 20
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:NonExistent"},
|
||||
params={"query": "type:NonExistent", "pageSize": "20", "pageNum": "0"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_no_results_key(self, mock_get, client):
|
||||
"""Test find with response missing results key."""
|
||||
mock_response_data = {"size": 0} # No results key
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.raise_for_status.return_value = None
|
||||
|
||||
result = await client.find("type:Schema")
|
||||
|
||||
assert result == []
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_error(self, mock_get, client):
|
||||
"""Test find error handling."""
|
||||
@@ -236,64 +233,108 @@ class TestCordraClient:
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_with_type_filter(self, mock_get, client):
|
||||
"""Test find operation with type filter constructs correct query."""
|
||||
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"results": []}
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.ok = True
|
||||
|
||||
await client.find("name:John", object_type="Person")
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:Person AND (name:John)"},
|
||||
params={"query": "type:Person AND (name:John)", "pageSize": "20", "pageNum": "0"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_with_limit(self, mock_get, client):
|
||||
"""Test find operation with limit adds pageSize parameter."""
|
||||
async def test_find_with_page_size(self, mock_get, client):
|
||||
"""Test find operation with custom page size."""
|
||||
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 50}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"results": []}
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.ok = True
|
||||
|
||||
await client.find("type:Test", limit=50)
|
||||
await client.find("type:Test", page_size=50)
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:Test", "pageSize": "50"},
|
||||
params={"query": "type:Test", "pageSize": "50", "pageNum": "0"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_with_type_and_limit(self, mock_get, client):
|
||||
"""Test find operation with both type filter and limit."""
|
||||
async def test_find_with_type_and_page_size(self, mock_get, client):
|
||||
"""Test find operation with both type filter and page size."""
|
||||
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 25}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"results": []}
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.ok = True
|
||||
|
||||
await client.find("title:Report", object_type="Document", limit=25)
|
||||
await client.find("title:Report", object_type="Document", page_size=25)
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:Document AND (title:Report)", "pageSize": "25"},
|
||||
params={"query": "type:Document AND (title:Report)", "pageSize": "25", "pageNum": "0"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_no_optional_params(self, mock_get, client):
|
||||
"""Test find operation with no optional parameters."""
|
||||
async def test_find_default_params(self, mock_get, client):
|
||||
"""Test find operation with default parameters."""
|
||||
mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"results": []}
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.ok = True
|
||||
|
||||
await client.find("content:test")
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "content:test"},
|
||||
params={"query": "content:test", "pageSize": "20", "pageNum": "0"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_with_page_num(self, mock_get, client):
|
||||
"""Test find operation with specific page number."""
|
||||
mock_response_data = {"results": [], "size": 100, "pageNum": 2, "pageSize": 20}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.ok = True
|
||||
|
||||
result = await client.find("type:Schema", page_num=2)
|
||||
|
||||
assert result["page_num"] == 2
|
||||
assert result["page_size"] == 20
|
||||
assert result["total_size"] == 100
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:Schema", "pageSize": "20", "pageNum": "2"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@patch("cordra_mcp.client.requests.Session.get")
|
||||
async def test_find_with_custom_page_size_and_num(self, mock_get, client):
|
||||
"""Test find operation with custom page size and page number."""
|
||||
mock_response_data = {"results": [], "size": 500, "pageNum": 5, "pageSize": 10}
|
||||
mock_response = mock_get.return_value
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_response_data
|
||||
mock_response.ok = True
|
||||
|
||||
result = await client.find("type:Document", page_size=10, page_num=5)
|
||||
|
||||
assert result["page_num"] == 5
|
||||
assert result["page_size"] == 10
|
||||
assert result["total_size"] == 500
|
||||
mock_get.assert_called_once_with(
|
||||
"https://test.example.com/search",
|
||||
params={"query": "type:Document", "pageSize": "10", "pageNum": "5"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
@@ -372,6 +413,5 @@ class TestCordraConfig:
|
||||
assert config.base_url == "https://localhost:8443"
|
||||
assert config.username is None
|
||||
assert config.password is None
|
||||
assert config.max_search_results == 1000
|
||||
assert config.verify_ssl is True
|
||||
assert config.timeout == 30
|
||||
|
||||
@@ -190,12 +190,17 @@ class TestSchemaResourceFunctions:
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
async def test_register_schema_resources_success(self, mock_client):
|
||||
"""Test successful schema resource registration."""
|
||||
mock_schemas = [
|
||||
{"content": {"name": "User"}, "id": "test/user-schema"},
|
||||
{"content": {"name": "Project"}, "id": "test/project-schema"},
|
||||
{"content": {"name": "Document"}, "id": "test/doc-schema"}
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_schemas)
|
||||
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"}
|
||||
],
|
||||
"total_size": 3,
|
||||
"page_num": 0,
|
||||
"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:
|
||||
@@ -203,7 +208,7 @@ class TestSchemaResourceFunctions:
|
||||
await register_schema_resources()
|
||||
|
||||
# Verify the client was called with correct query
|
||||
mock_client.find.assert_called_once_with("type:Schema")
|
||||
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
|
||||
@@ -211,12 +216,17 @@ class TestSchemaResourceFunctions:
|
||||
@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_schemas = [
|
||||
{"content": {"name": "User"}, "id": "test/user-schema"},
|
||||
{"content": {}, "id": "test/no-name-schema"}, # Missing name field
|
||||
{"content": {"name": "Project"}, "id": "test/project-schema"}
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_schemas)
|
||||
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"}
|
||||
],
|
||||
"total_size": 3,
|
||||
"page_num": 0,
|
||||
"page_size": 20
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
with patch('cordra_mcp.server.mcp') as mock_mcp:
|
||||
from cordra_mcp.server import register_schema_resources
|
||||
@@ -234,22 +244,63 @@ class TestSchemaResourceFunctions:
|
||||
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")
|
||||
mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0)
|
||||
|
||||
@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 = {
|
||||
"results": first_page_schemas,
|
||||
"total_size": 25,
|
||||
"page_num": 0,
|
||||
"page_size": 20
|
||||
}
|
||||
|
||||
# Second page with fewer results (indicating last page)
|
||||
second_page = {
|
||||
"results": [
|
||||
{"content": {"name": "Document"}, "id": "test/doc-schema"},
|
||||
],
|
||||
"total_size": 25,
|
||||
"page_num": 1,
|
||||
"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:
|
||||
from cordra_mcp.server import register_schema_resources
|
||||
await register_schema_resources()
|
||||
|
||||
# Verify pagination calls
|
||||
assert mock_client.find.call_count == 2
|
||||
mock_client.find.assert_any_call("type:Schema", page_size=20, page_num=0)
|
||||
mock_client.find.assert_any_call("type:Schema", page_size=20, page_num=1)
|
||||
|
||||
# Verify all 21 schemas were registered (20 from first page + 1 from second page)
|
||||
assert mock_mcp.add_resource.call_count == 21
|
||||
|
||||
|
||||
class TestSearchObjects:
|
||||
"""Test the search_objects tool."""
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_success(self, mock_config, mock_client):
|
||||
async def test_search_objects_success(self, mock_client):
|
||||
"""Test successful object search."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_results = [
|
||||
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
|
||||
{"id": "people/jane-smith", "type": "Person", "content": {"name": "Jane Smith"}},
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_results)
|
||||
mock_search_result = {
|
||||
"results": [
|
||||
{"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
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
result = await search_objects("name:John")
|
||||
|
||||
@@ -260,17 +311,20 @@ class TestSearchObjects:
|
||||
assert parsed_result[1]["id"] == "people/jane-smith"
|
||||
|
||||
# Verify the client was called with correct parameters
|
||||
mock_client.find.assert_called_once_with("name:John", object_type=None, limit=1000)
|
||||
mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=1, page_num=0)
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_with_type_filter(self, mock_config, mock_client):
|
||||
async def test_search_objects_with_type_filter(self, mock_client):
|
||||
"""Test object search with type filter."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_results = [
|
||||
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_results)
|
||||
mock_search_result = {
|
||||
"results": [
|
||||
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
|
||||
],
|
||||
"total_size": 1,
|
||||
"page_num": 0,
|
||||
"page_size": 1000
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
result = await search_objects("name:John", type="Person")
|
||||
|
||||
@@ -280,17 +334,20 @@ class TestSearchObjects:
|
||||
assert parsed_result[0]["type"] == "Person"
|
||||
|
||||
# Verify the client was called with type filter
|
||||
mock_client.find.assert_called_once_with("name:John", object_type="Person", limit=1000)
|
||||
mock_client.find.assert_called_once_with("name:John", object_type="Person", page_size=1, page_num=0)
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_with_limit(self, mock_config, mock_client):
|
||||
async def test_search_objects_with_limit(self, mock_client):
|
||||
"""Test object search with custom limit."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_results = [
|
||||
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_results)
|
||||
mock_search_result = {
|
||||
"results": [
|
||||
{"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}},
|
||||
],
|
||||
"total_size": 1,
|
||||
"page_num": 0,
|
||||
"page_size": 50
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
result = await search_objects("name:John", limit=50)
|
||||
|
||||
@@ -299,17 +356,20 @@ class TestSearchObjects:
|
||||
assert len(parsed_result) == 1
|
||||
|
||||
# Verify the client was called with custom limit
|
||||
mock_client.find.assert_called_once_with("name:John", object_type=None, limit=50)
|
||||
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.config')
|
||||
async def test_search_objects_with_all_parameters(self, mock_config, mock_client):
|
||||
async def test_search_objects_with_all_parameters(self, mock_client):
|
||||
"""Test object search with all parameters."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_results = [
|
||||
{"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}},
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_results)
|
||||
mock_search_result = {
|
||||
"results": [
|
||||
{"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}},
|
||||
],
|
||||
"total_size": 1,
|
||||
"page_num": 0,
|
||||
"page_size": 25
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
result = await search_objects("title:Report", type="Document", limit=25)
|
||||
|
||||
@@ -319,14 +379,18 @@ class TestSearchObjects:
|
||||
assert parsed_result[0]["type"] == "Document"
|
||||
|
||||
# Verify the client was called with all parameters
|
||||
mock_client.find.assert_called_once_with("title:Report", object_type="Document", limit=25)
|
||||
mock_client.find.assert_called_once_with("title:Report", object_type="Document", page_size=25, page_num=0)
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_empty_results(self, mock_config, mock_client):
|
||||
async def test_search_objects_empty_results(self, mock_client):
|
||||
"""Test object search with no results."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_client.find = AsyncMock(return_value=[])
|
||||
mock_search_result = {
|
||||
"results": [],
|
||||
"total_size": 0,
|
||||
"page_num": 0,
|
||||
"page_size": 1000
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
result = await search_objects("nonexistent:data")
|
||||
|
||||
@@ -334,43 +398,42 @@ class TestSearchObjects:
|
||||
parsed_result = json.loads(result)
|
||||
assert parsed_result == []
|
||||
|
||||
mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, limit=1000)
|
||||
mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, page_size=1, page_num=0)
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_client_error(self, mock_config, mock_client):
|
||||
async def test_search_objects_client_error(self, mock_client):
|
||||
"""Test object search with client error."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed"))
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await search_objects("test:query")
|
||||
|
||||
assert "Search failed:" in str(exc_info.value)
|
||||
mock_client.find.assert_called_once_with("test:query", object_type=None, limit=1000)
|
||||
mock_client.find.assert_called_once_with("test:query", object_type=None, page_size=1, page_num=0)
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_value_error(self, mock_config, mock_client):
|
||||
async def test_search_objects_value_error(self, mock_client):
|
||||
"""Test object search with value error."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_client.find = AsyncMock(side_effect=ValueError("Invalid query"))
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await search_objects("invalid:query")
|
||||
|
||||
assert "Invalid search parameters:" in str(exc_info.value)
|
||||
mock_client.find.assert_called_once_with("invalid:query", object_type=None, limit=1000)
|
||||
mock_client.find.assert_called_once_with("invalid:query", object_type=None, page_size=1, page_num=0)
|
||||
|
||||
@patch('cordra_mcp.server.cordra_client')
|
||||
@patch('cordra_mcp.server.config')
|
||||
async def test_search_objects_json_formatting(self, mock_config, mock_client):
|
||||
async def test_search_objects_json_formatting(self, mock_client):
|
||||
"""Test that search results are properly formatted as JSON."""
|
||||
mock_config.max_search_results = 1000
|
||||
mock_results = [
|
||||
{"id": "test/object", "type": "Test", "content": {"data": "value"}},
|
||||
]
|
||||
mock_client.find = AsyncMock(return_value=mock_results)
|
||||
mock_search_result = {
|
||||
"results": [
|
||||
{"id": "test/object", "type": "Test", "content": {"data": "value"}},
|
||||
],
|
||||
"total_size": 1,
|
||||
"page_num": 0,
|
||||
"page_size": 1000
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
result = await search_objects("test:query")
|
||||
|
||||
@@ -386,6 +449,42 @@ class TestSearchObjects:
|
||||
assert parsed_result[0]["type"] == "Test"
|
||||
assert parsed_result[0]["content"]["data"] == "value"
|
||||
|
||||
@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"}},
|
||||
],
|
||||
"total_size": 50,
|
||||
"page_num": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
await search_objects("type:Document", page_num=1)
|
||||
|
||||
# Verify the client was called with correct page number
|
||||
mock_client.find.assert_called_once_with("type:Document", object_type=None, page_size=1, page_num=1)
|
||||
|
||||
@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 = {
|
||||
"results": [
|
||||
{"id": "reports/report-51", "type": "Report", "content": {"title": "Report 51"}},
|
||||
],
|
||||
"total_size": 100,
|
||||
"page_num": 5,
|
||||
"page_size": 10
|
||||
}
|
||||
mock_client.find = AsyncMock(return_value=mock_search_result)
|
||||
|
||||
await search_objects("type:Report", type="Report", limit=10, page_num=5)
|
||||
|
||||
# Verify the client was called with all parameters
|
||||
mock_client.find.assert_called_once_with("type:Report", object_type="Report", page_size=10, page_num=5)
|
||||
|
||||
|
||||
class TestGetCordraDesign:
|
||||
"""Test the get_cordra_design resource handler."""
|
||||
|
||||
Reference in New Issue
Block a user