Files
2025-08-01 12:21:32 +02:00

173 lines
6.5 KiB
Python

import logging
import os
import uuid
from typing import Any, AsyncGenerator
import pytest
from httpx import HTTPStatusError
from mcp import ClientSession
from mcp.client.sse import sse_client
from nextcloud_mcp_server.client import NextcloudClient
logger = logging.getLogger(__name__)
@pytest.fixture(scope="session")
async def nc_client() -> AsyncGenerator[NextcloudClient, Any]:
"""
Fixture to create a NextcloudClient instance for integration tests.
Uses environment variables for configuration.
"""
assert os.getenv("NEXTCLOUD_HOST"), "NEXTCLOUD_HOST env var not set"
assert os.getenv("NEXTCLOUD_USERNAME"), "NEXTCLOUD_USERNAME env var not set"
assert os.getenv("NEXTCLOUD_PASSWORD"), "NEXTCLOUD_PASSWORD env var not set"
logger.info("Creating session-scoped NextcloudClient from environment variables.")
client = NextcloudClient.from_env()
# Optional: Perform a quick check like getting capabilities to ensure connection works
try:
await client.capabilities()
logger.info(
"NextcloudClient session fixture initialized and capabilities checked."
)
yield client
except Exception as e:
logger.error(f"Failed to initialize NextcloudClient session fixture: {e}")
pytest.fail(f"Failed to connect to Nextcloud or get capabilities: {e}")
finally:
await client.close()
@pytest.fixture
async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
"""
Fixture to create an MCP client session for integration tests.
"""
logger.info("Creating SSE client")
sse_context = sse_client(url="http://127.0.0.1:8000/sse")
session_context = None
try:
read, write = await sse_context.__aenter__()
session_context = ClientSession(read, write)
session = await session_context.__aenter__()
await session.initialize()
logger.info("MCP client session initialized successfully")
yield session
finally:
# Clean up in reverse order, ignoring task scope issues
if session_context is not None:
try:
await session_context.__aexit__(None, None, None)
except RuntimeError as e:
if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else:
logger.warning(f"Error closing session: {e}")
except Exception as e:
logger.warning(f"Error closing session: {e}")
try:
await sse_context.__aexit__(None, None, None)
except RuntimeError as e:
if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else:
logger.warning(f"Error closing SSE client: {e}")
except Exception as e:
logger.warning(f"Error closing SSE client: {e}")
@pytest.fixture
async def temporary_note(nc_client: NextcloudClient):
"""
Fixture to create a temporary note for a test and ensure its deletion afterward.
Yields the created note dictionary.
"""
note_id = None
unique_suffix = uuid.uuid4().hex[:8]
note_title = f"Temporary Test Note {unique_suffix}"
note_content = f"Content for temporary note {unique_suffix}"
note_category = "TemporaryTesting"
created_note_data = None
logger.info(f"Creating temporary note: {note_title}")
try:
created_note_data = await nc_client.notes.create_note(
title=note_title, content=note_content, category=note_category
)
note_id = created_note_data.get("id")
if not note_id:
pytest.fail("Failed to get ID from created temporary note.")
logger.info(f"Temporary note created with ID: {note_id}")
yield created_note_data # Provide the created note data to the test
finally:
if note_id:
logger.info(f"Cleaning up temporary note ID: {note_id}")
try:
await nc_client.notes.delete_note(note_id=note_id)
logger.info(f"Successfully deleted temporary note ID: {note_id}")
except HTTPStatusError as e:
# Ignore 404 if note was already deleted by the test itself
if e.response.status_code != 404:
logger.error(f"HTTP error deleting temporary note {note_id}: {e}")
else:
logger.warning(f"Temporary note {note_id} already deleted (404).")
except Exception as e:
logger.error(f"Unexpected error deleting temporary note {note_id}: {e}")
@pytest.fixture
async def temporary_note_with_attachment(
nc_client: NextcloudClient, temporary_note: dict
):
"""
Fixture that creates a temporary note, adds an attachment, and cleans up both.
Yields a tuple: (note_data, attachment_filename, attachment_content).
Depends on the temporary_note fixture.
"""
note_data = temporary_note
note_id = note_data["id"]
note_category = note_data.get("category") # Get category from the note data
unique_suffix = uuid.uuid4().hex[:8]
attachment_filename = f"temp_attach_{unique_suffix}.txt"
attachment_content = f"Content for {attachment_filename}".encode("utf-8")
attachment_mime = "text/plain"
logger.info(
f"Adding attachment '{attachment_filename}' to temporary note ID: {note_id} (category: '{note_category or ''}')"
)
try:
# Pass the category to add_note_attachment
upload_response = await nc_client.webdav.add_note_attachment(
note_id=note_id,
filename=attachment_filename,
content=attachment_content,
category=note_category, # Pass the fetched category
mime_type=attachment_mime,
)
assert upload_response.get("status_code") in [
201,
204,
], f"Failed to upload attachment: {upload_response}"
logger.info(f"Attachment '{attachment_filename}' added successfully.")
yield note_data, attachment_filename, attachment_content
# Cleanup for the attachment is handled by the notes_delete_note call
# in the temporary_note fixture's finally block (which deletes the .attachments dir)
except Exception as e:
logger.error(f"Failed to add attachment in fixture: {e}")
pytest.fail(f"Fixture setup failed during attachment upload: {e}")
# Note: The temporary_note fixture's finally block will handle note deletion,
# which should also trigger the WebDAV directory deletion attempt.