Skip to main content
Glama
test_insights_actions_and_values_e2e.py•22 kB
#!/usr/bin/env python3 """ Unit Tests for Insights Actions and Action Values Functionality This test suite validates the actions and action_values field implementation for the get_insights function in meta_ads_mcp/core/insights.py. Test cases cover: - Actions and action_values field inclusion in API requests - Different levels of aggregation (ad, adset, campaign, account) - Time range handling with actions and action_values - Error handling and validation - Purchase data extraction from actions and action_values """ import pytest import json import asyncio import requests from unittest.mock import AsyncMock, patch, MagicMock from typing import Dict, Any, List # Import the function to test from meta_ads_mcp.core.insights import get_insights class TestInsightsActionsAndValues: """Test suite for actions and action_values in insights""" @pytest.fixture def mock_api_request(self): """Mock for the make_api_request function""" with patch('meta_ads_mcp.core.insights.make_api_request') as mock: mock.return_value = { "data": [ { "campaign_id": "test_campaign_id", "campaign_name": "Test Campaign", "impressions": "1000", "clicks": "50", "spend": "100.00", "actions": [ {"action_type": "purchase", "value": "5"}, {"action_type": "lead", "value": "3"}, {"action_type": "view_content", "value": "20"} ], "action_values": [ {"action_type": "purchase", "value": "500.00"}, {"action_type": "lead", "value": "150.00"}, {"action_type": "view_content", "value": "0.00"} ], "cost_per_action_type": [ {"action_type": "purchase", "value": "20.00"}, {"action_type": "lead", "value": "33.33"} ] } ], "paging": {} } yield mock @pytest.fixture def mock_auth_manager(self): """Mock for the authentication manager""" with patch('meta_ads_mcp.core.api.auth_manager') as mock, \ patch('meta_ads_mcp.core.auth.get_current_access_token') as mock_get_token: # Mock a valid access token mock.get_current_access_token.return_value = "test_access_token" mock.is_token_valid.return_value = True mock.app_id = "test_app_id" mock_get_token.return_value = "test_access_token" yield mock @pytest.fixture def valid_campaign_id(self): """Valid campaign ID for testing""" return "123456789" @pytest.fixture def valid_account_id(self): """Valid account ID for testing""" return "act_701351919139047" @pytest.mark.asyncio async def test_actions_and_action_values_included_in_fields(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test that actions and action_values are included in the fields parameter""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="campaign" ) # Parse the result result_data = json.loads(result) # Verify the API was called with correct parameters mock_api_request.assert_called_once() call_args = mock_api_request.call_args # Check that the endpoint is correct (first argument) assert call_args[0][0] == f"{valid_campaign_id}/insights" # Check that actions and action_values are included in fields parameter params = call_args[0][2] # Third positional argument is params assert 'fields' in params fields = params['fields'] assert 'actions' in fields assert 'action_values' in fields assert 'cost_per_action_type' in fields # Verify the response structure assert 'data' in result_data assert len(result_data['data']) > 0 assert 'actions' in result_data['data'][0] assert 'action_values' in result_data['data'][0] @pytest.mark.asyncio async def test_purchase_data_extraction(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test that purchase data can be extracted from actions and action_values""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="campaign" ) # Parse the result result_data = json.loads(result) # Get the first data point data_point = result_data['data'][0] # Extract purchase data from actions actions = data_point.get('actions', []) purchase_actions = [action for action in actions if action.get('action_type') == 'purchase'] # Extract purchase data from action_values action_values = data_point.get('action_values', []) purchase_values = [action_value for action_value in action_values if action_value.get('action_type') == 'purchase'] # Verify purchase data exists assert len(purchase_actions) > 0, "No purchase actions found" assert len(purchase_values) > 0, "No purchase action_values found" # Verify purchase data values purchase_count = purchase_actions[0].get('value') purchase_value = purchase_values[0].get('value') assert purchase_count == "5", f"Expected purchase count 5, got {purchase_count}" assert purchase_value == "500.00", f"Expected purchase value 500.00, got {purchase_value}" @pytest.mark.asyncio async def test_actions_at_adset_level(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test actions and action_values at adset level""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="adset" ) # Parse the result result_data = json.loads(result) # Verify the API was called with correct parameters mock_api_request.assert_called_once() call_args = mock_api_request.call_args # Check that the level parameter is correct params = call_args[0][2] assert params['level'] == 'adset' # Verify the response structure assert 'data' in result_data @pytest.mark.asyncio async def test_actions_at_ad_level(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test actions and action_values at ad level""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="ad" ) # Parse the result result_data = json.loads(result) # Verify the API was called with correct parameters mock_api_request.assert_called_once() call_args = mock_api_request.call_args # Check that the level parameter is correct params = call_args[0][2] assert params['level'] == 'ad' # Verify the response structure assert 'data' in result_data @pytest.mark.asyncio async def test_actions_with_custom_time_range(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test actions and action_values with custom time range""" custom_time_range = {"since": "2024-01-01", "until": "2024-01-31"} result = await get_insights( object_id=valid_campaign_id, time_range=custom_time_range, level="campaign" ) # Parse the result result_data = json.loads(result) # Verify the API was called with correct parameters mock_api_request.assert_called_once() call_args = mock_api_request.call_args # Check that time_range is properly formatted params = call_args[0][2] assert 'time_range' in params assert params['time_range'] == json.dumps(custom_time_range) # Verify the response structure assert 'data' in result_data @pytest.mark.asyncio async def test_actions_with_breakdown(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test actions and action_values with breakdown dimension""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="campaign", breakdown="age" ) # Parse the result result_data = json.loads(result) # Verify the API was called with correct parameters mock_api_request.assert_called_once() call_args = mock_api_request.call_args # Check that breakdown is included params = call_args[0][2] assert 'breakdowns' in params assert params['breakdowns'] == 'age' # Verify the response structure assert 'data' in result_data @pytest.mark.asyncio async def test_actions_without_object_id(self, mock_api_request, mock_auth_manager): """Test error handling when no object_id is provided""" result = await get_insights( time_range="last_30d", level="campaign" ) # Parse the result. The decorator returns a dict error for missing required args if isinstance(result, dict): result_data = result else: result_data = json.loads(result) assert 'error' in result_data assert "missing 1 required positional argument: 'object_id'" in result_data['error'] # Verify API was not called mock_api_request.assert_not_called() @pytest.mark.asyncio async def test_actions_with_invalid_time_range(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test error handling with invalid time range""" invalid_time_range = {"since": "2024-01-01"} # Missing "until" result = await get_insights( object_id=valid_campaign_id, time_range=invalid_time_range, level="campaign" ) # Parse the result result_data = json.loads(result) # The error response is wrapped in a 'data' field if 'data' in result_data: error_data = json.loads(result_data['data']) assert 'error' in error_data assert 'since' in error_data['error'] and 'until' in error_data['error'] else: assert 'error' in result_data assert 'since' in result_data['error'] and 'until' in result_data['error'] # Verify API was not called mock_api_request.assert_not_called() @pytest.mark.asyncio async def test_actions_api_error_handling(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test error handling when API call fails""" # Mock API to raise an exception mock_api_request.side_effect = Exception("API Error") result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="campaign" ) # Parse the result # The API error handling returns a dict directly, not a JSON string if isinstance(result, dict): result_data = result else: result_data = json.loads(result) # Verify error response assert 'error' in result_data assert 'API Error' in result_data['error'] @pytest.mark.asyncio async def test_actions_fields_completeness(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test that all required fields are included in the request""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="campaign" ) # Verify the API was called with correct parameters mock_api_request.assert_called_once() call_args = mock_api_request.call_args # Check that all required fields are included params = call_args[0][2] fields = params['fields'] # Required fields for actions and action_values required_fields = [ 'account_id', 'account_name', 'campaign_id', 'campaign_name', 'adset_id', 'adset_name', 'ad_id', 'ad_name', 'impressions', 'clicks', 'spend', 'cpc', 'cpm', 'ctr', 'reach', 'frequency', 'actions', 'action_values', 'conversions', 'unique_clicks', 'cost_per_action_type' ] for field in required_fields: assert field in fields, f"Field '{field}' not found in fields parameter" @pytest.mark.asyncio async def test_multiple_action_types(self, mock_api_request, mock_auth_manager, valid_campaign_id): """Test handling of multiple action types in the response""" result = await get_insights( object_id=valid_campaign_id, time_range="last_30d", level="campaign" ) # Parse the result result_data = json.loads(result) # Get the first data point data_point = result_data['data'][0] # Check that multiple action types are present actions = data_point.get('actions', []) action_types = [action.get('action_type') for action in actions] assert 'purchase' in action_types, "Purchase action type not found" assert 'lead' in action_types, "Lead action type not found" assert 'view_content' in action_types, "View content action type not found" # Check action_values has corresponding entries action_values = data_point.get('action_values', []) action_value_types = [action_value.get('action_type') for action_value in action_values] assert 'purchase' in action_value_types, "Purchase action_value type not found" assert 'lead' in action_value_types, "Lead action_value type not found" @pytest.mark.e2e @pytest.mark.skip(reason="E2E test - run manually only") class TestInsightsActionsAndValuesE2E: """E2E tests for actions and action_values via MCP HTTP server""" def __init__(self, base_url: str = "http://localhost:8080"): self.base_url = base_url.rstrip('/') self.endpoint = f"{self.base_url}/mcp/" self.request_id = 1 # Default account from workspace rules self.account_id = "act_701351919139047" def _make_request(self, method: str, params: Dict[str, Any] = None) -> Dict[str, Any]: headers = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", "User-Agent": "Insights-E2E-Test-Client/1.0" } payload = { "jsonrpc": "2.0", "method": method, "id": self.request_id } if params: payload["params"] = params try: resp = requests.post(self.endpoint, headers=headers, json=payload, timeout=30) self.request_id += 1 return { "status_code": resp.status_code, "json": resp.json() if resp.status_code == 200 else None, "text": resp.text, "success": resp.status_code == 200 } except requests.exceptions.RequestException as e: return {"status_code": 0, "json": None, "text": str(e), "success": False, "error": str(e)} def _check_for_errors(self, parsed_content: Dict[str, Any]) -> Dict[str, Any]: if "data" in parsed_content: data = parsed_content["data"] if isinstance(data, dict) and 'error' in data: return {"has_error": True, "error_message": data['error'], "format": "wrapped_dict"} if isinstance(data, str): try: error_data = json.loads(data) if 'error' in error_data: return {"has_error": True, "error_message": error_data['error'], "format": "wrapped_json"} except json.JSONDecodeError: pass if 'error' in parsed_content: return {"has_error": True, "error_message": parsed_content['error'], "format": "direct"} return {"has_error": False} def test_tool_exists_and_has_required_fields(self): result = self._make_request("tools/list", {}) assert result["success"], f"tools/list failed: {result.get('text', '')}" tools = result["json"]["result"].get("tools", []) tool = next((t for t in tools if t["name"] == "get_insights"), None) assert tool is not None, "get_insights tool not found" props = tool.get("inputSchema", {}).get("properties", {}) for req in ["object_id", "time_range", "breakdown", "level"]: assert req in props, f"Missing parameter in schema: {req}" def test_get_insights_account_level(self): params = { "name": "get_insights", "arguments": { "object_id": self.account_id, "time_range": "last_30d", "level": "account" } } result = self._make_request("tools/call", params) assert result["success"], f"tools/call failed: {result.get('text', '')}" response_data = result["json"]["result"] content = response_data.get("content", [{}])[0].get("text", "") parsed = json.loads(content) err = self._check_for_errors(parsed) # Don't fail if auth or permissions block; just assert structure is parsable if err["has_error"]: assert isinstance(err["error_message"], (str, dict)) else: # Expect data list on success data = parsed.get("data") if isinstance(parsed, dict) else None assert data is not None def main(): tester = TestInsightsActionsAndValuesE2E() print("šŸš€ Insights Actions E2E (manual)") # Basic smoke run try: tester.test_tool_exists_and_has_required_fields() print("āœ… Tool schema ok") tester.test_get_insights_account_level() print("āœ… Account-level insights request executed") except AssertionError as e: print(f"āŒ Assertion failed: {e}") except Exception as e: print(f"āŒ Error: {e}") if __name__ == "__main__": main() def extract_purchase_data(insights_data): """ Helper function to extract purchase data from insights response. Args: insights_data: The data array from insights response Returns: dict: Dictionary with purchase count and value """ if not insights_data or len(insights_data) == 0: return {"purchase_count": 0, "purchase_value": 0.0} data_point = insights_data[0] # Extract purchase count from actions actions = data_point.get('actions', []) purchase_actions = [action for action in actions if action.get('action_type') == 'purchase'] purchase_count = int(purchase_actions[0].get('value', 0)) if purchase_actions else 0 # Extract purchase value from action_values action_values = data_point.get('action_values', []) purchase_values = [action_value for action_value in action_values if action_value.get('action_type') == 'purchase'] purchase_value = float(purchase_values[0].get('value', 0)) if purchase_values else 0.0 return { "purchase_count": purchase_count, "purchase_value": purchase_value } class TestPurchaseDataExtraction: """Test suite for purchase data extraction helper function""" def test_extract_purchase_data_with_purchases(self): """Test extraction when purchase data is present""" insights_data = [{ "actions": [ {"action_type": "purchase", "value": "5"}, {"action_type": "lead", "value": "3"} ], "action_values": [ {"action_type": "purchase", "value": "500.00"}, {"action_type": "lead", "value": "150.00"} ] }] result = extract_purchase_data(insights_data) assert result["purchase_count"] == 5 assert result["purchase_value"] == 500.0 def test_extract_purchase_data_without_purchases(self): """Test extraction when no purchase data is present""" insights_data = [{ "actions": [ {"action_type": "lead", "value": "3"}, {"action_type": "view_content", "value": "20"} ], "action_values": [ {"action_type": "lead", "value": "150.00"}, {"action_type": "view_content", "value": "0.00"} ] }] result = extract_purchase_data(insights_data) assert result["purchase_count"] == 0 assert result["purchase_value"] == 0.0 def test_extract_purchase_data_empty_data(self): """Test extraction with empty data""" insights_data = [] result = extract_purchase_data(insights_data) assert result["purchase_count"] == 0 assert result["purchase_value"] == 0.0

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pipeboard-co/meta-ads-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server