"""
Tests for the send_dynamic_card tool using FastMCP Client SDK.
This module specifically tests the send_dynamic_card tool with different
variations of card types and parameters.
"""
"""Test dynamic card sending functionality with AI-powered generation.
🔧 MCP Tools Used:
- send_dynamic_card: Send AI-generated cards from natural language descriptions
- Card generation pipeline: Convert descriptions to structured cards
- Parameter extraction: Extract card parameters from user input
- Card validation: Validate generated card structures
🧪 What's Being Tested:
- Dynamic card generation from user descriptions
- AI-powered card component selection and configuration
- Parameter parsing and validation
- Card sending workflow integration
- Error handling for invalid descriptions
- Generated card quality and user experience
- Integration with Chat spaces and threading
🔍 Potential Duplications:
- Overlaps significantly with test_nlp_card_parser.py (both test NLP card generation)
- Card sending overlaps with basic Chat tools tests
- AI generation patterns might be similar to other AI-powered tools
- Parameter extraction might overlap with other input parsing tests
"""
import asyncio
import json
import logging
import os
from datetime import datetime
import pytest
from dotenv import load_dotenv
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load environment variables from .env file
load_dotenv()
# Server configuration from environment variables with defaults
SERVER_HOST = os.getenv("MCP_SERVER_HOST", "localhost")
SERVER_PORT = os.getenv("MCP_SERVER_PORT", os.getenv("SERVER_PORT", "8002"))
# FastMCP servers in HTTP mode use the /mcp/ endpoint
SERVER_URL = os.getenv("MCP_SERVER_URL", f"http://{SERVER_HOST}:{SERVER_PORT}/mcp/")
# Test email address from environment variable - using your configured variable
TEST_EMAIL = os.getenv("TEST_CHAT_WEBHOOK_EMAIL", "srivers@groupon.com")
# Test webhook URLs for Google Chat
TEST_CHAT_WEBHOOK = os.getenv("TEST_CHAT_WEBHOOK", "")
TEST_WEBHOOK_URL = os.getenv(
"TEST_CHAT_WEBHOOK_URL",
TEST_CHAT_WEBHOOK
or "https://chat.googleapis.com/v1/spaces/AAAAAAAAAAA/messages?key=test&token=test",
)
def extract_space_id_from_webhook(webhook_url: str) -> str:
"""Extract the space ID from a Google Chat webhook URL."""
import re
# Match pattern: https://chat.googleapis.com/v1/spaces/{SPACE_ID}/messages
match = re.search(r"/spaces/([^/]+)/", webhook_url)
if match:
return f"spaces/{match.group(1)}"
else:
# Fallback to default test space
return "spaces/test"
# Extract the actual space ID from webhook URL
TEST_SPACE_ID = extract_space_id_from_webhook(TEST_WEBHOOK_URL)
class TestSendDynamicCard:
"""Test the send_dynamic_card tool with different card variations."""
# Use standardized client fixture from conftest.py
@pytest.mark.asyncio
async def test_send_dynamic_card_tool_available(self, client):
"""Test that the send_dynamic_card tool is available."""
tools = await client.list_tools()
tool_names = [tool.name for tool in tools]
assert (
"send_dynamic_card" in tool_names
), "Tool 'send_dynamic_card' not found in available tools"
@pytest.mark.asyncio
async def test_send_simple_card(self, client):
"""Test sending a simple card with basic header and text."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send a simple card
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "simple card with header and text",
"card_params": {
"header": {
"title": f"Simple Card Test ({timestamp})",
"subtitle": "Created for testing send_dynamic_card",
},
"text": "This is a simple card with just a header and text.",
},
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Handle both old list format and new CallToolResult format
if hasattr(result, "content"):
# New FastMCP format - result is CallToolResult with content list
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
# Old format - result is a list
assert len(result) > 0
content = result[0].text
else:
# Direct content
content = str(result)
# Should either succeed or return a meaningful error
valid_responses = [
"sent",
"successfully",
"webhook",
"status",
"❌",
"error",
"failed",
]
assert any(
keyword in content.lower() for keyword in valid_responses
), f"Response didn't match any expected pattern: {content}"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
else:
# If successful without explicit status, should contain "sent successfully"
if "❌" not in content and "error" not in content.lower():
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully"
@pytest.mark.asyncio
async def test_send_interactive_card(self, client):
"""Test sending an interactive card with buttons."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send an interactive card
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "interactive card with buttons",
"card_params": {
"header": {
"title": f"Interactive Card Test ({timestamp})",
"subtitle": "Created for testing send_dynamic_card",
"imageUrl": "https://picsum.photos/200/100",
},
"sections": [
{
"header": "Interactive Section",
"widgets": [
{
"textParagraph": {
"text": f"This interactive card was created at {timestamp}"
}
},
{
"buttonList": {
"buttons": [
{
"text": "Visit Google",
"onClick": {
"openLink": {
"url": "https://www.google.com"
}
},
},
{
"text": "Visit Documentation",
"onClick": {
"openLink": {
"url": "https://developers.google.com/chat/ui/widgets/button-list"
}
},
},
]
}
},
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Handle both old list format and new CallToolResult format
if hasattr(result, "content"):
# New FastMCP format - result is CallToolResult with content list
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
# Old format - result is a list
assert len(result) > 0
content = result[0].text
else:
# Direct content
content = str(result)
# Should either succeed or return a meaningful error
valid_responses = [
"sent",
"successfully",
"webhook",
"status",
"❌",
"error",
"failed",
]
assert any(
keyword in content.lower() for keyword in valid_responses
), f"Response didn't match any expected pattern: {content}"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
else:
# If successful without explicit status, should contain "sent successfully"
if "❌" not in content and "error" not in content.lower():
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully"
@pytest.mark.asyncio
async def test_send_card_with_image(self, client):
"""Test sending a card with an image."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send a card with an image
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with image",
"card_params": {
"header": {
"title": f"Image Card Test ({timestamp})",
"subtitle": "Created for testing send_dynamic_card",
},
"sections": [
{
"widgets": [
{
"textParagraph": {
"text": "This card contains an image."
}
},
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"altText": "Google Workspace Logo",
}
},
]
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Handle both old list format and new CallToolResult format
if hasattr(result, "content"):
# New FastMCP format - result is CallToolResult with content list
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
# Old format - result is a list
assert len(result) > 0
content = result[0].text
else:
# Direct content
content = str(result)
# Should either succeed or return a meaningful error
valid_responses = [
"sent",
"successfully",
"webhook",
"status",
"❌",
"error",
"failed",
]
assert any(
keyword in content.lower() for keyword in valid_responses
), f"Response didn't match any expected pattern: {content}"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
else:
# If successful without explicit status, should contain "sent successfully"
if "❌" not in content and "error" not in content.lower():
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully"
@pytest.mark.asyncio
async def test_send_form_card(self, client):
"""Test sending a form card with input fields."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send a form card
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "form card with input fields",
"card_params": {
"header": {
"title": f"Form Card Test ({timestamp})",
"subtitle": "Created for testing send_dynamic_card",
},
"sections": [
{
"header": "Form Section",
"widgets": [
{
"textParagraph": {
"text": "Please fill out this form:"
}
},
{
"textInput": {
"label": "Name",
"name": "name",
"value": "",
"hintText": "Enter your name",
}
},
{
"textInput": {
"label": "Email",
"name": "email",
"value": "",
"hintText": "Enter your email",
}
},
{
"selectionInput": {
"name": "department",
"label": "Department",
"type": "DROPDOWN",
"items": [
{
"text": "Engineering",
"value": "engineering",
"selected": True,
},
{
"text": "Marketing",
"value": "marketing",
"selected": False,
},
{
"text": "Sales",
"value": "sales",
"selected": False,
},
],
}
},
{
"buttonList": {
"buttons": [
{
"text": "Submit",
"onClick": {
"openLink": {
"url": "https://example.com/submit"
}
},
}
]
}
},
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Handle both old list format and new CallToolResult format
if hasattr(result, "content"):
# New FastMCP format - result is CallToolResult with content list
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
# Old format - result is a list
assert len(result) > 0
content = result[0].text
else:
# Direct content
content = str(result)
# Should either succeed or return a meaningful error
valid_responses = [
"sent",
"successfully",
"webhook",
"status",
"❌",
"error",
"failed",
]
assert any(
keyword in content.lower() for keyword in valid_responses
), f"Response didn't match any expected pattern: {content}"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
else:
# If successful without explicit status, should contain "sent successfully"
if "❌" not in content and "error" not in content.lower():
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully"
@pytest.mark.asyncio
async def test_send_card_with_natural_language_description(self, client):
"""Test sending a card using only natural language description."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send a card using natural language description
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "Create a card with a header, an image of a cat, and a button to visit a website",
"card_params": {
"header": {
"title": f"Natural Language Card ({timestamp})",
"subtitle": "Created using natural language description",
}
},
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Handle both old list format and new CallToolResult format
if hasattr(result, "content"):
# New FastMCP format - result is CallToolResult with content list
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
# Old format - result is a list
assert len(result) > 0
content = result[0].text
else:
# Direct content
content = str(result)
# Should either succeed or return a meaningful error
valid_responses = [
"sent",
"successfully",
"webhook",
"status",
"❌",
"error",
"failed",
]
assert any(
keyword in content.lower() for keyword in valid_responses
), f"Response didn't match any expected pattern: {content}"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
else:
# If successful without explicit status, should contain "sent successfully"
if "❌" not in content and "error" not in content.lower():
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully"
@pytest.mark.asyncio
async def test_send_card_with_complex_layout(self, client):
"""Test sending a card with a complex layout including columns and multiple sections."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send a card with complex layout
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "complex card with multiple sections and columns",
"card_params": {
"header": {
"title": f"Complex Layout Card ({timestamp})",
"subtitle": "Created for testing send_dynamic_card",
"imageUrl": "https://picsum.photos/200/100",
"imageType": "CIRCLE",
},
"sections": [
{
"header": "Section 1",
"widgets": [
{
"textParagraph": {
"text": "This is the first section with text."
}
}
],
},
{
"header": "Section 2",
"widgets": [
{
"columns": {
"columnItems": [
{
"horizontalAlignment": "CENTER",
"widgets": [
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
"altText": "Contact",
}
},
{
"textParagraph": {
"text": "Column 1"
}
},
],
},
{
"horizontalAlignment": "CENTER",
"widgets": [
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/product/2x/gmail_48dp.png",
"altText": "Gmail",
}
},
{
"textParagraph": {
"text": "Column 2"
}
},
],
},
]
}
}
],
},
{
"header": "Section 3",
"widgets": [
{
"buttonList": {
"buttons": [
{
"text": "Action 1",
"onClick": {
"openLink": {
"url": "https://example.com/action1"
}
},
},
{
"text": "Action 2",
"onClick": {
"openLink": {
"url": "https://example.com/action2"
}
},
},
]
}
}
],
},
],
},
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Handle both old list format and new CallToolResult format
if hasattr(result, "content"):
# New FastMCP format - result is CallToolResult with content list
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
# Old format - result is a list
assert len(result) > 0
content = result[0].text
else:
# Direct content
content = str(result)
# Should either succeed or return a meaningful error
valid_responses = [
"sent",
"successfully",
"webhook",
"status",
"❌",
"error",
"failed",
]
assert any(
keyword in content.lower() for keyword in valid_responses
), f"Response didn't match any expected pattern: {content}"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
else:
# If successful without explicit status, should contain "sent successfully"
if "❌" not in content and "error" not in content.lower():
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully"
@pytest.mark.asyncio
async def test_send_minimal_card(self, client):
"""Test sending the most basic card possible - just title and text."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Very simple test payload - just basic title and text
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "simple card", # Very basic description
"card_params": {
"title": f"Minimal Test ({timestamp})",
"text": "Hello from minimal test card!",
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🧪 MINIMAL CARD TEST - BASIC DEBUGGING")
print(f"{'='*60}")
print(f"📧 Test Email: {TEST_EMAIL}")
print(f"🔗 Webhook URL: {TEST_WEBHOOK_URL}")
print("📋 Minimal Payload:")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the minimal card
result = await client.call_tool("send_dynamic_card", test_payload)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== MINIMAL TEST RESPONSE ===")
print(f"Response type: {type(result)}")
print(f"Response content: '{content}'")
print(f"Content length: {len(content)} chars")
print("=== END MINIMAL TEST ===\n")
# Basic validation - just check we got some response
assert content is not None, "Response content should not be None"
assert (
len(content.strip()) > 0
), f"Response should not be empty, got: '{content}'"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("✅ SUCCESS: Card sent successfully")
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("⚠️ RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Card sent successfully")
elif "webhook" in content.lower() and "status" in content.lower():
print("ℹ️ INFO: Got webhook response with status info")
else:
print(f"❓ UNKNOWN: Response content: '{content}'")
logger.info(f"Minimal card test result: {content}")
@pytest.mark.asyncio
async def test_send_card_with_button_and_image(self, client):
"""Test sending a card with both a button and an image."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Card with button and image
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with button and image",
"card_params": {
"title": f"Button + Image Test ({timestamp})",
"text": "This card has both a button and an image.",
"image_url": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"buttons": [
{"text": "Click Me", "onclick_action": "https://example.com"}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🖼️ BUTTON + IMAGE TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the button + image card
result = await client.call_tool("send_dynamic_card", test_payload)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== BUTTON + IMAGE TEST RESPONSE ===")
print(f"Response: '{content}'")
print(f"Content length: {len(content)} chars")
# Check if it's blank/empty
if len(content.strip()) == 0:
print("❌ BLANK MESSAGE DETECTED!")
print("This might be where the blank message issue occurs")
print("=== END BUTTON + IMAGE TEST ===\n")
# Check response
assert content is not None
assert (
len(content.strip()) > 0
), f"Response should not be blank, got: '{content}'"
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("✅ SUCCESS: Button + image card sent successfully")
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("⚠️ RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Button + image card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Button + image card test result: {content}")
@pytest.mark.asyncio
async def test_send_basic_button_card(self, client):
"""Test sending a basic card with one simple button."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Basic card with one simple button
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "simple card with one button",
"card_params": {
"title": f"Basic Button Test ({timestamp})",
"text": "This card has one simple button.",
"buttons": [
{"text": "Click Me", "onclick_action": "https://example.com"}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🔘 BASIC BUTTON TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the basic button card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== BASIC BUTTON TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END BASIC BUTTON TEST ===\n")
# Check response
assert content is not None
assert len(content.strip()) > 0
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("✅ SUCCESS: Basic button card sent successfully")
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("⚠️ RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Basic button card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Basic button card test result: {content}")
@pytest.mark.asyncio
async def test_send_card_with_advanced_buttons(self, client):
"""Test sending a card with advanced Google Chat Cards v2 button features."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# SIMPLIFIED advanced button test - reduce complexity
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with styled buttons", # Simpler description
"card_params": {
"title": f"Button Styles Test ({timestamp})",
"text": "Testing different button styles",
"buttons": [
{
"text": "Filled Button",
"type": "FILLED",
"onclick_action": "https://example.com/filled",
},
{
"text": "Outlined Button",
"type": "OUTLINED",
"onclick_action": "https://example.com/outlined",
},
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🎨 SIMPLIFIED ADVANCED BUTTON TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the advanced button card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== ADVANCED BUTTON TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END ADVANCED BUTTON TEST ===\n")
# Basic validation
assert content is not None
assert len(content.strip()) > 0
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
# Success case - should contain "sent successfully"
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("✅ SUCCESS: Advanced button card sent successfully")
elif "status: 429" in content:
# Rate limiting is acceptable (indicates correct formatting)
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("⚠️ RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
# 4xx errors should fail the test
print(f"❌ 400 Error - API format issue: {content}")
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
# 5xx errors should fail the test
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
# Generic webhook failure should fail the test
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Advanced button card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Simplified advanced buttons test result: {content}")
@pytest.mark.asyncio
async def test_all_button_types(self, client):
"""Test all supported Google Chat button types."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test all supported button types
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card testing all button types",
"card_params": {
"title": f"All Button Types Test ({timestamp})",
"text": "Testing all supported Google Chat button types",
"buttons": [
{
"text": "Filled",
"type": "FILLED",
"onclick_action": "https://example.com/filled",
},
{
"text": "Filled Tonal",
"type": "FILLED_TONAL",
"onclick_action": "https://example.com/filled-tonal",
},
{
"text": "Outlined",
"type": "OUTLINED",
"onclick_action": "https://example.com/outlined",
},
{
"text": "Borderless",
"type": "BORDERLESS",
"onclick_action": "https://example.com/borderless",
},
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🎨 ALL BUTTON TYPES TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the all button types card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== ALL BUTTON TYPES TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END ALL BUTTON TYPES TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
# CRITICAL: Ensure we got a 200 response code
if "status: 200" in content:
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("✅ SUCCESS: All button types card sent successfully")
elif "status: 429" in content:
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("⚠️ RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
pytest.fail(f"Card formatting error (4xx status): {content}")
elif "status: 5" in content:
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: All button types card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"All button types test result: {content}")
@pytest.mark.asyncio
async def test_error_boundary_empty_card(self, client):
"""Test error handling for completely empty card params."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Test completely empty card params
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "empty card test",
"card_params": {}, # Completely empty
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🚫 EMPTY CARD ERROR BOUNDARY TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the empty card
result = await client.call_tool("send_dynamic_card", test_payload)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== EMPTY CARD ERROR BOUNDARY RESPONSE ===")
print(f"Response: '{content}'")
print("=== END EMPTY CARD ERROR BOUNDARY ===\n")
# Should either prevent sending or handle gracefully
assert content is not None
assert len(content.strip()) > 0
# Should either succeed with fallback or show validation error
if "blank message prevention" in content.lower():
print("✅ SUCCESS: Pre-send validation caught empty card")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Empty card handled with fallback content")
elif "validation" in content.lower() or "error" in content.lower():
print("✅ SUCCESS: Proper error handling for empty card")
else:
print(f"❓ Response: {content}")
logger.info(f"Empty card error boundary test result: {content}")
@pytest.mark.asyncio
async def test_error_boundary_malformed_buttons(self, client):
"""Test error handling for malformed button configurations."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test malformed buttons
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with malformed buttons",
"card_params": {
"title": f"Malformed Buttons Test ({timestamp})",
"text": "Testing malformed button handling",
"buttons": [
{
"text": "Valid Button",
"onclick_action": "https://example.com/valid",
},
{
# Missing required text field
"onclick_action": "https://example.com/missing-text"
},
{
"text": "Invalid Type",
"type": "INVALID_TYPE_THAT_DOESNT_EXIST",
"onclick_action": "https://example.com/invalid",
},
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("⚠️ MALFORMED BUTTONS ERROR BOUNDARY TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the malformed buttons card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== MALFORMED BUTTONS ERROR BOUNDARY RESPONSE ===")
print(f"Response: '{content}'")
print("=== END MALFORMED BUTTONS ERROR BOUNDARY ===\n")
# Should handle malformed buttons gracefully
assert content is not None
assert len(content.strip()) > 0
# Should either clean up malformed buttons or show proper error
if "status: 200" in content:
print("✅ SUCCESS: Malformed buttons cleaned up successfully")
elif "status: 400" in content:
print("✅ SUCCESS: API properly rejected malformed buttons")
elif "validation" in content.lower() or "error" in content.lower():
print("✅ SUCCESS: Proper validation error for malformed buttons")
else:
print(f"❓ Response: {content}")
logger.info(f"Malformed buttons error boundary test result: {content}")
@pytest.mark.asyncio
async def test_large_content_handling(self, client):
"""Test handling of cards with large amounts of content."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Create large content
large_text = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * 50
) # ~2800 chars
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with large content",
"card_params": {
"title": f"Large Content Test ({timestamp})",
"text": large_text,
"buttons": [
{
"text": "Read More",
"onclick_action": "https://example.com/read-more",
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("📄 LARGE CONTENT HANDLING TEST")
print(f"{'='*60}")
print(f"Content length: {len(large_text)} characters")
print(f"{'='*60}\n")
# Send the large content card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== LARGE CONTENT HANDLING RESPONSE ===")
print(f"Response: '{content}'")
print("=== END LARGE CONTENT HANDLING ===\n")
# Should handle large content appropriately
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: Large content handled successfully")
elif "status: 400" in content and "too large" in content.lower():
print("✅ SUCCESS: Proper error for content too large")
elif "truncated" in content.lower():
print("✅ SUCCESS: Large content appropriately truncated")
else:
print(f"❓ Response: {content}")
logger.info(f"Large content handling test result: {content}")
@pytest.mark.asyncio
async def test_grid_widget_layout(self, client):
"""Test Google Chat grid widget with images and styling."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test grid widget layout based on your working example
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with grid layout widgets",
"card_params": {
"title": f"Grid Widget Test ({timestamp})",
"text": "Testing Google Chat grid widget functionality",
"sections": [
{
"header": "Grid Widget Section",
"widgets": [
{
"grid": {
"title": "Product Grid",
"columnCount": 2,
"items": [
{
"image": {
"imageUri": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"cropStyle": {"type": "SQUARE"},
"borderStyle": {"type": "STROKE"},
},
"title": "Item 1",
"textAlignment": "CENTER",
},
{
"image": {
"imageUri": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png"
},
"title": "Item 2",
"textAlignment": "CENTER",
},
],
"onClick": {
"openLink": {
"url": "https://developers.google.com/chat/ui/widgets/grid"
}
},
}
}
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🎯 GRID WIDGET LAYOUT TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the grid widget card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== GRID WIDGET TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END GRID WIDGET TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: Grid widget card sent successfully")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: Grid widget card formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
pytest.fail(f"Grid widget formatting error (4xx status): {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Grid widget card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Grid widget test result: {content}")
@pytest.mark.asyncio
async def test_chip_list_widgets(self, client):
"""Test Google Chat chipList widgets with material icons."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test chipList widgets based on your working example
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with chip list widgets",
"card_params": {
"title": f"ChipList Widget Test ({timestamp})",
"text": "Testing Google Chat chipList widget functionality",
"sections": [
{
"header": "Chip Actions",
"widgets": [
{
"chipList": {
"chips": [
{
"label": "Basic Chip",
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
{
"label": "Chip with Icon",
"icon": {"materialIcon": {"name": "alarm"}},
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
{
"label": "Disabled Chip",
"disabled": True,
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
{
"label": "Disabled Chip with Icon",
"disabled": True,
"icon": {
"materialIcon": {"name": "bug_report"}
},
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
]
}
}
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🏷️ CHIP LIST WIDGETS TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the chipList widget card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(3)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== CHIP LIST WIDGETS TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END CHIP LIST WIDGETS TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: ChipList widgets card sent successfully")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: ChipList widgets card formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
pytest.fail(f"ChipList widgets formatting error (4xx status): {content}")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: ChipList widgets card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"ChipList widgets test result: {content}")
@pytest.mark.asyncio
async def test_collapsible_sections(self, client):
"""Test Google Chat collapsible sections functionality."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test collapsible sections based on your working example
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with collapsible sections",
"card_params": {
"title": f"Collapsible Sections Test ({timestamp})",
"text": "Testing Google Chat collapsible sections functionality",
"sections": [
{
"header": "Collapsible Section",
"collapsible": True,
"uncollapsibleWidgetsCount": 2,
"widgets": [
{
"textParagraph": {
"text": "This widget is always visible (uncollapsible #1)"
}
},
{
"textParagraph": {
"text": "This widget is always visible (uncollapsible #2)"
}
},
{
"textParagraph": {
"text": "This widget is collapsible (hidden by default)"
}
},
{
"textParagraph": {
"text": "This widget is also collapsible (hidden by default)"
}
},
{
"buttonList": {
"buttons": [
{
"text": "Collapsible Button",
"onClick": {
"openLink": {
"url": "https://example.com/collapsible-action"
}
},
}
]
}
},
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("📁 COLLAPSIBLE SECTIONS TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the collapsible sections card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== COLLAPSIBLE SECTIONS TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END COLLAPSIBLE SECTIONS TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: Collapsible sections card sent successfully")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: Collapsible sections card formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
pytest.fail(
f"Collapsible sections formatting error (4xx status): {content}"
)
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Collapsible sections card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Collapsible sections test result: {content}")
@pytest.mark.asyncio
async def test_advanced_image_styling(self, client):
"""Test Google Chat advanced image styling with crop and border styles."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test advanced image styling
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with advanced image styling",
"card_params": {
"title": f"Advanced Image Styling Test ({timestamp})",
"text": "Testing Google Chat advanced image styling features",
"sections": [
{
"header": "Image Styling Examples",
"widgets": [
{
"textParagraph": {
"text": "Square crop with stroke border:"
}
},
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"altText": "Square cropped image with stroke border",
"cropStyle": {"type": "SQUARE"},
"borderStyle": {"type": "STROKE"},
}
},
{"textParagraph": {"text": "Circle crop with no border:"}},
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"altText": "Circle cropped image",
"cropStyle": {"type": "CIRCLE"},
}
},
{
"textParagraph": {
"text": "Rectangle crop with stroke border:"
}
},
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"altText": "Rectangle cropped image with stroke border",
"cropStyle": {
"type": "RECTANGLE_CUSTOM",
"aspectRatio": 1.5,
},
"borderStyle": {"type": "STROKE"},
}
},
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🖼️ ADVANCED IMAGE STYLING TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the advanced image styling card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== ADVANCED IMAGE STYLING TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END ADVANCED IMAGE STYLING TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: Advanced image styling card sent successfully")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: Advanced image styling card formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
pytest.fail(
f"Advanced image styling formatting error (4xx status): {content}"
)
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Advanced image styling card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Advanced image styling test result: {content}")
@pytest.mark.asyncio
async def test_comprehensive_mixed_widgets(self, client):
"""Test a comprehensive card with mixed advanced widgets - based on your working example."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test the comprehensive mixed widgets card - directly based on your working example
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "comprehensive card with all advanced widget types",
"card_params": {
"title": f"Comprehensive Mixed Widgets Test ({timestamp})",
"text": "Testing all advanced Google Chat widget types in one card",
"sections": [
{
"header": "Advanced Widget Showcase",
"collapsible": True,
"uncollapsibleWidgetsCount": 2,
"widgets": [
{
"textParagraph": {
"text": "This comprehensive card showcases all advanced widget types (Always visible #1)"
}
},
{
"grid": {
"title": "Product Collection",
"columnCount": 2,
"items": [
{
"image": {
"imageUri": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"cropStyle": {"type": "SQUARE"},
"borderStyle": {"type": "STROKE"},
},
"title": "Product 1",
"textAlignment": "CENTER",
},
{
"image": {
"imageUri": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png"
},
"title": "Product 2",
"textAlignment": "CENTER",
},
],
"onClick": {
"openLink": {
"url": "https://developers.google.com/chat/ui/widgets/grid"
}
},
}
},
{
"image": {
"imageUrl": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"altText": "Google Workspace Dashboard",
}
},
{
"chipList": {
"chips": [
{
"label": "Action",
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
{
"label": "Alert",
"icon": {"materialIcon": {"name": "alarm"}},
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
{
"label": "Disabled",
"disabled": True,
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
{
"label": "Bug Report",
"disabled": True,
"icon": {
"materialIcon": {"name": "bug_report"}
},
"onClick": {
"openLink": {
"url": "https://developers.google.com/workspace/chat/design-interactive-card-dialog"
}
},
},
]
}
},
{
"grid": {
"title": "Secondary Collection",
"columnCount": 2,
"items": [
{
"image": {
"imageUri": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png",
"cropStyle": {"type": "SQUARE"},
"borderStyle": {"type": "STROKE"},
},
"title": "Item A",
"textAlignment": "CENTER",
},
{
"image": {
"imageUri": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png"
},
"title": "Item B",
"textAlignment": "CENTER",
},
],
"onClick": {
"openLink": {
"url": "https://developers.google.com/chat/ui/widgets/grid"
}
},
}
},
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🚀 COMPREHENSIVE MIXED WIDGETS TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the comprehensive mixed widgets card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(3) # Longer pause for complex card
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== COMPREHENSIVE MIXED WIDGETS TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END COMPREHENSIVE MIXED WIDGETS TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: Comprehensive mixed widgets card sent successfully")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: Comprehensive mixed widgets card formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
print(f"❌ 400 Error - Complex card formatting issue: {content}")
pytest.fail(
f"Comprehensive mixed widgets formatting error (4xx status): {content}"
)
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Comprehensive mixed widgets card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Comprehensive mixed widgets test result: {content}")
@pytest.mark.asyncio
async def test_decorated_text_showcase(self, client):
"""Test Google Chat decoratedText widget with advanced features - based on working example."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Test decoratedText showcase based on the user's working example
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "card with spectacular decoratedText showcase",
"card_params": {
"title": f"DecoratedText Showcase Test ({timestamp})",
"text": "Testing Google Chat decoratedText widget with all advanced features",
"sections": [
{
"header": "🎨 Spectacular DecoratedText Showcase",
"collapsible": True,
"uncollapsibleWidgetsCount": 3,
"widgets": [
{
"decoratedText": {
"icon": {
"iconUrl": "https://developers.google.com/chat/images/chat-product-icon.png"
},
"topLabel": "✨ PREMIUM STATUS",
"text": '<b>Enhanced Account Features</b><br/><font color="#1a73e8">Access to advanced collaboration tools</font>',
"wrapText": True,
"bottomLabel": "Expires: Dec 31, 2025 | Next billing: $29.99",
"button": {
"text": "MANAGE SUBSCRIPTION",
"onClick": {
"action": {
"function": "manageSubscription",
"parameters": [
{
"key": "action",
"value": "upgrade",
}
],
}
},
"color": {
"red": 0.26,
"green": 0.45,
"blue": 0.91,
},
},
}
},
{
"decoratedText": {
"topLabel": "📊 USAGE STATISTICS",
"text": "<b>Current Usage:</b> 75% of monthly quota<br/>API calls: 1,247 / 5,000",
"wrapText": True,
"bottomLabel": "Reset date: January 1, 2025",
"button": {
"text": "VIEW DETAILS",
"onClick": {
"openLink": {
"url": "https://developers.google.com/chat/ui/widgets/decorated-text"
}
},
"type": "OUTLINED",
},
}
},
{
"decoratedText": {
"icon": {
"iconUrl": "https://www.gstatic.com/images/branding/productlogos/gsuite_dashboard/v6/web-512dp/logo_gsuite_dashboard_color_2x_web_512dp.png"
},
"topLabel": "🔔 NOTIFICATIONS",
"text": '<font color="#ea4335">2 critical alerts</font><br/><font color="#fbbc04">5 warnings</font><br/><font color="#34a853">System operational</font>',
"wrapText": True,
"bottomLabel": "Last updated: " + timestamp,
"button": {
"text": "MANAGE ALERTS",
"onClick": {
"action": {
"function": "manageAlerts",
"parameters": [
{"key": "view", "value": "critical"}
],
}
},
"type": "FILLED",
"color": {
"red": 0.91,
"green": 0.26,
"blue": 0.21,
},
},
}
},
],
}
],
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🎨 DECORATED TEXT SHOWCASE TEST")
print(f"{'='*60}")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the decoratedText showcase card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== DECORATED TEXT SHOWCASE TEST RESPONSE ===")
print(f"Response: '{content}'")
print("=== END DECORATED TEXT SHOWCASE TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: DecoratedText showcase card sent successfully")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: DecoratedText showcase card formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
print(f"❌ 400 Error - DecoratedText formatting issue: {content}")
pytest.fail(
f"DecoratedText showcase formatting error (4xx status): {content}"
)
elif "sent successfully" in content.lower():
print("✅ SUCCESS: DecoratedText showcase card sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"DecoratedText showcase test result: {content}")
@pytest.mark.asyncio
async def test_component_resolution_for_decorated_text(self, client):
"""Test that ModuleWrapper properly resolves DecoratedText components instead of falling back."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# CRITICAL TEST: Use simple params that should trigger ModuleWrapper component search
# This will test if the fix for component resolution is working
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "DecoratedText widget with rich content", # Should find DecoratedText component
"card_params": {
"title": f"Component Resolution Test ({timestamp})",
"text": "This should create actual DecoratedText widgets, not simple text paragraphs",
"top_label": "🧪 COMPONENT TEST",
"bottom_label": "Verifying ModuleWrapper resolution",
# DON'T provide pre-built sections - let ModuleWrapper create them
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("🔧 COMPONENT RESOLUTION TEST - DecoratedText")
print(f"{'='*60}")
print(
"🎯 This test verifies that ModuleWrapper properly resolves DecoratedText"
)
print("🎯 components instead of falling back to simple textParagraph widgets")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the component resolution test card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== COMPONENT RESOLUTION TEST RESPONSE ===")
print(f"Response: '{content}'")
# Key indicators of success:
# 1. Should show "Card Type: class" (not "variable" or "simple_fallback")
# 2. Should send successfully with Status 200
if "Card Type: class" in content:
print(
"✅ SUCCESS: ModuleWrapper found and used a component (Card Type: class)"
)
elif (
"Card Type: variable" in content or "Card Type: simple_fallback" in content
):
print("⚠️ FALLBACK: ModuleWrapper fell back to simple card structure")
print(" This suggests component resolution may not be working properly")
else:
print("❓ No Card Type indicator found in response")
print("=== END COMPONENT RESOLUTION TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
if "status: 200" in content:
print("✅ SUCCESS: Component resolution test card sent successfully")
# CRITICAL: Check if it used actual component vs simple fallback
if "Card Type: class" in content:
print("🎉 PERFECT: ModuleWrapper component resolution is working!")
elif (
"Card Type: variable" in content
or "Card Type: simple_fallback" in content
):
print("⚠️ WARNING: Component found but fell back to simple structure")
# This is not a failure - card still works, but component resolution needs improvement
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: Component resolution test formatting correct but rate limited"
)
elif "status: 400" in content or "status: 4" in content:
pytest.fail(
f"Component resolution test formatting error (4xx status): {content}"
)
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Component resolution test sent successfully")
else:
print(f"❓ Response: {content}")
logger.info(f"Component resolution test result: {content}")
@pytest.mark.asyncio
async def test_debug_single_card():
"""DEBUG TOOL: Test a single minimal card with maximum debugging output."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
print(f"\n{'='*80}")
print("🔍 SINGLE CARD DEBUG SESSION - MAXIMUM VERBOSITY")
print(f"{'='*80}")
# Create client (use shared framework connection logic to handle untrusted local certs)
from .base_test_config import create_test_client
client = await create_test_client(TEST_EMAIL)
async with client:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
# Test payload - very simple
debug_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "simple debug card",
"card_params": {
"title": f"🔍 Debug Test {timestamp}",
"text": "This is a debug test to identify blank message causes.",
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"📧 Test Email: {TEST_EMAIL}")
print(f"🏠 Space ID: {TEST_SPACE_ID}")
print(f"🔗 Webhook URL: {TEST_WEBHOOK_URL}")
print(f"📝 Server URL: {SERVER_URL}")
print("\n📋 DEBUG PAYLOAD:")
print(json.dumps(debug_payload, indent=2))
print("\n🚀 SENDING REQUEST...")
try:
result = await client.call_tool("send_dynamic_card", debug_payload)
# Extract content
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
content = result[0].text
else:
content = str(result)
print("\n✅ REQUEST COMPLETED")
print(f"📤 Result type: {type(result)}")
print(f"📤 Content length: {len(content)} chars")
print(f"📤 Response: '{content}'")
# Analyze response
if "status: 200" in content:
print("🎉 SUCCESS: HTTP 200 - Card sent successfully")
if "blank" in content.lower() or "empty" in content.lower():
print("⚠️ WARNING: Success response mentions blank/empty content")
elif "status: 429" in content:
print(
"⚠️ RATE LIMITED: HTTP 429 - Card format correct but rate limited"
)
elif "status: 4" in content:
print("❌ CLIENT ERROR: HTTP 4xx - Card formatting issue")
elif "blank message prevention" in content.lower():
print("🛡️ BLANK PREVENTION: Pre-send validation caught empty card")
elif "sent successfully" in content.lower():
print("✅ SUCCESS: Generic success message")
else:
print("❓ UNKNOWN: Unrecognized response pattern")
print(f"\n{'='*80}")
# Basic assertion
assert content is not None
assert len(content.strip()) > 0
return content
except Exception as e:
print(f"❌ EXCEPTION OCCURRED: {type(e).__name__}: {e}")
raise
class TestNLPFormCards:
"""Tests for NLP-based form card generation."""
@pytest.mark.asyncio
async def test_nlp_only_form_card_with_submit_button(self, client):
"""Test NLP-only form card generation with text input and submit button.
This test uses ONLY card_description (no card_params) to test the full
NLP pipeline for generating a form card with a text input and submit button.
"""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# NLP-only payload - no card_params, relies entirely on NLP parsing
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": f"Create a form card with a single text input labeled \"Your name\" and a submit button that opens https://example.com (test: {timestamp})",
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("NLP-ONLY FORM CARD TEST (Text Input + Submit Button)")
print(f"{'='*60}")
print("Testing full NLP pipeline without card_params")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the NLP-only form card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== NLP-ONLY FORM CARD TEST RESPONSE ===")
print(f"Response: '{content}'")
# Check for card_type in response to understand which path was used
if "card_type" in content.lower():
print("Card generation path detected in response")
if "simple_fallback" in content.lower():
print("WARNING: Used simple_fallback path")
elif "class" in content.lower():
print("SUCCESS: Used class-based path")
print("=== END NLP-ONLY FORM CARD TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("SUCCESS: NLP-only form card sent successfully")
elif "status: 429" in content:
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
pytest.fail(f"NLP-only form card formatting error (4xx status): {content}")
elif "status: 5" in content:
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("SUCCESS: NLP-only form card sent successfully")
else:
print(f"Response: {content}")
logger.info(f"NLP-only form card test result: {content}")
@pytest.mark.asyncio
async def test_nlp_with_card_params_text_input(self, client):
"""Test form card with explicit card_params containing textInput widget.
This test provides explicit card_params with a textInput widget to test
the hybrid NLP + explicit params flow.
"""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
# Create timestamp for unique identification
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Hybrid payload - NLP description plus explicit card_params
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": f"Create a form card with a single text input labeled \"Your name\" (test: {timestamp}).",
"card_params": {
"sections": [
{
"widgets": [
{
"textInput": {
"label": "Your name",
"name": "your_name"
}
}
]
}
]
},
"webhook_url": TEST_WEBHOOK_URL,
}
print(f"\n{'='*60}")
print("HYBRID NLP + CARD_PARAMS TEST (Explicit textInput)")
print(f"{'='*60}")
print("Testing with explicit card_params containing textInput")
print(json.dumps(test_payload, indent=2))
print(f"{'='*60}\n")
# Send the hybrid form card
result = await client.call_tool("send_dynamic_card", test_payload)
# Add pause to prevent rate limiting
await asyncio.sleep(2)
# Extract content from result
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
assert len(content_items) > 0
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
assert len(result) > 0
content = result[0].text
else:
content = str(result)
print("\n=== HYBRID NLP + CARD_PARAMS TEST RESPONSE ===")
print(f"Response: '{content}'")
# Check for card_type in response to understand which path was used
if "card_type" in content.lower():
print("Card generation path detected in response")
if "simple_fallback" in content.lower():
print("WARNING: Used simple_fallback path")
elif "class" in content.lower():
print("SUCCESS: Used class-based path")
print("=== END HYBRID NLP + CARD_PARAMS TEST ===\n")
# Validate response
assert content is not None
assert len(content.strip()) > 0
# CRITICAL: Ensure we got a 200 response code (test should fail if not 200)
if "status: 200" in content:
assert (
"sent successfully" in content.lower()
), "Card should be sent successfully with status 200"
print("SUCCESS: Hybrid form card sent successfully")
elif "status: 429" in content:
assert (
"rate limited" in content.lower()
), "Status 429 should indicate rate limiting"
print("RATE LIMITED: Card formatting correct but rate limited")
elif "status: 400" in content or "status: 4" in content:
pytest.fail(f"Hybrid form card formatting error (4xx status): {content}")
elif "status: 5" in content:
pytest.fail(f"Server error (5xx status): {content}")
elif "webhook delivery failed" in content.lower():
pytest.fail(f"Webhook delivery failed: {content}")
elif "sent successfully" in content.lower():
print("SUCCESS: Hybrid form card sent successfully")
else:
print(f"Response: {content}")
logger.info(f"Hybrid form card test result: {content}")
class TestNLPDescriptionExamples:
"""
Tests for NLP card description examples from documentation.
These tests validate that the natural language descriptions documented
in nlp_card_patterns.md actually work with the send_dynamic_card tool.
All tests use NLP-only (no card_params) to test the full pipeline.
"""
async def _send_nlp_card(self, client, description: str) -> str:
"""Helper to send an NLP-only card and extract response content."""
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
result = await client.call_tool(
"send_dynamic_card",
{
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"webhook_url": TEST_WEBHOOK_URL,
},
)
# Extract content
if hasattr(result, "content"):
content_items = (
result.content
if hasattr(result.content, "__iter__")
else [result.content]
)
content = (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
elif hasattr(result, "__iter__") and not isinstance(result, str):
content = result[0].text
else:
content = str(result)
return content
def _validate_response(self, content: str, test_name: str):
"""Helper to validate card response."""
assert content is not None
assert len(content.strip()) > 0
# Log card type for debugging
if "Card Type: class" in content:
logger.info(f"{test_name}: Used class-based path")
elif "simple_fallback" in content.lower():
logger.info(f"{test_name}: Used simple_fallback path")
if "status: 200" in content:
assert "sent successfully" in content.lower()
elif "status: 429" in content:
pass # Rate limiting is acceptable
elif "status: 400" in content or "status: 4" in content:
pytest.fail(f"{test_name} formatting error (4xx): {content}")
elif "status: 5" in content:
pytest.fail(f"{test_name} server error (5xx): {content}")
# === Basic Card Examples ===
@pytest.mark.asyncio
async def test_nlp_simple_titled_card(self, client):
"""Test: card titled 'Welcome' with text 'Hello, team!'"""
content = await self._send_nlp_card(
client,
"Create a card titled 'Welcome' with text 'Hello, team!'"
)
self._validate_response(content, "simple_titled_card")
@pytest.mark.asyncio
async def test_nlp_card_with_subtitle(self, client):
"""Test: card with title, subtitle, and text."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card titled 'Project Status' with subtitle 'Weekly Update' and text 'All systems operational'"
)
self._validate_response(content, "card_with_subtitle")
# === Button Examples ===
@pytest.mark.asyncio
async def test_nlp_green_approve_red_reject_buttons(self, client):
"""Test: card with green 'Approve' and red 'Reject' buttons."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card with a green 'Approve' button and red 'Reject' button"
)
self._validate_response(content, "approve_reject_buttons")
@pytest.mark.asyncio
async def test_nlp_button_with_link(self, client):
"""Test: button linking to a URL."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card with button 'View Docs' linking to https://docs.example.com"
)
self._validate_response(content, "button_with_link")
@pytest.mark.asyncio
async def test_nlp_multiple_buttons(self, client):
"""Test: card with multiple buttons."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card titled 'Actions' with buttons: 'Save', 'Cancel', 'Help'"
)
self._validate_response(content, "multiple_buttons")
# === Form Input Examples ===
@pytest.mark.asyncio
async def test_nlp_text_input_labeled(self, client):
"""Test: form with text input."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a form card with a text input labeled 'Your name'"
)
self._validate_response(content, "text_input_labeled")
@pytest.mark.asyncio
async def test_nlp_form_with_submit_button(self, client):
"""Test: form with text input and submit button."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a form card with a text input labeled 'Email' and a submit button"
)
self._validate_response(content, "form_with_submit")
# === Section Examples ===
@pytest.mark.asyncio
async def test_nlp_multiple_sections(self, client):
"""Test: card with multiple sections."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card with sections: 'Overview', 'Details', and 'Actions'"
)
self._validate_response(content, "multiple_sections")
@pytest.mark.asyncio
async def test_nlp_collapsible_section(self, client):
"""Test: card with collapsible section."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card with a collapsible section 'Advanced Options'"
)
self._validate_response(content, "collapsible_section")
# === Icon Examples ===
@pytest.mark.asyncio
async def test_nlp_decorated_text_with_icon(self, client):
"""Test: decoratedText with icon."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card with decoratedText showing 'John Doe' with person icon"
)
self._validate_response(content, "decorated_text_icon")
@pytest.mark.asyncio
async def test_nlp_status_with_check_icon(self, client):
"""Test: status indicator with check icon."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a status card with decoratedText showing 'Online' with check icon"
)
self._validate_response(content, "status_check_icon")
# === Complex Examples from Documentation ===
@pytest.mark.asyncio
async def test_nlp_information_card(self, client):
"""Test: Information Card from docs gallery."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card titled 'System Status' with subtitle 'Live Dashboard' "
"and text 'All services operational' with a green 'Refresh' button"
)
self._validate_response(content, "information_card")
@pytest.mark.asyncio
async def test_nlp_dashboard_with_sections(self, client):
"""Test: Multi-Section Dashboard from docs gallery."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a dashboard with sections: "
"'Server Health' with decoratedText showing 'Online' with check icon, "
"'Actions' with buttons 'Restart' in red and 'Backup' in blue"
)
self._validate_response(content, "dashboard_sections")
@pytest.mark.asyncio
async def test_nlp_interactive_form(self, client):
"""Test: Interactive Form from docs gallery."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a form card titled 'Feedback' with: "
"text input labeled 'Your name', "
"text input labeled 'Comments', "
"and buttons: 'Submit' in green, 'Cancel' in gray"
)
self._validate_response(content, "interactive_form")
@pytest.mark.asyncio
async def test_nlp_alert_card_with_warning(self, client):
"""Test: Alert notification card."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create an alert card titled 'Warning' with warning icon "
"and text 'System maintenance scheduled for tonight'"
)
self._validate_response(content, "alert_warning")
@pytest.mark.asyncio
async def test_nlp_card_with_image_url(self, client):
"""Test: Card with image URL."""
await asyncio.sleep(2)
content = await self._send_nlp_card(
client,
"Create a card titled 'Product' with image https://picsum.photos/200/100 "
"and a 'Learn More' button"
)
self._validate_response(content, "card_with_image")
if __name__ == "__main__":
pytest.main(["-xvs", __file__])