ci: [skip ci] Move tests to subdirs
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Integration tests for Contacts MCP tools."""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from mcp import ClientSession
|
||||
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
async def test_mcp_contacts_workflow(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test complete Contacts workflow via MCP tools with verification via NextcloudClient."""
|
||||
|
||||
addressbook_name = f"mcp-test-addressbook-{uuid.uuid4().hex[:8]}"
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
contact_uid = f"mcp-contact-{unique_suffix}"
|
||||
contact_data = {
|
||||
"fn": f"MCP Contact {unique_suffix}",
|
||||
"email": f"mcp.contact.{unique_suffix}@example.com",
|
||||
"tel": "1234567890",
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Create address book via MCP
|
||||
logger.info(f"Creating address book via MCP: {addressbook_name}")
|
||||
create_ab_result = await nc_mcp_client.call_tool(
|
||||
"nc_contacts_create_addressbook",
|
||||
{"name": addressbook_name, "display_name": f"MCP Test {addressbook_name}"},
|
||||
)
|
||||
assert create_ab_result.isError is False
|
||||
|
||||
# 2. Verify address book creation
|
||||
addressbooks = await nc_client.contacts.list_addressbooks()
|
||||
assert any(ab["name"] == addressbook_name for ab in addressbooks)
|
||||
|
||||
# 3. Create contact via MCP
|
||||
logger.info(f"Creating contact in {addressbook_name} via MCP")
|
||||
create_c_result = await nc_mcp_client.call_tool(
|
||||
"nc_contacts_create_contact",
|
||||
{
|
||||
"addressbook": addressbook_name,
|
||||
"uid": contact_uid,
|
||||
"contact_data": contact_data,
|
||||
},
|
||||
)
|
||||
assert create_c_result.isError is False
|
||||
|
||||
# 4. Verify contact creation
|
||||
contacts = await nc_client.contacts.list_contacts(addressbook=addressbook_name)
|
||||
assert any(c["vcard_id"] == contact_uid for c in contacts)
|
||||
|
||||
# 5. Delete contact via MCP
|
||||
logger.info(f"Deleting contact {contact_uid} via MCP")
|
||||
delete_c_result = await nc_mcp_client.call_tool(
|
||||
"nc_contacts_delete_contact",
|
||||
{"addressbook": addressbook_name, "uid": contact_uid},
|
||||
)
|
||||
assert delete_c_result.isError is False
|
||||
|
||||
# 6. Verify contact deletion
|
||||
contacts = await nc_client.contacts.list_contacts(addressbook=addressbook_name)
|
||||
assert not any(c["vcard_id"] == contact_uid for c in contacts)
|
||||
|
||||
# 7. Delete address book via MCP
|
||||
logger.info(f"Deleting address book {addressbook_name} via MCP")
|
||||
delete_ab_result = await nc_mcp_client.call_tool(
|
||||
"nc_contacts_delete_addressbook", {"name": addressbook_name}
|
||||
)
|
||||
assert delete_ab_result.isError is False
|
||||
|
||||
# 8. Verify address book deletion
|
||||
addressbooks = await nc_client.contacts.list_addressbooks()
|
||||
assert not any(ab["name"] == addressbook_name for ab in addressbooks)
|
||||
|
||||
finally:
|
||||
# Cleanup in case of failure
|
||||
try:
|
||||
await nc_client.contacts.delete_addressbook(name=addressbook_name)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,223 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from mcp import ClientSession
|
||||
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
async def test_deck_mcp_connectivity(nc_mcp_client: ClientSession):
|
||||
"""Test deck MCP tools are available and functional."""
|
||||
|
||||
# List available tools
|
||||
tools = await nc_mcp_client.list_tools()
|
||||
tool_names = [tool.name for tool in tools.tools]
|
||||
|
||||
# Verify expected deck tools are present
|
||||
expected_deck_tools = ["deck_create_board"]
|
||||
|
||||
for expected_tool in expected_deck_tools:
|
||||
assert expected_tool in tool_names, (
|
||||
f"Expected deck tool '{expected_tool}' not found in available tools"
|
||||
)
|
||||
logger.info(f"Found expected deck tool: {expected_tool}")
|
||||
|
||||
# List available resource templates
|
||||
templates = await nc_mcp_client.list_resource_templates()
|
||||
template_uris = [template.uriTemplate for template in templates.resourceTemplates]
|
||||
|
||||
# Verify expected deck resource templates
|
||||
expected_deck_templates = [
|
||||
"nc://Deck/boards/{board_id}",
|
||||
]
|
||||
|
||||
for expected_template in expected_deck_templates:
|
||||
assert expected_template in template_uris, (
|
||||
f"Expected deck template '{expected_template}' not found"
|
||||
)
|
||||
logger.info(f"Found expected deck resource template: {expected_template}")
|
||||
|
||||
# List available resources
|
||||
resources = await nc_mcp_client.list_resources()
|
||||
resource_uris = [str(resource.uri) for resource in resources.resources]
|
||||
|
||||
# Verify expected deck resources
|
||||
expected_deck_resources = [
|
||||
"nc://Deck/boards",
|
||||
]
|
||||
|
||||
for expected_resource in expected_deck_resources:
|
||||
assert expected_resource in resource_uris, (
|
||||
f"Expected deck resource '{expected_resource}' not found"
|
||||
)
|
||||
logger.info(f"Found expected deck resource: {expected_resource}")
|
||||
|
||||
|
||||
async def test_deck_board_crud_workflow_mcp(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test complete Deck board CRUD workflow via MCP tools with verification via NextcloudClient."""
|
||||
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
board_title = f"MCP Test Board {unique_suffix}"
|
||||
board_color = "0000FF" # Blue
|
||||
|
||||
# 1. Create board via MCP
|
||||
logger.info(f"Creating board via MCP: {board_title}")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"deck_create_board",
|
||||
{"title": board_title, "color": board_color},
|
||||
)
|
||||
|
||||
assert create_result.isError is False, (
|
||||
f"MCP board creation failed: {create_result.content}"
|
||||
)
|
||||
created_board_json = create_result.content[0].text
|
||||
created_board_response = json.loads(created_board_json)
|
||||
board_id = created_board_response["id"]
|
||||
|
||||
logger.info(f"Board created via MCP with ID: {board_id}")
|
||||
assert created_board_response["title"] == board_title
|
||||
assert created_board_response["color"] == board_color
|
||||
|
||||
# 2. Verify creation via direct NextcloudClient
|
||||
direct_board = await nc_client.deck.get_board(board_id)
|
||||
assert direct_board.title == board_title, (
|
||||
f"Title mismatch: {direct_board.title} != {board_title}"
|
||||
)
|
||||
assert direct_board.color == board_color, "Color mismatch"
|
||||
logger.info("Board creation verified via direct client")
|
||||
|
||||
# 3. Read board via MCP resource
|
||||
logger.info(f"Reading board via MCP resource: {board_id}")
|
||||
read_result = await nc_mcp_client.read_resource(f"nc://Deck/boards/{board_id}")
|
||||
assert len(read_result.contents) == 1, "Expected exactly one content item"
|
||||
read_board_data = json.loads(read_result.contents[0].text)
|
||||
|
||||
assert read_board_data["title"] == board_title
|
||||
assert read_board_data["color"] == board_color
|
||||
logger.info("Board read via MCP resource successfully")
|
||||
|
||||
# 4. Verify board via direct read of resource
|
||||
logger.info(f"Verifying board via resource read: {board_id}")
|
||||
# This was already done in step 3, so we'll just log confirmation
|
||||
logger.info("Board structure verified successfully")
|
||||
|
||||
# 5. Read boards list via MCP resource
|
||||
logger.info("Reading boards list via MCP resource")
|
||||
boards_resource_result = await nc_mcp_client.read_resource("nc://Deck/boards")
|
||||
assert len(boards_resource_result.contents) == 1, (
|
||||
"Expected exactly one content item"
|
||||
)
|
||||
boards_resource_data = json.loads(boards_resource_result.contents[0].text)
|
||||
assert isinstance(boards_resource_data, list) # Resources return raw lists
|
||||
|
||||
# Verify our board is in the resource list
|
||||
resource_board_ids = [board["id"] for board in boards_resource_data]
|
||||
assert board_id in resource_board_ids, "Created board not found in resource list"
|
||||
logger.info("Board found in boards resource list")
|
||||
|
||||
# Clean up - delete board
|
||||
await nc_client.deck.delete_board(board_id)
|
||||
logger.info(f"Cleaned up board ID: {board_id}")
|
||||
|
||||
|
||||
async def test_deck_board_operations_error_handling_mcp(nc_mcp_client: ClientSession):
|
||||
"""Test MCP deck tools handle errors appropriately."""
|
||||
|
||||
non_existent_id = 999999999
|
||||
|
||||
# Test create board with invalid parameters via MCP tool
|
||||
logger.info("Testing board creation with invalid parameters via MCP")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"deck_create_board",
|
||||
{"title": "", "color": "FF0000"},
|
||||
)
|
||||
|
||||
assert create_result.isError is True, "Expected error for invalid board creation"
|
||||
logger.info("Invalid board creation correctly failed via MCP tool")
|
||||
|
||||
# Test read non-existent board via MCP resource
|
||||
logger.info(f"Testing read non-existent board via MCP resource: {non_existent_id}")
|
||||
try:
|
||||
read_result = await nc_mcp_client.read_resource(
|
||||
f"nc://Deck/boards/{non_existent_id}"
|
||||
)
|
||||
# If no error is thrown, check if the result indicates an error
|
||||
assert len(read_result.contents) == 0, (
|
||||
"Expected empty content for non-existent board"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(f"Read non-existent board correctly failed via MCP resource: {e}")
|
||||
|
||||
|
||||
async def test_deck_board_creation_validation_mcp(nc_mcp_client: ClientSession):
|
||||
"""Test deck board creation validation via MCP tools."""
|
||||
|
||||
# Test creating board with empty title should fail
|
||||
logger.info("Testing board creation with empty title via MCP")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"deck_create_board",
|
||||
{"title": "", "color": "FF0000"},
|
||||
)
|
||||
|
||||
assert create_result.isError is True, "Expected error for empty board title"
|
||||
logger.info("Empty title board creation correctly failed via MCP")
|
||||
|
||||
|
||||
async def test_deck_board_creation_success_mcp(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test deck board creation with valid parameters via MCP tools."""
|
||||
|
||||
# Test creating board with valid parameters
|
||||
logger.info("Testing board creation with valid parameters via MCP")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"deck_create_board",
|
||||
{"title": f"Valid Board {uuid.uuid4().hex[:8]}", "color": "00FF00"},
|
||||
)
|
||||
|
||||
assert create_result.isError is False, "Valid board creation should succeed"
|
||||
created_board = json.loads(create_result.content[0].text)
|
||||
board_id = created_board["id"]
|
||||
logger.info(f"Valid board created successfully with ID: {board_id}")
|
||||
|
||||
# Clean up - delete board
|
||||
await nc_client.deck.delete_board(board_id)
|
||||
logger.info(f"Cleaned up board ID: {board_id}")
|
||||
|
||||
|
||||
async def test_deck_workflow_integration_mcp(
|
||||
nc_mcp_client: ClientSession, temporary_board_with_card: tuple
|
||||
):
|
||||
"""Test a complete deck workflow using MCP tools with temporary resources."""
|
||||
|
||||
board_data, stack_data, card_data = temporary_board_with_card
|
||||
board_id = board_data["id"]
|
||||
board_title = board_data["title"]
|
||||
|
||||
# 1. Read board via MCP to verify the structure
|
||||
logger.info(f"Reading board via MCP resource: {board_id}")
|
||||
read_result = await nc_mcp_client.read_resource(f"nc://Deck/boards/{board_id}")
|
||||
board_mcp_data = json.loads(read_result.contents[0].text)
|
||||
|
||||
assert board_mcp_data["title"] == board_title
|
||||
logger.info("Board structure verified via MCP resource")
|
||||
|
||||
# 2. List boards via MCP resource and verify our board is there
|
||||
logger.info("Listing boards via MCP resource")
|
||||
list_result = await nc_mcp_client.read_resource("nc://Deck/boards")
|
||||
boards_data = json.loads(list_result.contents[0].text)
|
||||
|
||||
board_found = any(board["id"] == board_id for board in boards_data)
|
||||
assert board_found, "Board not found in boards list"
|
||||
logger.info("Board found in boards list")
|
||||
|
||||
# 3. Verify board data matches via resource (already done in step 1)
|
||||
logger.info(f"Board data verification completed for board: {board_id}")
|
||||
logger.info("Board structure and data verified successfully")
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Test error propagation in the MCP server for various error scenarios."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from mcp import ClientSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Mark all tests in this module as integration tests
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
async def test_missing_note_tool_error(nc_mcp_client: ClientSession):
|
||||
"""Test that accessing a non-existent note via tool returns proper error."""
|
||||
# Try to get a non-existent note via tool - should return error response
|
||||
response = await nc_mcp_client.call_tool("nc_notes_get_note", {"note_id": 999999})
|
||||
|
||||
# Should return error response (not raise exception) for tools
|
||||
assert response is not None
|
||||
assert response.isError is True
|
||||
assert "Note 999999 not found" in response.content[0].text
|
||||
|
||||
|
||||
async def test_delete_missing_note_tool_error(nc_mcp_client: ClientSession):
|
||||
"""Test that deleting a non-existent note returns proper error."""
|
||||
# Try to delete a non-existent note - should return error response
|
||||
response = await nc_mcp_client.call_tool(
|
||||
"nc_notes_delete_note", {"note_id": 999999}
|
||||
)
|
||||
|
||||
# Should return error response (not raise exception) for tools
|
||||
assert response is not None
|
||||
assert response.isError is True
|
||||
assert "Note 999999 not found" in response.content[0].text
|
||||
|
||||
|
||||
async def test_search_with_empty_query(nc_mcp_client: ClientSession):
|
||||
"""Test search behavior with empty query."""
|
||||
# Search with empty query
|
||||
response = await nc_mcp_client.call_tool("nc_notes_search_notes", {"query": ""})
|
||||
|
||||
logger.info(f"Empty search query response: {response}")
|
||||
|
||||
# Should return successful response with empty or valid results
|
||||
assert response is not None
|
||||
assert response.isError is False
|
||||
|
||||
|
||||
async def test_tool_missing_required_parameters(nc_mcp_client: ClientSession):
|
||||
"""Test calling a tool with missing required parameters."""
|
||||
# Try to create note with missing parameters
|
||||
response = await nc_mcp_client.call_tool(
|
||||
"nc_notes_create_note",
|
||||
{"title": "Test"}, # Missing content and category
|
||||
)
|
||||
logger.info(f"Missing params response: {response}")
|
||||
|
||||
# Should return error response for missing required parameters
|
||||
assert response is not None
|
||||
assert response.isError is True
|
||||
assert (
|
||||
"required" in response.content[0].text.lower()
|
||||
or "missing" in response.content[0].text.lower()
|
||||
)
|
||||
|
||||
|
||||
async def test_update_note_with_invalid_etag(nc_mcp_client: ClientSession, nc_client):
|
||||
"""Test updating a note with invalid ETag."""
|
||||
# First create a note
|
||||
note_data = await nc_client.notes.create_note(
|
||||
title="Test Note for ETag", content="Test content", category=""
|
||||
)
|
||||
note_id = note_data["id"]
|
||||
|
||||
try:
|
||||
# Try to update with invalid ETag - should return error response
|
||||
response = await nc_mcp_client.call_tool(
|
||||
"nc_notes_update_note",
|
||||
{
|
||||
"note_id": note_id,
|
||||
"etag": "invalid-etag",
|
||||
"title": "Updated Title",
|
||||
"content": None,
|
||||
"category": None,
|
||||
},
|
||||
)
|
||||
|
||||
# Should return error response (not raise exception) for tools
|
||||
assert response is not None
|
||||
assert response.isError is True
|
||||
assert "modified by someone else" in response.content[0].text
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
await nc_client.notes.delete_note(note_id)
|
||||
|
||||
|
||||
async def test_calendar_missing_calendar_error(nc_mcp_client: ClientSession):
|
||||
"""Test calendar operations with non-existent calendar."""
|
||||
# Try to create event in non-existent calendar
|
||||
response = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_create_event",
|
||||
{
|
||||
"calendar_name": "non-existent-calendar",
|
||||
"title": "Test Event",
|
||||
"start_datetime": "2025-01-15T14:00:00",
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(f"Non-existent calendar response: {response}")
|
||||
|
||||
# Should return structured error response
|
||||
assert response is not None
|
||||
# Note: Some modules may not have improved error handling yet
|
||||
# Check if we have structured content with success=false or isError=true
|
||||
if (
|
||||
hasattr(response, "structuredContent")
|
||||
and response.structuredContent
|
||||
and "result" in response.structuredContent
|
||||
):
|
||||
assert response.structuredContent["result"]["success"] is False
|
||||
else:
|
||||
assert response.isError is True
|
||||
|
||||
|
||||
async def test_webdav_read_missing_file_error(nc_mcp_client: ClientSession):
|
||||
"""Test WebDAV operations with non-existent file."""
|
||||
# Try to read a non-existent file
|
||||
response = await nc_mcp_client.call_tool(
|
||||
"nc_webdav_read_file", {"path": "non-existent-file.txt"}
|
||||
)
|
||||
|
||||
logger.info(f"Missing file response: {response}")
|
||||
|
||||
# Should return structured error response
|
||||
assert response is not None
|
||||
# Note: Some modules may not have improved error handling yet
|
||||
# Check if we have structured content with success=false or isError=true
|
||||
if (
|
||||
hasattr(response, "structuredContent")
|
||||
and response.structuredContent
|
||||
and "result" in response.structuredContent
|
||||
):
|
||||
assert response.structuredContent["result"]["success"] is False
|
||||
else:
|
||||
assert response.isError is True
|
||||
|
||||
|
||||
async def test_tables_missing_table_error(nc_mcp_client: ClientSession):
|
||||
"""Test Tables operations with non-existent table."""
|
||||
# Try to get schema of non-existent table
|
||||
response = await nc_mcp_client.call_tool(
|
||||
"nc_tables_get_schema", {"table_id": 999999}
|
||||
)
|
||||
|
||||
logger.info(f"Missing table response: {response}")
|
||||
|
||||
# Should return structured error response
|
||||
assert response is not None
|
||||
# Note: Some modules may not have improved error handling yet
|
||||
# Check if we have structured content with success=false or isError=true
|
||||
if (
|
||||
hasattr(response, "structuredContent")
|
||||
and response.structuredContent
|
||||
and "result" in response.structuredContent
|
||||
):
|
||||
assert response.structuredContent["result"]["success"] is False
|
||||
else:
|
||||
assert response.isError is True
|
||||
@@ -0,0 +1,795 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from mcp import ClientSession
|
||||
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
async def test_mcp_connectivity(nc_mcp_client: ClientSession):
|
||||
"""Test basic MCP server connectivity and list available tools/resources."""
|
||||
|
||||
# List available tools
|
||||
tools = await nc_mcp_client.list_tools()
|
||||
logger.info("Available MCP tools:")
|
||||
tool_names = []
|
||||
for tool in tools.tools:
|
||||
logger.info(f" - {tool.name}: {tool.description}")
|
||||
tool_names.append(tool.name)
|
||||
|
||||
# Verify expected tools are present
|
||||
expected_tools = [
|
||||
"nc_notes_create_note",
|
||||
"nc_notes_update_note",
|
||||
"nc_notes_append_content",
|
||||
"nc_notes_search_notes",
|
||||
"nc_notes_delete_note",
|
||||
"nc_tables_list_tables",
|
||||
"nc_tables_get_schema",
|
||||
"nc_tables_read_table",
|
||||
"nc_tables_insert_row",
|
||||
"nc_tables_update_row",
|
||||
"nc_tables_delete_row",
|
||||
"nc_webdav_list_directory",
|
||||
"nc_webdav_read_file",
|
||||
"nc_webdav_write_file",
|
||||
"nc_webdav_create_directory",
|
||||
"nc_webdav_delete_resource",
|
||||
"nc_calendar_list_calendars",
|
||||
"nc_calendar_create_event",
|
||||
"nc_calendar_list_events",
|
||||
"nc_calendar_get_event",
|
||||
"nc_calendar_update_event",
|
||||
"nc_calendar_delete_event",
|
||||
"nc_calendar_create_meeting",
|
||||
"nc_calendar_get_upcoming_events",
|
||||
"nc_calendar_find_availability",
|
||||
"nc_calendar_bulk_operations",
|
||||
"nc_calendar_manage_calendar",
|
||||
"deck_create_board",
|
||||
]
|
||||
|
||||
for expected_tool in expected_tools:
|
||||
assert expected_tool in tool_names, (
|
||||
f"Expected tool '{expected_tool}' not found in available tools"
|
||||
)
|
||||
|
||||
# List available resource templates
|
||||
templates = await nc_mcp_client.list_resource_templates()
|
||||
logger.info("\nAvailable resource templates:")
|
||||
template_uris = []
|
||||
for template in templates.resourceTemplates:
|
||||
logger.info(f" - {template.uriTemplate}")
|
||||
template_uris.append(template.uriTemplate)
|
||||
|
||||
# Verify expected resource templates
|
||||
# Note: Notes attachments are now handled via tools, not resource templates
|
||||
expected_templates = []
|
||||
|
||||
for expected_template in expected_templates:
|
||||
assert expected_template in template_uris, (
|
||||
f"Expected template '{expected_template}' not found"
|
||||
)
|
||||
|
||||
# List available resources
|
||||
resources = await nc_mcp_client.list_resources()
|
||||
logger.info("\nAvailable resources:")
|
||||
resource_uris = []
|
||||
for resource in resources.resources:
|
||||
logger.info(f" - {resource.uri}: {resource.name}")
|
||||
resource_uris.append(str(resource.uri)) # Convert to string for comparison
|
||||
|
||||
# Verify expected resources
|
||||
expected_resources = ["nc://capabilities", "notes://settings", "nc://Deck/boards"]
|
||||
|
||||
for expected_resource in expected_resources:
|
||||
assert expected_resource in resource_uris, (
|
||||
f"Expected resource '{expected_resource}' not found"
|
||||
)
|
||||
|
||||
# List available prompts
|
||||
prompts = await nc_mcp_client.list_prompts()
|
||||
logger.info("\nAvailable prompts:")
|
||||
for prompt in prompts.prompts:
|
||||
logger.info(f" - {prompt.name}")
|
||||
|
||||
|
||||
async def test_mcp_notes_crud_workflow(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test complete Notes CRUD workflow via MCP tools with verification via NextcloudClient."""
|
||||
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
test_title = f"MCP Test Note {unique_suffix}"
|
||||
test_content = f"This is test content for note {unique_suffix}"
|
||||
test_category = "MCPTesting"
|
||||
|
||||
created_note = None
|
||||
|
||||
try:
|
||||
# 1. Create note via MCP
|
||||
logger.info(f"Creating note via MCP: {test_title}")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_create_note",
|
||||
{"title": test_title, "content": test_content, "category": test_category},
|
||||
)
|
||||
|
||||
assert create_result.isError is False, (
|
||||
f"MCP note creation failed: {create_result.content}"
|
||||
)
|
||||
created_note = create_result.content[0].text
|
||||
note_data = json.loads(created_note) # Parse the returned JSON
|
||||
note_id = note_data["id"]
|
||||
create_etag = note_data["etag"] # Verify create response includes ETag
|
||||
|
||||
logger.info(f"Note created via MCP with ID: {note_id}, ETag: {create_etag}")
|
||||
assert "etag" in note_data, "Create response should include ETag"
|
||||
assert create_etag, "Create ETag should not be empty"
|
||||
|
||||
# 2. Verify creation via direct NextcloudClient
|
||||
direct_note = await nc_client.notes.get_note(note_id)
|
||||
assert direct_note["title"] == test_title, (
|
||||
f"Title mismatch: {direct_note['title']} != {test_title}"
|
||||
)
|
||||
assert direct_note["content"] == test_content, "Content mismatch"
|
||||
assert direct_note["category"] == test_category, "Category mismatch"
|
||||
|
||||
# 3. Read note via MCP
|
||||
logger.info(f"Reading note via MCP: {note_id}")
|
||||
read_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_get_note", {"note_id": note_id}
|
||||
)
|
||||
read_note_data = read_result.content[0].text
|
||||
read_note_data = json.loads(read_note_data)
|
||||
|
||||
assert read_note_data["title"] == test_title
|
||||
assert read_note_data["content"] == test_content
|
||||
assert read_note_data["category"] == test_category
|
||||
|
||||
# 4. Update note via MCP
|
||||
updated_title = f"Updated {test_title}"
|
||||
updated_content = f"Updated content: {test_content}"
|
||||
etag = read_note_data["etag"]
|
||||
|
||||
logger.info(f"Updating note via MCP: {note_id}")
|
||||
update_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_update_note",
|
||||
{
|
||||
"note_id": note_id,
|
||||
"etag": etag,
|
||||
"title": updated_title,
|
||||
"content": updated_content,
|
||||
"category": test_category,
|
||||
},
|
||||
)
|
||||
|
||||
assert update_result.isError is False, (
|
||||
f"MCP note update failed: {update_result.content}"
|
||||
)
|
||||
|
||||
# Verify update response includes new ETag
|
||||
updated_note_data = json.loads(update_result.content[0].text)
|
||||
update_etag = updated_note_data["etag"]
|
||||
|
||||
logger.info(f"Note updated via MCP, new ETag: {update_etag}")
|
||||
assert "etag" in updated_note_data, "Update response should include ETag"
|
||||
assert update_etag, "Update ETag should not be empty"
|
||||
assert update_etag != etag, "ETag should change after update"
|
||||
|
||||
# 5. Verify update via direct NextcloudClient
|
||||
updated_direct_note = await nc_client.notes.get_note(note_id)
|
||||
assert updated_direct_note["title"] == updated_title
|
||||
assert updated_direct_note["content"] == updated_content
|
||||
|
||||
# 6. Append content via MCP
|
||||
append_content = "\n\nThis is appended content via MCP."
|
||||
logger.info(f"Appending content to note via MCP: {note_id}")
|
||||
append_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_append_content", {"note_id": note_id, "content": append_content}
|
||||
)
|
||||
|
||||
assert append_result.isError is False, (
|
||||
f"MCP note append failed: {append_result.content}"
|
||||
)
|
||||
|
||||
# Verify append response includes new ETag
|
||||
appended_note_data = json.loads(append_result.content[0].text)
|
||||
append_etag = appended_note_data["etag"]
|
||||
|
||||
logger.info(f"Content appended via MCP, new ETag: {append_etag}")
|
||||
assert "etag" in appended_note_data, "Append response should include ETag"
|
||||
assert append_etag, "Append ETag should not be empty"
|
||||
assert append_etag != update_etag, "ETag should change after append"
|
||||
|
||||
# 7. Verify append via direct NextcloudClient
|
||||
appended_direct_note = await nc_client.notes.get_note(note_id)
|
||||
assert append_content in appended_direct_note["content"]
|
||||
|
||||
# 8. Search for note via MCP
|
||||
logger.info(f"Searching for note via MCP with query: {unique_suffix}")
|
||||
search_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_search_notes", {"query": unique_suffix}
|
||||
)
|
||||
|
||||
assert search_result.isError is False, (
|
||||
f"MCP note search failed: {search_result.content}"
|
||||
)
|
||||
search_notes_text = search_result.content[0].text
|
||||
logger.info(f"Search result text: {search_notes_text}")
|
||||
search_response = json.loads(search_notes_text)
|
||||
|
||||
# Expect structured response with Pydantic format
|
||||
assert isinstance(search_response, dict), (
|
||||
f"Expected search response to be a dict with structured format, got: {type(search_response)}"
|
||||
)
|
||||
assert "results" in search_response, (
|
||||
f"Expected 'results' field in search response, got keys: {list(search_response.keys())}"
|
||||
)
|
||||
assert "success" in search_response and search_response["success"], (
|
||||
f"Expected successful search response, got: {search_response}"
|
||||
)
|
||||
|
||||
search_notes = search_response["results"]
|
||||
assert isinstance(search_notes, list), (
|
||||
f"Expected results to be a list, got: {type(search_notes)}"
|
||||
)
|
||||
|
||||
# Find our note in search results
|
||||
found_note = None
|
||||
for note in search_notes:
|
||||
if isinstance(note, dict) and note.get("id") == note_id:
|
||||
found_note = note
|
||||
break
|
||||
|
||||
assert found_note is not None, (
|
||||
f"Created note not found in search results. Search returned: {search_response}"
|
||||
)
|
||||
assert found_note["title"] == updated_title
|
||||
|
||||
# 9. Delete note via MCP
|
||||
logger.info(f"Deleting note via MCP: {note_id}")
|
||||
delete_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_delete_note", {"note_id": note_id}
|
||||
)
|
||||
|
||||
assert delete_result.isError is False, (
|
||||
f"MCP note deletion failed: {delete_result.content}"
|
||||
)
|
||||
|
||||
# 10. Verify deletion via direct NextcloudClient
|
||||
try:
|
||||
await nc_client.notes.get_note(note_id)
|
||||
pytest.fail("Note should have been deleted but was still found")
|
||||
except Exception:
|
||||
# Expected - note should be deleted
|
||||
logger.info(f"Successfully verified note {note_id} was deleted")
|
||||
created_note = None # Mark as cleaned up
|
||||
|
||||
finally:
|
||||
# Cleanup in case of test failure
|
||||
if created_note is not None:
|
||||
try:
|
||||
note_data = json.loads(created_note)
|
||||
await nc_client.notes.delete_note(note_data["id"])
|
||||
logger.info(f"Cleaned up note {note_data['id']} after test failure")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup note: {e}")
|
||||
|
||||
|
||||
async def test_mcp_notes_etag_conflict(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test that MCP note updates fail when using stale ETags."""
|
||||
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
test_title = f"ETag Test Note {unique_suffix}"
|
||||
test_content = f"This is test content for ETag testing {unique_suffix}"
|
||||
test_category = "ETTesting"
|
||||
|
||||
created_note = None
|
||||
|
||||
try:
|
||||
# 1. Create note via MCP
|
||||
logger.info(f"Creating note for ETag conflict test: {test_title}")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_create_note",
|
||||
{"title": test_title, "content": test_content, "category": test_category},
|
||||
)
|
||||
|
||||
assert create_result.isError is False
|
||||
note_data = json.loads(create_result.content[0].text)
|
||||
note_id = note_data["id"]
|
||||
original_etag = note_data["etag"]
|
||||
created_note = note_data
|
||||
|
||||
# 2. Update note to change ETag
|
||||
first_update_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_update_note",
|
||||
{
|
||||
"note_id": note_id,
|
||||
"etag": original_etag,
|
||||
"title": f"First Update {test_title}",
|
||||
"content": test_content,
|
||||
"category": test_category,
|
||||
},
|
||||
)
|
||||
|
||||
assert first_update_result.isError is False
|
||||
updated_data = json.loads(first_update_result.content[0].text)
|
||||
new_etag = updated_data["etag"]
|
||||
assert new_etag != original_etag, "ETag should have changed after update"
|
||||
|
||||
# 3. Try to update with the stale (original) ETag - this should fail
|
||||
logger.info(f"Attempting update with stale ETag: {original_etag}")
|
||||
conflict_result = await nc_mcp_client.call_tool(
|
||||
"nc_notes_update_note",
|
||||
{
|
||||
"note_id": note_id,
|
||||
"etag": original_etag, # Use stale ETag
|
||||
"title": "This should fail",
|
||||
"content": "This update should be rejected",
|
||||
"category": test_category,
|
||||
},
|
||||
)
|
||||
|
||||
# 4. Verify the update was rejected with a 412 error
|
||||
# With McpError, tools now return proper error responses
|
||||
assert conflict_result.isError is True, "Update with stale ETag should fail"
|
||||
response_content = conflict_result.content[0].text
|
||||
assert "modified by someone else" in response_content, (
|
||||
f"Expected conflict error message, got: {response_content}"
|
||||
)
|
||||
|
||||
logger.info("Successfully verified ETag conflict handling via MCP")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if created_note is not None:
|
||||
try:
|
||||
await nc_client.notes.delete_note(created_note["id"])
|
||||
logger.info(f"Cleaned up test note {created_note['id']}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup test note: {e}")
|
||||
|
||||
|
||||
async def test_mcp_webdav_workflow(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test WebDAV file operations via MCP tools with verification via NextcloudClient."""
|
||||
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
test_dir = f"mcp_test_dir_{unique_suffix}"
|
||||
test_file = f"mcp_test_file_{unique_suffix}.txt"
|
||||
test_file_path = f"{test_dir}/{test_file}"
|
||||
test_content = f"This is test content for MCP WebDAV testing {unique_suffix}"
|
||||
|
||||
try:
|
||||
# 1. Create directory via MCP
|
||||
logger.info(f"Creating directory via MCP: {test_dir}")
|
||||
create_dir_result = await nc_mcp_client.call_tool(
|
||||
"nc_webdav_create_directory", {"path": test_dir}
|
||||
)
|
||||
|
||||
assert create_dir_result.isError is False, (
|
||||
f"MCP directory creation failed: {create_dir_result.content}"
|
||||
)
|
||||
|
||||
# 2. Verify directory creation via direct WebDAV
|
||||
dir_listing = await nc_client.webdav.list_directory("")
|
||||
dir_names = [item["name"] for item in dir_listing if item["is_directory"]]
|
||||
assert test_dir in dir_names, f"Directory {test_dir} not found in root listing"
|
||||
|
||||
# 3. Write file via MCP
|
||||
logger.info(f"Writing file via MCP: {test_file_path}")
|
||||
write_result = await nc_mcp_client.call_tool(
|
||||
"nc_webdav_write_file",
|
||||
{
|
||||
"path": test_file_path,
|
||||
"content": test_content,
|
||||
"content_type": "text/plain",
|
||||
},
|
||||
)
|
||||
|
||||
assert write_result.isError is False, (
|
||||
f"MCP file write failed: {write_result.content}"
|
||||
)
|
||||
|
||||
# 4. Verify file creation via direct WebDAV
|
||||
file_listing = await nc_client.webdav.list_directory(test_dir)
|
||||
file_names = [item["name"] for item in file_listing if not item["is_directory"]]
|
||||
assert test_file in file_names, (
|
||||
f"File {test_file} not found in directory listing"
|
||||
)
|
||||
|
||||
# 5. Read file via MCP
|
||||
logger.info(f"Reading file via MCP: {test_file_path}")
|
||||
read_result = await nc_mcp_client.call_tool(
|
||||
"nc_webdav_read_file", {"path": test_file_path}
|
||||
)
|
||||
|
||||
assert read_result.isError is False, (
|
||||
f"MCP file read failed: {read_result.content}"
|
||||
)
|
||||
read_data = json.loads(read_result.content[0].text)
|
||||
|
||||
assert read_data["content"] == test_content, "File content mismatch"
|
||||
assert read_data["path"] == test_file_path
|
||||
assert "text/plain" in read_data["content_type"]
|
||||
|
||||
# 6. Verify file content via direct WebDAV
|
||||
direct_content, direct_content_type = await nc_client.webdav.read_file(
|
||||
test_file_path
|
||||
)
|
||||
assert direct_content.decode("utf-8") == test_content
|
||||
|
||||
# 7. List directory via MCP
|
||||
logger.info(f"Listing directory via MCP: {test_dir}")
|
||||
list_result = await nc_mcp_client.call_tool(
|
||||
"nc_webdav_list_directory", {"path": test_dir}
|
||||
)
|
||||
|
||||
assert list_result.isError is False, (
|
||||
f"MCP directory listing failed: {list_result.content}"
|
||||
)
|
||||
listing_text = list_result.content[0].text
|
||||
logger.info(f"Directory listing response: {listing_text}")
|
||||
listing_data = json.loads(listing_text)
|
||||
|
||||
# Ensure listing_data is a list
|
||||
if not isinstance(listing_data, list):
|
||||
logger.warning(
|
||||
f"Expected directory listing to be a list, got: {type(listing_data)}"
|
||||
)
|
||||
listing_data = [listing_data] if listing_data else []
|
||||
|
||||
# Find our file in the listing
|
||||
found_file = None
|
||||
for item in listing_data:
|
||||
if isinstance(item, dict) and item.get("name") == test_file:
|
||||
found_file = item
|
||||
break
|
||||
|
||||
assert found_file is not None, (
|
||||
f"File {test_file} not found in MCP directory listing"
|
||||
)
|
||||
assert found_file["is_directory"] is False
|
||||
assert found_file["size"] == len(test_content.encode("utf-8"))
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
logger.info(f"Cleaning up test file: {test_file_path}")
|
||||
await nc_mcp_client.call_tool(
|
||||
"nc_webdav_delete_resource", {"path": test_file_path}
|
||||
)
|
||||
|
||||
logger.info(f"Cleaning up test directory: {test_dir}")
|
||||
await nc_mcp_client.call_tool(
|
||||
"nc_webdav_delete_resource", {"path": test_dir}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup WebDAV resources: {e}")
|
||||
|
||||
|
||||
async def test_mcp_resources_access(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test accessing MCP resources and compare with direct API calls."""
|
||||
|
||||
# 1. Test capabilities resource
|
||||
logger.info("Testing capabilities resource via MCP")
|
||||
caps_result = await nc_mcp_client.read_resource("nc://capabilities")
|
||||
assert len(caps_result.contents) == 1
|
||||
mcp_capabilities = json.loads(caps_result.contents[0].text)
|
||||
|
||||
# Compare with direct API call
|
||||
direct_capabilities = await nc_client.capabilities()
|
||||
|
||||
# Basic validation - both should have similar structure
|
||||
# Both return full OCS response structure
|
||||
assert "ocs" in mcp_capabilities
|
||||
assert "data" in mcp_capabilities["ocs"]
|
||||
assert "version" in mcp_capabilities["ocs"]["data"]
|
||||
assert "ocs" in direct_capabilities
|
||||
assert "data" in direct_capabilities["ocs"]
|
||||
assert "version" in direct_capabilities["ocs"]["data"]
|
||||
|
||||
# 2. Test notes settings resource
|
||||
logger.info("Testing notes settings resource via MCP")
|
||||
settings_result = await nc_mcp_client.read_resource("notes://settings")
|
||||
assert len(settings_result.contents) == 1
|
||||
mcp_settings = json.loads(settings_result.contents[0].text)
|
||||
|
||||
# Compare with direct API call
|
||||
direct_settings = await nc_client.notes.get_settings()
|
||||
|
||||
# Both should have settings data
|
||||
assert isinstance(mcp_settings, dict)
|
||||
assert isinstance(direct_settings, dict)
|
||||
|
||||
logger.info("Successfully verified MCP resources match direct API calls")
|
||||
|
||||
|
||||
async def test_mcp_calendar_workflow(
|
||||
nc_mcp_client: ClientSession, nc_client: NextcloudClient
|
||||
):
|
||||
"""Test complete Calendar workflow via MCP tools with verification via NextcloudClient."""
|
||||
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
test_event_title = f"MCP Test Event {unique_suffix}"
|
||||
test_location = f"MCP Test Location {unique_suffix}"
|
||||
|
||||
created_event = None
|
||||
calendar_name = None
|
||||
|
||||
try:
|
||||
# 1. List calendars via MCP
|
||||
logger.info("Listing calendars via MCP")
|
||||
calendars_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_list_calendars", {}
|
||||
)
|
||||
|
||||
assert calendars_result.isError is False, (
|
||||
f"MCP calendar listing failed: {calendars_result.content}"
|
||||
)
|
||||
|
||||
calendars_response = json.loads(calendars_result.content[0].text)
|
||||
|
||||
# Debug output to understand the structure
|
||||
logger.info(f"calendars_response type: {type(calendars_response)}")
|
||||
logger.info(f"calendars_response content: {calendars_response}")
|
||||
|
||||
# Expect structured response with Pydantic format
|
||||
assert isinstance(calendars_response, dict), (
|
||||
f"Expected calendar response to be a dict with structured format, got: {type(calendars_response)}"
|
||||
)
|
||||
assert "calendars" in calendars_response, (
|
||||
f"Expected 'calendars' field in response, got keys: {list(calendars_response.keys())}"
|
||||
)
|
||||
assert "success" in calendars_response and calendars_response["success"], (
|
||||
f"Expected successful calendar response, got: {calendars_response}"
|
||||
)
|
||||
|
||||
calendars_list = calendars_response["calendars"]
|
||||
assert isinstance(calendars_list, list), (
|
||||
f"Expected calendars to be a list, got: {type(calendars_list)}"
|
||||
)
|
||||
|
||||
if not calendars_list:
|
||||
pytest.skip("No calendars available for testing")
|
||||
|
||||
# Use the first available calendar
|
||||
calendar_name = calendars_list[0]["name"]
|
||||
logger.info(f"Using calendar: {calendar_name}")
|
||||
|
||||
# 2. Create event via MCP
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
tomorrow = datetime.now() + timedelta(days=1)
|
||||
start_datetime = tomorrow.strftime("%Y-%m-%dT14:00:00")
|
||||
end_datetime = tomorrow.strftime("%Y-%m-%dT15:00:00")
|
||||
|
||||
event_data = {
|
||||
"calendar_name": calendar_name,
|
||||
"title": test_event_title,
|
||||
"start_datetime": start_datetime,
|
||||
"end_datetime": end_datetime,
|
||||
"description": f"Test event created via MCP {unique_suffix}",
|
||||
"location": test_location,
|
||||
"categories": "testing,mcp",
|
||||
"status": "CONFIRMED",
|
||||
"priority": 5,
|
||||
}
|
||||
|
||||
logger.info(f"Creating event via MCP: {test_event_title}")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_create_event", event_data
|
||||
)
|
||||
|
||||
assert create_result.isError is False, (
|
||||
f"MCP event creation failed: {create_result.content}"
|
||||
)
|
||||
|
||||
created_event_data = json.loads(create_result.content[0].text)
|
||||
event_uid = created_event_data["uid"]
|
||||
created_event = {"uid": event_uid, "calendar_name": calendar_name}
|
||||
|
||||
logger.info(f"Event created via MCP with UID: {event_uid}")
|
||||
|
||||
# 3. Verify creation via direct NextcloudClient
|
||||
direct_event, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
||||
assert direct_event["title"] == test_event_title
|
||||
assert direct_event["location"] == test_location
|
||||
assert "testing" in direct_event.get("categories", "")
|
||||
|
||||
# 4. Get event via MCP
|
||||
logger.info(f"Getting event via MCP: {event_uid}")
|
||||
get_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_get_event",
|
||||
{"calendar_name": calendar_name, "event_uid": event_uid},
|
||||
)
|
||||
|
||||
assert get_result.isError is False, (
|
||||
f"MCP event get failed: {get_result.content}"
|
||||
)
|
||||
|
||||
get_event_data = json.loads(get_result.content[0].text)
|
||||
assert get_event_data["title"] == test_event_title
|
||||
assert get_event_data["location"] == test_location
|
||||
|
||||
# 5. **TEST nc_calendar_list_events - This is the main tool we're testing**
|
||||
logger.info("Testing nc_calendar_list_events via MCP")
|
||||
|
||||
# Get today and next week for date range
|
||||
today = datetime.now()
|
||||
next_week = today + timedelta(days=7)
|
||||
start_date = today.strftime("%Y-%m-%d")
|
||||
end_date = next_week.strftime("%Y-%m-%d")
|
||||
|
||||
list_events_data = {
|
||||
"calendar_name": calendar_name,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"limit": 50,
|
||||
"location_contains": "MCP Test",
|
||||
"title_contains": unique_suffix,
|
||||
}
|
||||
|
||||
list_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_list_events", list_events_data
|
||||
)
|
||||
|
||||
assert list_result.isError is False, (
|
||||
f"MCP list events failed: {list_result.content}"
|
||||
)
|
||||
|
||||
events_data = json.loads(list_result.content[0].text)
|
||||
|
||||
# Debug output to understand what nc_calendar_list_events returns
|
||||
logger.info(f"list_events result type: {type(events_data)}")
|
||||
logger.info(f"list_events result content: {events_data}")
|
||||
|
||||
# Handle single event returned as dict instead of list (same fix as calendars)
|
||||
if isinstance(events_data, dict):
|
||||
# Single event returned as dict instead of list
|
||||
events_data = [events_data]
|
||||
|
||||
assert isinstance(events_data, list), "Expected events list"
|
||||
|
||||
# Our created event should be in the list
|
||||
found_event = None
|
||||
for event in events_data:
|
||||
if event.get("uid") == event_uid:
|
||||
found_event = event
|
||||
break
|
||||
|
||||
assert found_event is not None, (
|
||||
f"Created event {event_uid} not found in events list"
|
||||
)
|
||||
assert found_event["title"] == test_event_title
|
||||
|
||||
# 6. Test list events across all calendars
|
||||
logger.info("Testing nc_calendar_list_events across all calendars")
|
||||
|
||||
all_calendars_data = {
|
||||
"calendar_name": "", # Will be ignored
|
||||
"search_all_calendars": True,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"title_contains": unique_suffix,
|
||||
}
|
||||
|
||||
all_list_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_list_events", all_calendars_data
|
||||
)
|
||||
|
||||
assert all_list_result.isError is False, (
|
||||
f"MCP list all events failed: {all_list_result.content}"
|
||||
)
|
||||
|
||||
all_events_data = json.loads(all_list_result.content[0].text)
|
||||
|
||||
# Handle single event returned as dict instead of list (same fix as calendars)
|
||||
if isinstance(all_events_data, dict):
|
||||
# Single event returned as dict instead of list
|
||||
all_events_data = [all_events_data]
|
||||
|
||||
assert isinstance(all_events_data, list), "Expected events list"
|
||||
|
||||
# Our event should still be found when searching all calendars
|
||||
found_in_all = any(event.get("uid") == event_uid for event in all_events_data)
|
||||
assert found_in_all, "Event not found when searching all calendars"
|
||||
|
||||
# 7. Update event via MCP
|
||||
updated_title = f"Updated {test_event_title}"
|
||||
updated_description = f"Updated description {unique_suffix}"
|
||||
|
||||
update_data = {
|
||||
"calendar_name": calendar_name,
|
||||
"event_uid": event_uid,
|
||||
"title": updated_title,
|
||||
"description": updated_description,
|
||||
"priority": 1,
|
||||
}
|
||||
|
||||
logger.info(f"Updating event via MCP: {event_uid}")
|
||||
update_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_update_event", update_data
|
||||
)
|
||||
|
||||
assert update_result.isError is False, (
|
||||
f"MCP event update failed: {update_result.content}"
|
||||
)
|
||||
|
||||
# 8. Verify update via direct NextcloudClient
|
||||
updated_direct_event, _ = await nc_client.calendar.get_event(
|
||||
calendar_name, event_uid
|
||||
)
|
||||
assert updated_direct_event["title"] == updated_title
|
||||
assert updated_direct_event["description"] == updated_description
|
||||
assert updated_direct_event["priority"] == 1
|
||||
|
||||
# 9. Test upcoming events via MCP
|
||||
logger.info("Testing nc_calendar_get_upcoming_events via MCP")
|
||||
upcoming_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_get_upcoming_events",
|
||||
{"calendar_name": calendar_name, "days_ahead": 7, "limit": 10},
|
||||
)
|
||||
|
||||
assert upcoming_result.isError is False, (
|
||||
f"MCP upcoming events failed: {upcoming_result.content}"
|
||||
)
|
||||
|
||||
upcoming_events = json.loads(upcoming_result.content[0].text)
|
||||
|
||||
# Handle single event returned as dict instead of list (same fix as other tools)
|
||||
if isinstance(upcoming_events, dict):
|
||||
# Single event returned as dict instead of list
|
||||
upcoming_events = [upcoming_events]
|
||||
|
||||
assert isinstance(upcoming_events, list), "Expected upcoming events list"
|
||||
|
||||
# 10. Delete event via MCP
|
||||
logger.info(f"Deleting event via MCP: {event_uid}")
|
||||
delete_result = await nc_mcp_client.call_tool(
|
||||
"nc_calendar_delete_event",
|
||||
{"calendar_name": calendar_name, "event_uid": event_uid},
|
||||
)
|
||||
|
||||
assert delete_result.isError is False, (
|
||||
f"MCP event deletion failed: {delete_result.content}"
|
||||
)
|
||||
|
||||
# 11. Verify deletion via direct NextcloudClient
|
||||
try:
|
||||
await nc_client.calendar.get_event(calendar_name, event_uid)
|
||||
pytest.fail("Event should have been deleted but was still found")
|
||||
except Exception:
|
||||
# Expected - event should be deleted
|
||||
logger.info(f"Successfully verified event {event_uid} was deleted")
|
||||
created_event = None # Mark as cleaned up
|
||||
|
||||
except Exception as e:
|
||||
if "Calendar app may not be enabled" in str(
|
||||
e
|
||||
) or "No calendars available" in str(e):
|
||||
pytest.skip("Calendar functionality not available for testing")
|
||||
raise
|
||||
|
||||
finally:
|
||||
# Cleanup in case of test failure
|
||||
if created_event is not None:
|
||||
try:
|
||||
await nc_client.calendar.delete_event(
|
||||
created_event["calendar_name"], created_event["uid"]
|
||||
)
|
||||
logger.info(
|
||||
f"Cleaned up event {created_event['uid']} after test failure"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup event: {e}")
|
||||
Reference in New Issue
Block a user