"""
Improved Tests for Google Photos tools using FastMCP Client SDK.
This module provides comprehensive tests for all Google Photos MCP tools including both
standard and advanced optimized tools. This version fixes parameter mismatches and
removes problematic rate limiting tests.
"""
import asyncio
import glob
import json
import logging
import os
import re
from datetime import datetime
from typing import Any, Dict, Optional
import pytest
from dotenv import load_dotenv
from .base_test_config import TEST_EMAIL, create_test_client
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load environment variables from .env file
load_dotenv()
# Test email address from environment variable - use valid tokens
# Use specific email for Photos testing with actual photos data
PHOTO_TEST_EMAIL = os.getenv("PHOTO_TEST_EMAIL_ADDRESS", TEST_EMAIL)
# Test photo path pattern from environment variable
# Allow $USER or ~ in the path; expand env vars and user home
PHOTO_TEST_PATH_PATTERN = os.getenv(
"PHOTO_TEST_PATH_PATTERN", "/Users/$USER/Pictures/*.png"
)
PHOTO_TEST_PATH_PATTERN = os.path.expandvars(
os.path.expanduser(PHOTO_TEST_PATH_PATTERN)
)
def print_tool_result(tool_name: str, result: Any, extra_info: Optional[Dict] = None):
"""Helper function to print tool results in a consistent, visible format."""
# Extract content from result
if hasattr(result, "content"):
content = result.content[0].text if result.content else str(result)
elif hasattr(result, "__iter__") and not isinstance(result, str):
content = str(result[0]) if result else "No result"
else:
content = str(result)
# Print formatted output
print(f"\n{'='*70}")
print(f"š ļø TOOL EXECUTION: {tool_name}")
print(f"{'='*70}")
# Print any extra information
if extra_info:
for key, value in extra_info.items():
print(f"š {key}: {value}")
print(f"{'-'*70}")
# Print the result (truncate if too long)
if len(content) > 1000:
print("š Result (truncated to 1000 chars):")
print(f"{content[:1000]}...")
else:
print("š Result:")
print(content)
print(f"{'='*70}\n")
return content
def find_test_images():
"""Find test images for upload testing."""
# Try the environment variable pattern first
test_images = glob.glob(PHOTO_TEST_PATH_PATTERN)
if not test_images:
# Try specific known paths as fallback
fallback_paths = [
"/Users/$USER/Pictures/poor_air_quality.png",
"/Users/$USER/Pictures/*.png",
"/Users/$USER/Pictures/*.jpg",
"/Users/$USER/Pictures/*.jpeg",
os.path.expanduser("~/Pictures/*.png"),
os.path.expanduser("~/Pictures/*.jpg"),
]
for pattern in fallback_paths:
test_images = (
glob.glob(pattern)
if "*" in pattern
else ([pattern] if os.path.exists(pattern) else [])
)
if test_images:
break
# Filter to only valid image files
valid_extensions = {
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".webp",
".heic",
".heif",
}
test_images = [
img
for img in test_images
if os.path.splitext(img.lower())[1] in valid_extensions
]
return test_images
@pytest.mark.service("photos")
class TestPhotosTools:
"""Test both standard and advanced Google Photos MCP tools with comprehensive coverage."""
# Class attributes to share data between tests
created_album_id = None
created_album_name = None
found_media_item_ids = []
@pytest.mark.asyncio
@pytest.mark.order(1)
async def test_photos_tools_available(self, client):
"""Test that both standard and advanced Google Photos tools are available."""
tools = await client.list_tools()
tool_names = [tool.name for tool in tools]
# Standard Photos tools
expected_standard_tools = [
"list_photos_albums",
"search_photos",
"list_album_photos", # Note: this is the actual tool name, not get_album_photos
"get_photo_details",
"create_photos_album",
"get_photos_library_info",
]
# Advanced Photos tools
expected_advanced_tools = [
"photos_smart_search",
"photos_batch_details",
"photos_performance_stats",
"photos_optimized_album_sync",
"upload_photos",
"upload_folder_photos",
]
all_expected_tools = expected_standard_tools + expected_advanced_tools
missing_tools = []
found_tools = []
for tool_name in all_expected_tools:
if tool_name not in tool_names:
missing_tools.append(tool_name)
else:
found_tools.append(tool_name)
if missing_tools:
logger.warning(f"Missing Photos tools: {missing_tools}")
logger.info(f"Available tools: {sorted(tool_names)}")
# Verify that we found at least some Photos tools
assert len(found_tools) > 0, f"No Photos tools found in: {tool_names}"
logger.info(
f"Found {len(found_tools)}/{len(all_expected_tools)} Photos tools: {found_tools}"
)
print_tool_result(
"test_photos_tools_available",
f"Found {len(found_tools)} tools: Standard={len([t for t in found_tools if t in expected_standard_tools])}, Advanced={len([t for t in found_tools if t in expected_advanced_tools])}",
)
@pytest.mark.asyncio
@pytest.mark.order(2)
async def test_list_photos_albums(self, client):
"""Test listing photo albums."""
try:
result = await client.call_tool(
"list_photos_albums",
{"user_google_email": PHOTO_TEST_EMAIL, "max_results": 10},
)
content = print_tool_result("list_photos_albums", result)
# Should either succeed with albums or show appropriate message
success_indicators = [
"albums found",
"album",
"total",
"items",
"no albums",
"empty",
"successfully",
"retrieved",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
# Check if we got a meaningful response
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and "scope" in content.lower():
pytest.skip(f"Skipping due to scope/permission issue: {content}")
elif is_error and "unauthorized" in content.lower():
pytest.skip(f"Skipping due to auth issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
# If successful, try to extract album info for later tests
if is_success and "albums" in content.lower():
import re
# Try to extract first album ID
album_id_match = re.search(r'"id":\s*"([^"]+)"', content)
if not album_id_match:
album_id_match = re.search(r"ID:\s*([A-Za-z0-9_-]+)", content)
if album_id_match:
TestPhotosTools.created_album_id = album_id_match.group(1)
print(
f"ā
Found existing album ID for other tests: {TestPhotosTools.created_album_id}"
)
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"list_photos_albums failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(3)
async def test_search_photos(self, client):
"""Test searching for photos."""
try:
result = await client.call_tool(
"search_photos",
{
"user_google_email": PHOTO_TEST_EMAIL,
"content_categories": ["PEOPLE", "LANDSCAPES"],
"max_results": 5,
},
)
content = print_tool_result("search_photos", result)
# Should either succeed with photos or show appropriate message
success_indicators = [
"photos found",
"media items",
"search",
"results",
"no photos",
"empty",
"successfully",
"retrieved",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
# Extract media IDs for later tests
if is_success:
import re
media_ids = re.findall(r'"id":\s*"([^"]+)"', content)
if media_ids:
TestPhotosTools.found_media_item_ids = media_ids[:5]
print(
f"ā
Stored {len(TestPhotosTools.found_media_item_ids)} media item IDs for other tests"
)
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"search_photos failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(4)
async def test_get_photos_library_info(self, client):
"""Test getting library information."""
try:
result = await client.call_tool(
"get_photos_library_info", {"user_google_email": PHOTO_TEST_EMAIL}
)
content = print_tool_result("get_photos_library_info", result)
# Should either succeed with library info or show appropriate message
success_indicators = [
"library",
"storage",
"info",
"details",
"successfully",
"retrieved",
"account",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"get_photos_library_info failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(5)
async def test_create_photos_album(self, client):
"""Test creating a photo album."""
try:
# Create timestamp for unique album name
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
album_title = f"Test Album {timestamp}"
result = await client.call_tool(
"create_photos_album",
{"user_google_email": PHOTO_TEST_EMAIL, "title": album_title},
)
content = print_tool_result(
"create_photos_album", result, {"Album Title": album_title}
)
# Should either succeed with album creation or show appropriate message
success_indicators = [
"album created",
"successfully",
"created",
"album",
"id",
"shareableUrl",
album_title.lower(),
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
# If successful, extract and store the album ID for use in other tests
if is_success:
import re
album_id_match = re.search(r"ID:\s*([A-Za-z0-9_-]+)", content)
if not album_id_match:
album_id_match = re.search(r'"id":\s*"([^"]+)"', content)
if album_id_match:
TestPhotosTools.created_album_id = album_id_match.group(1)
TestPhotosTools.created_album_name = album_title
print(
f"ā
Stored album ID for other tests: {TestPhotosTools.created_album_id}"
)
# Add pause to prevent rate limiting
await asyncio.sleep(1)
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"create_photos_album failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(6)
async def test_list_album_photos(self, client):
"""Test getting photos from an album."""
try:
album_id = TestPhotosTools.created_album_id
if album_id:
result = await client.call_tool(
"list_album_photos",
{
"user_google_email": PHOTO_TEST_EMAIL,
"album_id": album_id,
"max_results": 5,
},
)
content = print_tool_result(
"list_album_photos", result, {"Album ID": album_id}
)
# Should either succeed with photos or show appropriate message
success_indicators = [
"photos found",
"media items",
"album",
"photos",
"no photos",
"empty",
"successfully",
"retrieved",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
"not found",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert (
is_success or is_error
), f"Unexpected response format: {content}"
else:
pytest.skip("No album ID available to test list_album_photos")
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"list_album_photos failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(7)
async def test_get_photo_details(self, client):
"""Test getting details of a specific photo."""
try:
# Use media IDs from previous tests if available
if TestPhotosTools.found_media_item_ids:
media_item_id = TestPhotosTools.found_media_item_ids[0]
print(f"š Using media ID from previous tests: {media_item_id}")
else:
# Try to get some media IDs via search
search_result = await client.call_tool(
"search_photos",
{"user_google_email": PHOTO_TEST_EMAIL, "max_results": 1},
)
search_content = print_tool_result("search_photos", search_result)
# Extract media item ID
import re
media_ids = re.findall(r'"id":\s*"([^"]+)"', search_content)
media_item_id = media_ids[0] if media_ids else None
if media_item_id:
result = await client.call_tool(
"get_photo_details",
{
"user_google_email": PHOTO_TEST_EMAIL,
"media_item_id": media_item_id,
},
)
content = print_tool_result(
"get_photo_details", result, {"Media Item ID": media_item_id}
)
# Should either succeed with photo details or show appropriate message
success_indicators = [
"media item",
"photo",
"details",
"metadata",
"filename",
"mime",
"successfully",
"retrieved",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
"not found",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert (
is_success or is_error
), f"Unexpected response format: {content}"
else:
pytest.skip("No media item ID found to test get_photo_details")
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"get_photo_details failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(8)
async def test_photos_smart_search(self, client):
"""Test smart photo search with filtering."""
try:
result = await client.call_tool(
"photos_smart_search",
{
"user_google_email": PHOTO_TEST_EMAIL,
"include_photos": True,
"include_videos": False,
"max_results": 10,
},
)
content = print_tool_result("photos_smart_search", result)
# Parse the JSON response - tool returns structured PhotosSmartSearchResponse
try:
data = json.loads(content)
except json.JSONDecodeError:
# If not valid JSON, treat as string response
data = {"text_response": content}
# Check for error in structured response
if "error" in data and data["error"]:
error_msg = data["error"].lower()
if (
"scope" in error_msg
or "unauthorized" in error_msg
or "permission" in error_msg
):
pytest.skip(
f"Skipping due to scope/permission issue: {data['error']}"
)
else:
# Some errors are acceptable (e.g., no results found)
print(f"ā ļø Tool returned error: {data['error']}")
# Store found media items for later tests from structured response
if "media_items" in data and data["media_items"]:
media_ids = [
item.get("id") for item in data["media_items"] if item.get("id")
]
if media_ids:
TestPhotosTools.found_media_item_ids = media_ids[:5]
print(
f"ā
Stored {len(TestPhotosTools.found_media_item_ids)} media item IDs for batch test"
)
# Validate structured response fields (PhotosSmartSearchResponse)
expected_fields = [
"media_items",
"total_found",
"search_time_seconds",
"user_email",
]
has_structured_response = any(field in data for field in expected_fields)
if has_structured_response:
# Validate the structured response
print("š Search Results:")
print(f" - Total found: {data.get('total_found', 'N/A')}")
print(f" - Search time: {data.get('search_time_seconds', 'N/A')}s")
print(f" - User email: {data.get('user_email', 'N/A')}")
print(f" - Filters applied: {data.get('filters_applied', 'N/A')}")
if data.get("text_summary"):
print(f" - Summary: {data['text_summary']}")
# Assert basic structure is valid
assert (
"media_items" in data or "error" in data
), "Response should have media_items or error"
if "media_items" in data:
assert isinstance(
data["media_items"], list
), "media_items should be a list"
else:
# Fallback to string-based validation for non-structured responses
success_indicators = ["smart search", "results", "found", "photos"]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
assert is_success, f"Unexpected response format: {content}"
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"photos_smart_search failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(9)
async def test_photos_batch_details(self, client):
"""Test getting batch photo details with optimization."""
try:
# Use media IDs from previous test if available
if TestPhotosTools.found_media_item_ids:
test_ids = TestPhotosTools.found_media_item_ids[:3]
print(f"š Using media IDs from previous tests: {test_ids}")
else:
# If no stored IDs, try to get some via smart search
search_result = await client.call_tool(
"photos_smart_search",
{"user_google_email": PHOTO_TEST_EMAIL, "max_results": 3},
)
search_content = print_tool_result(
"photos_smart_search (for IDs)", search_result
)
# Parse the JSON response to get media item IDs
try:
search_data = json.loads(search_content)
if "media_items" in search_data and search_data["media_items"]:
test_ids = [
item.get("id")
for item in search_data["media_items"]
if item.get("id")
][:3]
else:
test_ids = None
except json.JSONDecodeError:
# Fallback to regex extraction
media_ids = re.findall(r'"id":\s*"([^"]+)"', search_content)
test_ids = media_ids[:3] if media_ids else None
if test_ids:
result = await client.call_tool(
"photos_batch_details",
{"user_google_email": PHOTO_TEST_EMAIL, "media_item_ids": test_ids},
)
content = print_tool_result(
"photos_batch_details", result, {"Media IDs": test_ids}
)
# Parse the JSON response - tool returns structured PhotosBatchDetailsResponse
try:
data = json.loads(content)
except json.JSONDecodeError:
data = {"text_response": content}
# Check for error in structured response
if "error" in data and data["error"]:
error_msg = data["error"].lower()
if (
"scope" in error_msg
or "unauthorized" in error_msg
or "permission" in error_msg
):
pytest.skip(
f"Skipping due to scope/permission issue: {data['error']}"
)
else:
print(f"ā ļø Tool returned error: {data['error']}")
# Validate structured response fields (PhotosBatchDetailsResponse)
expected_fields = [
"successful_items",
"failed_items",
"total_requested",
"successful_count",
]
has_structured_response = any(
field in data for field in expected_fields
)
if has_structured_response:
# Validate the structured response
print("š Batch Details Results:")
print(f" - Total requested: {data.get('total_requested', 'N/A')}")
print(
f" - Successful count: {data.get('successful_count', 'N/A')}"
)
print(f" - Failed count: {data.get('failed_count', 'N/A')}")
print(
f" - Processing time: {data.get('processing_time_seconds', 'N/A')}s"
)
if data.get("text_summary"):
print(f" - Summary: {data['text_summary']}")
# Assert basic structure is valid
assert (
"successful_items" in data or "error" in data
), "Response should have successful_items or error"
if "successful_items" in data:
assert isinstance(
data["successful_items"], list
), "successful_items should be a list"
else:
# Fallback to string-based validation
success_indicators = [
"bulk",
"details",
"batch",
"processed",
"retrieved",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
assert (
is_success or "error" in content.lower()
), f"Unexpected response format: {content}"
else:
pytest.skip("No media item IDs found to test photos_batch_details")
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"photos_batch_details failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(10)
async def test_photos_performance_stats(self, client):
"""Test getting performance statistics for Photos API usage."""
try:
result = await client.call_tool(
"photos_performance_stats",
{"user_google_email": PHOTO_TEST_EMAIL, "clear_cache": False},
)
content = print_tool_result("photos_performance_stats", result)
# Should either succeed with performance stats or show appropriate message
success_indicators = [
"performance",
"stats",
"cache",
"api",
"usage",
"requests",
"successfully",
"statistics",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"photos_performance_stats failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(11)
async def test_upload_photos_with_album_creation(self, client):
"""Test uploading photos and creating an album simultaneously."""
try:
# Find test images using helper function
test_images = find_test_images()
if not test_images:
pytest.skip(f"No test images found at {PHOTO_TEST_PATH_PATTERN}")
# Create timestamp for unique album name
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
album_name = f"Test Album {timestamp}"
# Use first available image
test_image = test_images[0]
result = await client.call_tool(
"upload_photos",
{
"user_google_email": PHOTO_TEST_EMAIL,
"file_paths": test_image,
"create_album": album_name,
"description": "Test photo upload with album creation",
},
)
content = print_tool_result(
"upload_photos",
result,
{"Album Name": album_name, "File": os.path.basename(test_image)},
)
# Check for success indicators
success_indicators = [
"successful",
"uploaded",
"photo upload",
"album created",
"media_item_id",
"google photos id",
"ā
",
"šø",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
# If successful, try to extract album ID for other tests
if is_success:
# Try to extract album ID from response
import re
album_id_match = re.search(
r'album.*?id["\']?\s*[:=]\s*["\']?([A-Za-z0-9_-]+)',
content,
re.IGNORECASE,
)
if album_id_match:
TestPhotosTools.created_album_id = album_id_match.group(1)
TestPhotosTools.created_album_name = album_name
print(
f"ā
Stored album ID for other tests: {TestPhotosTools.created_album_id}"
)
# Add pause to prevent rate limiting
await asyncio.sleep(1)
except FileNotFoundError as e:
pytest.skip(f"Test images not found: {e}")
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"upload_photos_with_album_creation failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(12) # Run after album creation
async def test_photos_optimized_album_sync(self, client):
"""Test optimized album sync functionality."""
try:
# Use album ID from previous test if available
if TestPhotosTools.created_album_id:
album_id = TestPhotosTools.created_album_id
print(f"š Using album ID from previous test: {album_id}")
result = await client.call_tool(
"photos_optimized_album_sync",
{
"user_google_email": PHOTO_TEST_EMAIL,
"album_id": album_id,
"analyze_metadata": True,
"max_items": 100,
},
)
content = print_tool_result(
"photos_optimized_album_sync", result, {"Album ID": album_id}
)
# Should either succeed with sync results or show appropriate message
success_indicators = [
"sync",
"optimized",
"album",
"analyzed",
"metadata",
"results",
"successfully",
"items",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert (
is_success or is_error
), f"Unexpected response format: {content}"
else:
pytest.skip(
"Skipping photos_optimized_album_sync - no album_id available from previous tests"
)
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"photos_optimized_album_sync failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(13)
async def test_upload_photos(self, client):
"""Test uploading photos to Google Photos."""
try:
# Find test images using helper function
test_images = find_test_images()
if not test_images:
pytest.skip(f"No test images found at {PHOTO_TEST_PATH_PATTERN}")
# Test single photo upload
single_image = test_images[0]
result = await client.call_tool(
"upload_photos",
{
"user_google_email": PHOTO_TEST_EMAIL,
"file_paths": single_image,
"description": "Test single photo upload from pytest",
},
)
content = print_tool_result(
"upload_photos", result, {"File": os.path.basename(single_image)}
)
# Check for success indicators
success_indicators = [
"successful",
"uploaded",
"photo upload",
"media_item_id",
"google photos id",
"ā
",
"šø",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
else:
assert is_success or is_error, f"Unexpected response format: {content}"
except FileNotFoundError as e:
pytest.skip(f"Test images not found: {e}")
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"upload_photos failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
@pytest.mark.order(14)
async def test_upload_folder_photos(self, client):
"""Test uploading all photos from a folder."""
try:
# Get the folder path from the pattern
folder_path = os.path.dirname(PHOTO_TEST_PATH_PATTERN.rstrip("/*"))
if not os.path.exists(folder_path):
pytest.skip(f"Test folder not found: {folder_path}")
# Create a unique album name for this test
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
album_name = f"Folder Upload Test {timestamp}"
result = await client.call_tool(
"upload_folder_photos",
{
"user_google_email": PHOTO_TEST_EMAIL,
"folder_path": folder_path,
"recursive": False, # Don't recurse for test
"create_album": album_name,
},
)
content = print_tool_result(
"upload_folder_photos",
result,
{"Folder": folder_path, "Album": album_name},
)
# Check for success indicators
success_indicators = [
"folder photo upload",
"successfully uploaded",
"total photos",
"success rate",
"ā
",
"š",
"summary",
]
error_indicators = [
"error",
"failed",
"unauthorized",
"permission",
"scope",
"no image files found",
]
is_success = any(
indicator in content.lower() for indicator in success_indicators
)
is_error = any(
indicator in content.lower() for indicator in error_indicators
)
if is_error and (
"scope" in content.lower() or "unauthorized" in content.lower()
):
pytest.skip(f"Skipping due to scope/permission issue: {content}")
elif "no image files found" in content.lower():
pytest.skip(f"No images found in folder: {content}")
else:
# Either success or acceptable error
assert is_success or is_error, f"Unexpected response format: {content}"
except FileNotFoundError as e:
pytest.skip(f"Test folder not found: {e}")
except Exception as e:
if "permission" in str(e).lower() or "scope" in str(e).lower():
pytest.skip(f"Skipping due to permissions: {e}")
else:
logger.error(f"upload_folder_photos failed: {e}")
assert False, f"Tool execution failed: {e}"
@pytest.mark.asyncio
async def test_debug_single_photo_tool():
"""DEBUG TOOL: Test a single Photos tool with maximum debugging output."""
print(f"\n{'='*80}")
print("š SINGLE PHOTOS TOOL DEBUG SESSION - MAXIMUM VERBOSITY")
print(f"{'='*80}")
# Create client
client = await create_test_client(PHOTO_TEST_EMAIL)
async with client:
# Test photos_performance_stats as it's the simplest advanced tool
test_payload = {"user_google_email": PHOTO_TEST_EMAIL, "clear_cache": False}
print(f"š§ Test Email: {PHOTO_TEST_EMAIL}")
print("š Test Payload:")
print(json.dumps(test_payload, indent=2))
print(f"{'='*80}\n")
try:
# Get list of available tools first
tools = await client.list_tools()
tool_names = [tool.name for tool in tools]
photos_tools = [
tool
for tool in tool_names
if "photos" in tool or "album" in tool or "upload" in tool
]
print(f"š Available Photos-related tools: {photos_tools}")
if "photos_performance_stats" in tool_names:
print("\nšÆ Testing photos_performance_stats...")
result = await client.call_tool(
"photos_performance_stats", test_payload
)
# Extract content from result
if hasattr(result, "content"):
content = result.content[0].text if result.content else str(result)
elif hasattr(result, "__iter__") and not isinstance(result, str):
content = str(result[0]) if result else "No result"
else:
content = str(result)
print(f"\n{'='*60}")
print("š§ DEBUG: photos_performance_stats")
print(f"{'='*60}")
print(f"Response type: {type(result)}")
print(f"Content length: {len(content)} chars")
print(f"{'='*60}")
print("Full Response:")
print(f"{'-'*60}")
print(content)
print(f"{'='*60}\n")
# Basic validation
assert content is not None, "Response content should not be None"
assert (
len(content.strip()) > 0
), f"Response should not be empty, got: '{content}'"
logger.info(f"Debug Photos tool result: {content}")
else:
print("ā photos_performance_stats not found in available tools")
print(f"š All available tools: {sorted(tool_names)}")
except Exception as e:
print(f"ā Debug test failed: {e}")
logger.error(f"Debug single photos tool failed: {e}")
if __name__ == "__main__":
pytest.main(["-xvs", __file__])