This commit is contained in:
Chris Coutinho
2025-05-06 16:55:49 +02:00
parent dea882c2f5
commit c6ce5bd338
9 changed files with 429 additions and 754 deletions
+117
View File
@@ -0,0 +1,117 @@
import pytest
import logging
import time
import uuid
from httpx import HTTPStatusError
from nextcloud_mcp_server.client import NextcloudClient
# Note: nc_client fixture is session-scoped in conftest.py
# Note: temporary_note and temporary_note_with_attachment fixtures are function-scoped in conftest.py
logger = logging.getLogger(__name__)
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
def test_attachments_add_and_get(nc_client: NextcloudClient, temporary_note_with_attachment: tuple):
"""
Tests adding an attachment (via fixture) and retrieving it.
"""
note_data, attachment_filename, attachment_content = temporary_note_with_attachment
note_id = note_data["id"]
logger.info(f"Attempting to retrieve attachment '{attachment_filename}' added by fixture for note ID: {note_id}")
retrieved_content, retrieved_mime = nc_client.get_note_attachment(
note_id=note_id,
filename=attachment_filename
)
logger.info(f"Attachment retrieved. Mime type: {retrieved_mime}, Size: {len(retrieved_content)} bytes")
assert retrieved_content == attachment_content
assert "text/plain" in retrieved_mime # Fixture uses text/plain
logger.info("Retrieved attachment content and mime type verified successfully.")
def test_attachments_add_to_note_with_category(nc_client: NextcloudClient, temporary_note: dict):
"""
Tests adding and retrieving an attachment specifically for a note that has a category.
Uses temporary_note fixture and adds attachment manually within the test.
"""
note_data = temporary_note # Note created by fixture (has category 'TemporaryTesting')
note_id = note_data["id"]
note_category = note_data["category"]
logger.info(f"Using note ID: {note_id} with category '{note_category}' for attachment test.")
# Add attachment within the test
unique_suffix = uuid.uuid4().hex[:8]
attachment_filename = f"category_attach_{unique_suffix}.txt"
attachment_content = f"Content for {attachment_filename}".encode('utf-8')
attachment_mime = "text/plain"
logger.info(f"Attempting to add attachment '{attachment_filename}' to note ID: {note_id}")
upload_response = nc_client.add_note_attachment(
note_id=note_id,
filename=attachment_filename,
content=attachment_content,
mime_type=attachment_mime
)
assert upload_response and "status_code" in upload_response
assert upload_response["status_code"] in [201, 204]
logger.info(f"Attachment '{attachment_filename}' added successfully (Status: {upload_response['status_code']}).")
time.sleep(1)
# Get and Verify Attachment
logger.info(f"Attempting to retrieve attachment '{attachment_filename}' from note ID: {note_id}")
retrieved_content, retrieved_mime = nc_client.get_note_attachment(
note_id=note_id,
filename=attachment_filename
)
logger.info(f"Attachment retrieved. Mime type: {retrieved_mime}, Size: {len(retrieved_content)} bytes")
assert retrieved_content == attachment_content
assert attachment_mime in retrieved_mime
logger.info("Retrieved attachment content and mime type verified successfully for note with category.")
# Cleanup is handled by the temporary_note fixture
def test_attachments_cleanup_on_note_delete(nc_client: NextcloudClient, temporary_note_with_attachment: tuple):
"""
Tests that the attachment (and its directory) are deleted when the parent note is deleted.
Relies on the cleanup mechanism within notes_delete_note and the temporary_note fixture.
"""
note_data, attachment_filename, _ = temporary_note_with_attachment
note_id = note_data["id"]
# Fixture setup already added the attachment.
# Fixture teardown (from temporary_note) will delete the note.
# We just need to verify the attachment is gone *after* the test finishes
# and the fixture cleanup runs. However, pytest fixtures don't easily allow
# checking state *after* cleanup.
# Instead, we will manually delete the note here and verify the attachment is gone.
logger.info(f"Attachment '{attachment_filename}' exists for note {note_id} (added by fixture).")
# Manually delete the note
logger.info(f"Manually deleting note ID: {note_id} within the test.")
nc_client.notes_delete_note(note_id=note_id)
logger.info(f"Note ID: {note_id} deleted successfully.")
time.sleep(1)
# Verify Note Is Deleted
with pytest.raises(HTTPStatusError) as excinfo_note:
nc_client.notes_get_note(note_id=note_id)
assert excinfo_note.value.response.status_code == 404
logger.info(f"Verified note {note_id} deletion (404 received).")
# Verify Attachment Is Deleted (via 404 on GET)
logger.info(f"Verifying attachment '{attachment_filename}' is deleted for note ID: {note_id}")
with pytest.raises(HTTPStatusError) as excinfo_attach:
nc_client.get_note_attachment(
note_id=note_id,
filename=attachment_filename
)
# Expect 404 because the parent directory (.attachments.NOTE_ID) should be gone
assert excinfo_attach.value.response.status_code == 404
logger.info(f"Attachment '{attachment_filename}' correctly not found (404) after note deletion.")
# Note: The temporary_note fixture will still run its cleanup,
# but it will find the note already deleted (404) and handle it gracefully.
+106
View File
@@ -0,0 +1,106 @@
import pytest
import os
import time
import uuid
import logging
import tempfile
from PIL import Image, ImageDraw
from io import BytesIO
from httpx import HTTPStatusError # Import if needed for specific error checks
from nextcloud_mcp_server.client import NextcloudClient
# Note: nc_client fixture is session-scoped in conftest.py
# Note: temporary_note fixture is function-scoped in conftest.py
logger = logging.getLogger(__name__)
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
# Keep the test_image fixture as it's specific to generating image data
@pytest.fixture(scope="module") # Keep module scope if image generation is slow
def test_image_data() -> tuple[bytes, str]:
"""
Generate test image data (bytes) and suggest a filename.
Returns (image_bytes, suggested_filename).
"""
logger.info("Generating test image data in memory.")
img = Image.new('RGB', (300, 200), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
draw.rectangle([(20, 20), (280, 180)], fill=(0, 120, 212)) # Blue rectangle
draw.text((50, 90), "Nextcloud Notes Test Image", fill=(255, 255, 255)) # White text
img_byte_arr = BytesIO()
img.save(img_byte_arr, format='PNG')
image_bytes = img_byte_arr.getvalue()
suggested_filename = "test_image.png"
logger.info(f"Generated test image data ({len(image_bytes)} bytes).")
return image_bytes, suggested_filename
def test_note_with_embedded_image(nc_client: NextcloudClient, temporary_note: dict, test_image_data: tuple):
"""
Tests creating a note, attaching an image, embedding it in the content,
and verifying the attachment can be retrieved.
"""
note_data = temporary_note # Use fixture for note creation/cleanup
note_id = note_data["id"]
note_etag = note_data["etag"]
image_content, suggested_filename = test_image_data # Get image data from fixture
unique_suffix = uuid.uuid4().hex[:8]
attachment_filename = f"test_image_{unique_suffix}.png" # Make filename unique per run
# 1. Upload the image as an attachment
logger.info(f"Uploading image attachment '{attachment_filename}' to note {note_id}...")
upload_response = nc_client.add_note_attachment(
note_id=note_id,
filename=attachment_filename,
content=image_content,
mime_type="image/png"
)
assert upload_response and upload_response.get("status_code") in [201, 204]
logger.info(f"Image uploaded successfully (Status: {upload_response.get('status_code')}).")
time.sleep(1) # Allow potential processing time
# 2. Update the note content to include the embedded image references
updated_content = f"""{note_data['content']}
## Image Embedding Test
### Markdown Syntax
![Test Image MD](.attachments.{note_id}/{attachment_filename})
### HTML Syntax
<img src=".attachments.{note_id}/{attachment_filename}" alt="Test Image HTML" width="150" />
"""
logger.info("Updating note content with image references...")
updated_note = nc_client.notes_update_note(
note_id=note_id,
etag=note_etag, # Use etag from the created note
content=updated_content,
title=note_data['title'], # Pass required fields
category=note_data['category'] # Pass required fields
)
new_etag = updated_note["etag"]
assert new_etag != note_etag
logger.info("Note content updated with image references.")
time.sleep(1)
# 3. Verify the updated note content
retrieved_note = nc_client.notes_get_note(note_id=note_id)
assert f".attachments.{note_id}/{attachment_filename}" in retrieved_note["content"]
logger.info("Verified image reference exists in updated note content.")
# 4. Verify the image attachment can be retrieved
logger.info(f"Retrieving image attachment '{attachment_filename}'...")
retrieved_img_content, mime_type = nc_client.get_note_attachment(
note_id=note_id,
filename=attachment_filename
)
assert retrieved_img_content == image_content
assert mime_type.startswith("image/png")
logger.info("Successfully retrieved and verified image attachment content and mime type.")
# Note cleanup is handled by the temporary_note fixture
+114
View File
@@ -0,0 +1,114 @@
import pytest
import logging
import time
import uuid # Keep uuid if needed for generating unique data within tests
from httpx import HTTPStatusError
from nextcloud_mcp_server.client import NextcloudClient
# Note: nc_client fixture is now session-scoped in conftest.py
logger = logging.getLogger(__name__)
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
def test_notes_api_create_and_read(nc_client: NextcloudClient, temporary_note: dict):
"""
Tests creating a note via the API (using fixture) and then reading it back.
"""
created_note_data = temporary_note # Get data from fixture
note_id = created_note_data["id"]
logger.info(f"Reading note created by fixture, ID: {note_id}")
read_note = nc_client.notes_get_note(note_id=note_id)
assert read_note["id"] == note_id
assert read_note["title"] == created_note_data["title"]
assert read_note["content"] == created_note_data["content"]
assert read_note["category"] == created_note_data["category"]
logger.info(f"Successfully read and verified note ID: {note_id}")
def test_notes_api_update(nc_client: NextcloudClient, temporary_note: dict):
"""
Tests updating a note created by the fixture.
"""
created_note_data = temporary_note
note_id = created_note_data["id"]
original_etag = created_note_data["etag"]
original_category = created_note_data["category"]
update_title = f"Updated Title {uuid.uuid4().hex[:8]}"
update_content = f"Updated Content {uuid.uuid4().hex[:8]}"
logger.info(f"Attempting to update note ID: {note_id} with etag: {original_etag}")
updated_note = nc_client.notes_update_note(
note_id=note_id,
etag=original_etag,
title=update_title,
content=update_content,
# category=original_category # Explicitly pass category if required by update
)
logger.info(f"Note updated: {updated_note}")
assert updated_note["id"] == note_id
assert updated_note["title"] == update_title
assert updated_note["content"] == update_content
assert updated_note["category"] == original_category # Verify category didn't change
assert "etag" in updated_note
assert updated_note["etag"] != original_etag # Etag must change
# Optional: Verify update by reading again
time.sleep(1) # Allow potential propagation delay
read_updated_note = nc_client.notes_get_note(note_id=note_id)
assert read_updated_note["title"] == update_title
assert read_updated_note["content"] == update_content
logger.info(f"Successfully updated and verified note ID: {note_id}")
def test_notes_api_update_conflict(nc_client: NextcloudClient, temporary_note: dict):
"""
Tests that attempting to update with an old etag fails with 412.
"""
created_note_data = temporary_note
note_id = created_note_data["id"]
original_etag = created_note_data["etag"]
# Perform a first update to change the etag
first_update_title = f"First Update {uuid.uuid4().hex[:8]}"
logger.info(f"Performing first update on note ID: {note_id} to change etag.")
first_updated_note = nc_client.notes_update_note(
note_id=note_id,
etag=original_etag,
title=first_update_title,
content="First update content",
# category=created_note_data["category"] # Pass category if required
)
new_etag = first_updated_note["etag"]
assert new_etag != original_etag
logger.info(f"Note ID: {note_id} updated, new etag: {new_etag}")
time.sleep(1)
# Now attempt update with the *original* etag
logger.info(f"Attempting second update on note ID: {note_id} with OLD etag: {original_etag}")
with pytest.raises(HTTPStatusError) as excinfo:
nc_client.notes_update_note(
note_id=note_id,
etag=original_etag, # Use the stale etag
title="This update should fail due to conflict",
# category=created_note_data["category"] # Pass category if required
)
assert excinfo.value.response.status_code == 412 # Precondition Failed
logger.info("Update with old etag correctly failed with 412 Precondition Failed.")
def test_notes_api_delete_nonexistent(nc_client: NextcloudClient):
"""
Tests deleting a note that doesn't exist fails with 404.
"""
non_existent_id = 999999999 # Use an ID highly unlikely to exist
logger.info(f"\nAttempting to delete non-existent note ID: {non_existent_id}")
with pytest.raises(HTTPStatusError) as excinfo:
nc_client.notes_delete_note(note_id=non_existent_id)
assert excinfo.value.response.status_code == 404
logger.info(f"Deleting non-existent note ID: {non_existent_id} correctly failed with 404.")
# --- Attachment tests moved to test_attachments.py ---