387 lines
14 KiB
Python
387 lines
14 KiB
Python
import asyncio
|
|
import logging
|
|
import uuid
|
|
|
|
import pytest
|
|
from httpx import HTTPStatusError
|
|
|
|
from nextcloud_mcp_server.client import NextcloudClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Mark all tests in this module as integration tests
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
async def test_cookbook_version(nc_client: NextcloudClient):
|
|
"""Test getting Cookbook app version."""
|
|
logger.info("Getting Cookbook app version")
|
|
version_data = await nc_client.cookbook.get_version()
|
|
|
|
assert "cookbook_version" in version_data
|
|
assert "api_version" in version_data
|
|
logger.info(f"Cookbook version: {version_data}")
|
|
|
|
|
|
async def test_cookbook_config(nc_client: NextcloudClient):
|
|
"""Test getting Cookbook app configuration."""
|
|
logger.info("Getting Cookbook app configuration")
|
|
config_data = await nc_client.cookbook.get_config()
|
|
|
|
# Config may be empty initially, just verify we can get it
|
|
assert isinstance(config_data, dict)
|
|
logger.info(f"Cookbook config: {config_data}")
|
|
|
|
|
|
async def test_cookbook_list_recipes(nc_client: NextcloudClient):
|
|
"""Test listing all recipes."""
|
|
logger.info("Listing all recipes")
|
|
recipes = await nc_client.cookbook.list_recipes()
|
|
|
|
assert isinstance(recipes, list)
|
|
logger.info(f"Found {len(recipes)} recipes")
|
|
|
|
|
|
async def test_cookbook_create_and_read_recipe(nc_client: NextcloudClient):
|
|
"""Test creating a recipe and reading it back."""
|
|
# Create a test recipe
|
|
recipe_name = f"Test Recipe {uuid.uuid4().hex[:8]}"
|
|
recipe_data = {
|
|
"name": recipe_name,
|
|
"description": "A test recipe for integration testing",
|
|
"recipeIngredient": ["100g flour", "2 eggs", "200ml milk"],
|
|
"recipeInstructions": [
|
|
"Mix ingredients",
|
|
"Cook for 20 minutes",
|
|
"Serve hot",
|
|
],
|
|
"recipeCategory": "Test",
|
|
"keywords": "test,integration",
|
|
"recipeYield": 4,
|
|
"prepTime": "PT15M",
|
|
"cookTime": "PT20M",
|
|
"totalTime": "PT35M",
|
|
}
|
|
|
|
logger.info(f"Creating recipe: {recipe_name}")
|
|
recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
|
|
logger.info(f"Created recipe with ID: {recipe_id}")
|
|
|
|
try:
|
|
# Read the recipe back
|
|
logger.info(f"Reading recipe ID: {recipe_id}")
|
|
retrieved_recipe = await nc_client.cookbook.get_recipe(recipe_id)
|
|
|
|
assert retrieved_recipe["name"] == recipe_name
|
|
assert (
|
|
retrieved_recipe["description"] == "A test recipe for integration testing"
|
|
)
|
|
assert len(retrieved_recipe["recipeIngredient"]) == 3
|
|
assert len(retrieved_recipe["recipeInstructions"]) == 3
|
|
assert retrieved_recipe["recipeCategory"] == "Test"
|
|
assert retrieved_recipe["recipeYield"] == 4
|
|
logger.info(f"Successfully verified recipe: {recipe_name}")
|
|
|
|
finally:
|
|
# Clean up
|
|
logger.info(f"Deleting recipe ID: {recipe_id}")
|
|
await nc_client.cookbook.delete_recipe(recipe_id)
|
|
logger.info(f"Successfully deleted recipe ID: {recipe_id}")
|
|
|
|
|
|
async def test_cookbook_update_recipe(nc_client: NextcloudClient):
|
|
"""Test updating a recipe."""
|
|
# Create a test recipe
|
|
recipe_name = f"Test Recipe {uuid.uuid4().hex[:8]}"
|
|
recipe_data = {
|
|
"name": recipe_name,
|
|
"description": "Original description",
|
|
"recipeIngredient": ["100g flour"],
|
|
"recipeInstructions": ["Mix ingredients"],
|
|
"recipeCategory": "Original",
|
|
}
|
|
|
|
logger.info(f"Creating recipe for update test: {recipe_name}")
|
|
recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
|
|
|
|
try:
|
|
# Get the current recipe first
|
|
current_recipe = await nc_client.cookbook.get_recipe(recipe_id)
|
|
|
|
# Update the recipe with all required fields
|
|
updated_data = current_recipe.copy()
|
|
updated_data["description"] = "Updated description"
|
|
updated_data["recipeIngredient"] = ["100g flour", "2 eggs"]
|
|
updated_data["recipeInstructions"] = ["Mix ingredients", "Cook"]
|
|
updated_data["recipeCategory"] = "Updated"
|
|
|
|
logger.info(f"Updating recipe ID: {recipe_id}")
|
|
updated_id = await nc_client.cookbook.update_recipe(recipe_id, updated_data)
|
|
assert updated_id == recipe_id
|
|
|
|
# Verify the update
|
|
await asyncio.sleep(1) # Allow propagation
|
|
updated_recipe = await nc_client.cookbook.get_recipe(recipe_id)
|
|
assert updated_recipe["description"] == "Updated description"
|
|
assert len(updated_recipe["recipeIngredient"]) == 2
|
|
assert len(updated_recipe["recipeInstructions"]) == 2
|
|
assert updated_recipe["recipeCategory"] == "Updated"
|
|
logger.info(f"Successfully updated recipe ID: {recipe_id}")
|
|
|
|
finally:
|
|
# Clean up
|
|
logger.info(f"Deleting recipe ID: {recipe_id}")
|
|
await nc_client.cookbook.delete_recipe(recipe_id)
|
|
|
|
|
|
async def test_cookbook_delete_nonexistent_recipe(nc_client: NextcloudClient):
|
|
"""Test deleting a non-existent recipe.
|
|
|
|
Note: The Cookbook API may return 502 or succeed silently for non-existent IDs
|
|
rather than 404. This test verifies the behavior."""
|
|
non_existent_id = 999999999
|
|
|
|
logger.info(f"Attempting to delete non-existent recipe ID: {non_existent_id}")
|
|
try:
|
|
result = await nc_client.cookbook.delete_recipe(non_existent_id)
|
|
logger.info(f"Delete returned: {result}")
|
|
# API may succeed silently or return an error message
|
|
assert isinstance(result, str)
|
|
except HTTPStatusError as e:
|
|
# API may return 404 or 502 for non-existent recipes
|
|
assert e.response.status_code in [404, 502]
|
|
logger.info(f"Delete correctly failed with {e.response.status_code}")
|
|
|
|
|
|
async def test_cookbook_import_recipe_from_url(nc_client: NextcloudClient):
|
|
"""Test importing a recipe from a URL.
|
|
|
|
This is the key feature test - importing recipes from URLs using schema.org metadata.
|
|
Uses an nginx container to serve reliable, controlled test data.
|
|
"""
|
|
|
|
# Use the nginx container hostname within the Docker network
|
|
test_url = "http://recipes/black-pepper-tofu"
|
|
|
|
logger.info(f"Importing recipe from nginx container: {test_url}")
|
|
|
|
try:
|
|
imported_recipe = await nc_client.cookbook.import_recipe(test_url)
|
|
logger.info(f"Successfully imported recipe: {imported_recipe.get('name')}")
|
|
|
|
# Verify basic recipe structure
|
|
assert "name" in imported_recipe
|
|
assert imported_recipe["name"] == "Black Pepper Tofu"
|
|
assert "id" in imported_recipe
|
|
|
|
# Verify schema.org fields were imported correctly
|
|
assert imported_recipe.get("description")
|
|
assert len(imported_recipe.get("recipeIngredient", [])) > 0
|
|
assert len(imported_recipe.get("recipeInstructions", [])) > 0
|
|
assert imported_recipe.get("recipeCategory") == "Main Course"
|
|
assert "tofu" in imported_recipe.get("keywords", "").lower()
|
|
|
|
recipe_id = int(imported_recipe["id"])
|
|
|
|
# Verify we can read it back
|
|
retrieved = await nc_client.cookbook.get_recipe(recipe_id)
|
|
assert retrieved["name"] == imported_recipe["name"]
|
|
logger.info(f"Verified imported recipe ID: {recipe_id}")
|
|
|
|
# Clean up
|
|
logger.info(f"Deleting imported recipe ID: {recipe_id}")
|
|
await nc_client.cookbook.delete_recipe(recipe_id)
|
|
logger.info("Successfully deleted imported recipe")
|
|
|
|
except HTTPStatusError as e:
|
|
if e.response.status_code == 409:
|
|
# Recipe already exists - this is acceptable in tests
|
|
logger.warning("Recipe already exists (409 conflict)")
|
|
pytest.skip("Recipe already exists in test environment")
|
|
elif e.response.status_code == 400:
|
|
# URL couldn't be imported
|
|
logger.error(
|
|
f"Failed to import recipe from nginx container: {test_url}. "
|
|
f"Status: {e.response.status_code}, Response: {e.response.text}"
|
|
)
|
|
raise
|
|
else:
|
|
raise
|
|
|
|
|
|
async def test_cookbook_search_recipes(nc_client: NextcloudClient):
|
|
"""Test searching for recipes."""
|
|
# Create a test recipe with unique keywords
|
|
unique_keyword = f"testkeyword{uuid.uuid4().hex[:8]}"
|
|
recipe_name = f"Test Recipe {uuid.uuid4().hex[:8]}"
|
|
recipe_data = {
|
|
"name": recipe_name,
|
|
"description": f"Recipe for testing search with {unique_keyword}",
|
|
"keywords": unique_keyword,
|
|
"recipeIngredient": ["test ingredient"],
|
|
"recipeInstructions": ["test instruction"],
|
|
}
|
|
|
|
logger.info(f"Creating recipe for search test with keyword: {unique_keyword}")
|
|
recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
|
|
|
|
try:
|
|
# Allow time for indexing
|
|
await asyncio.sleep(2)
|
|
|
|
# Search for the recipe
|
|
logger.info(f"Searching for recipes with keyword: {unique_keyword}")
|
|
search_results = await nc_client.cookbook.search_recipes(unique_keyword)
|
|
|
|
assert isinstance(search_results, list)
|
|
# Should find at least our recipe
|
|
assert len(search_results) > 0
|
|
|
|
# Verify our recipe is in the results
|
|
found = any(str(r.get("id")) == str(recipe_id) for r in search_results)
|
|
assert found, f"Recipe {recipe_id} not found in search results"
|
|
logger.info(f"Successfully found recipe {recipe_id} in search results")
|
|
|
|
finally:
|
|
# Clean up
|
|
logger.info(f"Deleting recipe ID: {recipe_id}")
|
|
await nc_client.cookbook.delete_recipe(recipe_id)
|
|
|
|
|
|
async def test_cookbook_list_categories(nc_client: NextcloudClient):
|
|
"""Test listing recipe categories."""
|
|
logger.info("Listing recipe categories")
|
|
categories = await nc_client.cookbook.list_categories()
|
|
|
|
assert isinstance(categories, list)
|
|
logger.info(f"Found {len(categories)} categories")
|
|
|
|
# Each category should have name and recipe_count
|
|
if categories:
|
|
assert "name" in categories[0]
|
|
assert "recipe_count" in categories[0]
|
|
|
|
|
|
async def test_cookbook_get_recipes_in_category(nc_client: NextcloudClient):
|
|
"""Test getting recipes in a specific category."""
|
|
# Create a recipe in a test category
|
|
unique_category = f"TestCategory{uuid.uuid4().hex[:8]}"
|
|
recipe_name = f"Test Recipe {uuid.uuid4().hex[:8]}"
|
|
recipe_data = {
|
|
"name": recipe_name,
|
|
"recipeCategory": unique_category,
|
|
"recipeIngredient": ["test"],
|
|
"recipeInstructions": ["test"],
|
|
}
|
|
|
|
logger.info(f"Creating recipe in category: {unique_category}")
|
|
recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
|
|
|
|
try:
|
|
# Allow time for indexing
|
|
await asyncio.sleep(2)
|
|
|
|
# Get recipes in this category
|
|
logger.info(f"Getting recipes in category: {unique_category}")
|
|
recipes_in_category = await nc_client.cookbook.get_recipes_in_category(
|
|
unique_category
|
|
)
|
|
|
|
assert isinstance(recipes_in_category, list)
|
|
assert len(recipes_in_category) > 0
|
|
|
|
# Verify our recipe is in the results
|
|
found = any(str(r.get("id")) == str(recipe_id) for r in recipes_in_category)
|
|
assert found, f"Recipe {recipe_id} not found in category {unique_category}"
|
|
logger.info(f"Successfully found recipe in category {unique_category}")
|
|
|
|
finally:
|
|
# Clean up
|
|
logger.info(f"Deleting recipe ID: {recipe_id}")
|
|
await nc_client.cookbook.delete_recipe(recipe_id)
|
|
|
|
|
|
async def test_cookbook_list_keywords(nc_client: NextcloudClient):
|
|
"""Test listing recipe keywords."""
|
|
logger.info("Listing recipe keywords")
|
|
keywords = await nc_client.cookbook.list_keywords()
|
|
|
|
assert isinstance(keywords, list)
|
|
logger.info(f"Found {len(keywords)} keywords")
|
|
|
|
# Each keyword should have name and recipe_count
|
|
if keywords:
|
|
assert "name" in keywords[0]
|
|
assert "recipe_count" in keywords[0]
|
|
|
|
|
|
async def test_cookbook_get_recipes_with_keywords(nc_client: NextcloudClient):
|
|
"""Test getting recipes with specific keywords.
|
|
|
|
Note: The keywords filtering may require exact keyword matches and sufficient
|
|
indexing time. This test uses a longer wait time."""
|
|
# Create a recipe with unique keywords
|
|
unique_keyword = f"testtag{uuid.uuid4().hex[:8]}"
|
|
recipe_name = f"Test Recipe {uuid.uuid4().hex[:8]}"
|
|
recipe_data = {
|
|
"name": recipe_name,
|
|
"keywords": f"{unique_keyword},integration",
|
|
"recipeIngredient": ["test"],
|
|
"recipeInstructions": ["test"],
|
|
}
|
|
|
|
logger.info(f"Creating recipe with keyword: {unique_keyword}")
|
|
recipe_id = await nc_client.cookbook.create_recipe(recipe_data)
|
|
|
|
try:
|
|
# Allow extra time for indexing
|
|
await asyncio.sleep(3)
|
|
|
|
# Trigger a reindex to ensure the recipe is indexed
|
|
await nc_client.cookbook.reindex()
|
|
await asyncio.sleep(2)
|
|
|
|
# Get recipes with this keyword
|
|
logger.info(f"Getting recipes with keyword: {unique_keyword}")
|
|
recipes_with_keywords = await nc_client.cookbook.get_recipes_with_keywords(
|
|
[unique_keyword]
|
|
)
|
|
|
|
assert isinstance(recipes_with_keywords, list)
|
|
# Keyword filtering might not find recipes immediately due to indexing
|
|
# Log the results for debugging
|
|
logger.info(
|
|
f"Found {len(recipes_with_keywords)} recipes with keyword {unique_keyword}"
|
|
)
|
|
|
|
if len(recipes_with_keywords) > 0:
|
|
# Verify our recipe is in the results if any are found
|
|
found = any(
|
|
str(r.get("id")) == str(recipe_id) for r in recipes_with_keywords
|
|
)
|
|
if found:
|
|
logger.info(f"Successfully found recipe with keyword {unique_keyword}")
|
|
else:
|
|
logger.warning(
|
|
f"Recipe {recipe_id} not in keyword results, but other recipes found"
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"No recipes found with keyword {unique_keyword} - may be indexing delay"
|
|
)
|
|
|
|
finally:
|
|
# Clean up
|
|
logger.info(f"Deleting recipe ID: {recipe_id}")
|
|
await nc_client.cookbook.delete_recipe(recipe_id)
|
|
|
|
|
|
async def test_cookbook_reindex(nc_client: NextcloudClient):
|
|
"""Test triggering a reindex of recipes."""
|
|
logger.info("Triggering recipe reindex")
|
|
result = await nc_client.cookbook.reindex()
|
|
|
|
# Should return a success message
|
|
assert isinstance(result, str)
|
|
logger.info(f"Reindex result: {result}")
|