meta-ads-mcp 0.7.9__tar.gz → 0.7.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/PKG-INFO +1 -1
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/ads.py +201 -16
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/pyproject.toml +1 -1
- meta_ads_mcp-0.7.10/tests/test_page_discovery.py +377 -0
- meta_ads_mcp-0.7.10/tests/test_page_discovery_integration.py +245 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/.gitignore +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/Dockerfile +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/LICENSE +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/README.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/RELEASE.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/examples/README.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/future_improvements.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/openai_deep_research.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/requirements.txt +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/setup.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/smithery.yaml +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/README.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_dsa_beneficiary.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_dsa_integration.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_insights_actions_and_values.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_openai.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_targeting.py +0 -0
- {meta_ads_mcp-0.7.9 → meta_ads_mcp-0.7.10}/tests/test_targeting_search_e2e.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.10
|
|
4
4
|
Summary: Model Context Protocol (MCP) plugin for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
|
|
@@ -670,31 +670,31 @@ async def create_ad_creative(
|
|
|
670
670
|
if not account_id.startswith("act_"):
|
|
671
671
|
account_id = f"act_{account_id}"
|
|
672
672
|
|
|
673
|
-
# If no page ID is provided,
|
|
673
|
+
# Enhanced page discovery: If no page ID is provided, use robust discovery methods
|
|
674
674
|
if not page_id:
|
|
675
675
|
try:
|
|
676
|
-
#
|
|
677
|
-
|
|
678
|
-
pages_params = {
|
|
679
|
-
"fields": "id,name",
|
|
680
|
-
"limit": 1
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
|
|
676
|
+
# Use the comprehensive page discovery logic from get_account_pages
|
|
677
|
+
page_discovery_result = await _discover_pages_for_account(account_id, access_token)
|
|
684
678
|
|
|
685
|
-
if "
|
|
686
|
-
page_id =
|
|
687
|
-
|
|
679
|
+
if page_discovery_result.get("success"):
|
|
680
|
+
page_id = page_discovery_result["page_id"]
|
|
681
|
+
page_name = page_discovery_result.get("page_name", "Unknown")
|
|
682
|
+
print(f"Auto-discovered page ID: {page_id} ({page_name})")
|
|
688
683
|
else:
|
|
689
684
|
return json.dumps({
|
|
690
|
-
"error": "No page ID provided and no pages found for this account",
|
|
691
|
-
"
|
|
685
|
+
"error": "No page ID provided and no suitable pages found for this account",
|
|
686
|
+
"details": page_discovery_result.get("message", "Page discovery failed"),
|
|
687
|
+
"suggestions": [
|
|
688
|
+
"Use get_account_pages to see available pages",
|
|
689
|
+
"Use search_pages_by_name to find specific pages",
|
|
690
|
+
"Provide a page_id parameter manually"
|
|
691
|
+
]
|
|
692
692
|
}, indent=2)
|
|
693
693
|
except Exception as e:
|
|
694
694
|
return json.dumps({
|
|
695
|
-
"error": "Error
|
|
695
|
+
"error": "Error during page discovery",
|
|
696
696
|
"details": str(e),
|
|
697
|
-
"suggestion": "Please provide a page_id parameter"
|
|
697
|
+
"suggestion": "Please provide a page_id parameter or use get_account_pages to find available pages"
|
|
698
698
|
}, indent=2)
|
|
699
699
|
|
|
700
700
|
# Prepare the creative data
|
|
@@ -759,6 +759,191 @@ async def create_ad_creative(
|
|
|
759
759
|
}, indent=2)
|
|
760
760
|
|
|
761
761
|
|
|
762
|
+
async def _discover_pages_for_account(account_id: str, access_token: str) -> dict:
|
|
763
|
+
"""
|
|
764
|
+
Internal function to discover pages for an account using multiple approaches.
|
|
765
|
+
Returns the best available page ID for ad creation.
|
|
766
|
+
"""
|
|
767
|
+
try:
|
|
768
|
+
# Approach 1: Extract page IDs from tracking_specs in ads (most reliable)
|
|
769
|
+
endpoint = f"{account_id}/ads"
|
|
770
|
+
params = {
|
|
771
|
+
"fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
|
|
772
|
+
"limit": 100
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
tracking_ads_data = await make_api_request(endpoint, access_token, params)
|
|
776
|
+
|
|
777
|
+
tracking_page_ids = set()
|
|
778
|
+
if "data" in tracking_ads_data:
|
|
779
|
+
for ad in tracking_ads_data.get("data", []):
|
|
780
|
+
tracking_specs = ad.get("tracking_specs", [])
|
|
781
|
+
if isinstance(tracking_specs, list):
|
|
782
|
+
for spec in tracking_specs:
|
|
783
|
+
if isinstance(spec, dict) and "page" in spec:
|
|
784
|
+
page_list = spec["page"]
|
|
785
|
+
if isinstance(page_list, list):
|
|
786
|
+
for page_id in page_list:
|
|
787
|
+
if isinstance(page_id, (str, int)) and str(page_id).isdigit():
|
|
788
|
+
tracking_page_ids.add(str(page_id))
|
|
789
|
+
|
|
790
|
+
if tracking_page_ids:
|
|
791
|
+
# Get details for the first page found
|
|
792
|
+
page_id = list(tracking_page_ids)[0]
|
|
793
|
+
page_endpoint = f"{page_id}"
|
|
794
|
+
page_params = {
|
|
795
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
799
|
+
if "id" in page_data:
|
|
800
|
+
return {
|
|
801
|
+
"success": True,
|
|
802
|
+
"page_id": page_id,
|
|
803
|
+
"page_name": page_data.get("name", "Unknown"),
|
|
804
|
+
"source": "tracking_specs",
|
|
805
|
+
"note": "Page ID extracted from existing ads - most reliable for ad creation"
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# Approach 2: Try client_pages endpoint
|
|
809
|
+
endpoint = f"{account_id}/client_pages"
|
|
810
|
+
params = {
|
|
811
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
client_pages_data = await make_api_request(endpoint, access_token, params)
|
|
815
|
+
|
|
816
|
+
if "data" in client_pages_data and client_pages_data["data"]:
|
|
817
|
+
page = client_pages_data["data"][0]
|
|
818
|
+
return {
|
|
819
|
+
"success": True,
|
|
820
|
+
"page_id": page["id"],
|
|
821
|
+
"page_name": page.get("name", "Unknown"),
|
|
822
|
+
"source": "client_pages"
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
# Approach 3: Try assigned_pages endpoint
|
|
826
|
+
pages_endpoint = f"{account_id}/assigned_pages"
|
|
827
|
+
pages_params = {
|
|
828
|
+
"fields": "id,name",
|
|
829
|
+
"limit": 1
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
|
|
833
|
+
|
|
834
|
+
if "data" in pages_data and pages_data["data"]:
|
|
835
|
+
page = pages_data["data"][0]
|
|
836
|
+
return {
|
|
837
|
+
"success": True,
|
|
838
|
+
"page_id": page["id"],
|
|
839
|
+
"page_name": page.get("name", "Unknown"),
|
|
840
|
+
"source": "assigned_pages"
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
# If all approaches failed
|
|
844
|
+
return {
|
|
845
|
+
"success": False,
|
|
846
|
+
"message": "No suitable pages found for this account",
|
|
847
|
+
"note": "Try using get_account_pages to see all available pages or provide page_id manually"
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
except Exception as e:
|
|
851
|
+
return {
|
|
852
|
+
"success": False,
|
|
853
|
+
"message": f"Error during page discovery: {str(e)}"
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
async def _search_pages_by_name_core(access_token: str, account_id: str, search_term: str = None) -> str:
|
|
858
|
+
"""
|
|
859
|
+
Core logic for searching pages by name.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
access_token: Meta API access token
|
|
863
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
864
|
+
search_term: Search term to find pages by name (optional - returns all pages if not provided)
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
JSON string with search results
|
|
868
|
+
"""
|
|
869
|
+
# Ensure account_id has the 'act_' prefix
|
|
870
|
+
if not account_id.startswith("act_"):
|
|
871
|
+
account_id = f"act_{account_id}"
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
# Use the internal discovery function directly
|
|
875
|
+
page_discovery_result = await _discover_pages_for_account(account_id, access_token)
|
|
876
|
+
|
|
877
|
+
if not page_discovery_result.get("success"):
|
|
878
|
+
return json.dumps({
|
|
879
|
+
"data": [],
|
|
880
|
+
"message": "No pages found for this account",
|
|
881
|
+
"details": page_discovery_result.get("message", "Page discovery failed")
|
|
882
|
+
}, indent=2)
|
|
883
|
+
|
|
884
|
+
# Create a single page result
|
|
885
|
+
page_data = {
|
|
886
|
+
"id": page_discovery_result["page_id"],
|
|
887
|
+
"name": page_discovery_result.get("page_name", "Unknown"),
|
|
888
|
+
"source": page_discovery_result.get("source", "unknown")
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
all_pages_data = {"data": [page_data]}
|
|
892
|
+
|
|
893
|
+
# Filter pages by search term if provided
|
|
894
|
+
if search_term:
|
|
895
|
+
search_term_lower = search_term.lower()
|
|
896
|
+
filtered_pages = []
|
|
897
|
+
|
|
898
|
+
for page in all_pages_data["data"]:
|
|
899
|
+
page_name = page.get("name", "").lower()
|
|
900
|
+
if search_term_lower in page_name:
|
|
901
|
+
filtered_pages.append(page)
|
|
902
|
+
|
|
903
|
+
return json.dumps({
|
|
904
|
+
"data": filtered_pages,
|
|
905
|
+
"search_term": search_term,
|
|
906
|
+
"total_found": len(filtered_pages),
|
|
907
|
+
"total_available": len(all_pages_data["data"])
|
|
908
|
+
}, indent=2)
|
|
909
|
+
else:
|
|
910
|
+
# Return all pages if no search term provided
|
|
911
|
+
return json.dumps({
|
|
912
|
+
"data": all_pages_data["data"],
|
|
913
|
+
"total_available": len(all_pages_data["data"]),
|
|
914
|
+
"note": "Use search_term parameter to filter pages by name"
|
|
915
|
+
}, indent=2)
|
|
916
|
+
|
|
917
|
+
except Exception as e:
|
|
918
|
+
return json.dumps({
|
|
919
|
+
"error": "Failed to search pages by name",
|
|
920
|
+
"details": str(e)
|
|
921
|
+
}, indent=2)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
@mcp_server.tool()
|
|
925
|
+
@meta_api_tool
|
|
926
|
+
async def search_pages_by_name(access_token: str = None, account_id: str = None, search_term: str = None) -> str:
|
|
927
|
+
"""
|
|
928
|
+
Search for pages by name within an account.
|
|
929
|
+
|
|
930
|
+
Args:
|
|
931
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
932
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
933
|
+
search_term: Search term to find pages by name (optional - returns all pages if not provided)
|
|
934
|
+
|
|
935
|
+
Returns:
|
|
936
|
+
JSON response with matching pages
|
|
937
|
+
"""
|
|
938
|
+
# Check required parameters
|
|
939
|
+
if not account_id:
|
|
940
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
941
|
+
|
|
942
|
+
# Call the core function
|
|
943
|
+
result = await _search_pages_by_name_core(access_token, account_id, search_term)
|
|
944
|
+
return result
|
|
945
|
+
|
|
946
|
+
|
|
762
947
|
@mcp_server.tool()
|
|
763
948
|
@meta_api_tool
|
|
764
949
|
async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test page discovery functionality for Meta Ads MCP.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import json
|
|
7
|
+
from unittest.mock import AsyncMock, patch
|
|
8
|
+
from meta_ads_mcp.core.ads import _discover_pages_for_account, search_pages_by_name, _search_pages_by_name_core
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestPageDiscovery:
|
|
12
|
+
"""Test page discovery functionality."""
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_discover_pages_from_tracking_specs(self):
|
|
16
|
+
"""Test page discovery from tracking specs (most reliable method)."""
|
|
17
|
+
mock_ads_data = {
|
|
18
|
+
"data": [
|
|
19
|
+
{
|
|
20
|
+
"id": "123456789",
|
|
21
|
+
"tracking_specs": [
|
|
22
|
+
{
|
|
23
|
+
"page": ["987654321", "111222333"]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
mock_page_data = {
|
|
31
|
+
"id": "987654321",
|
|
32
|
+
"name": "Test Page",
|
|
33
|
+
"username": "testpage",
|
|
34
|
+
"category": "Test Category"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
38
|
+
# Mock the ads endpoint call
|
|
39
|
+
mock_api.side_effect = [
|
|
40
|
+
mock_ads_data, # First call for ads
|
|
41
|
+
mock_page_data # Second call for page details
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
45
|
+
|
|
46
|
+
assert result["success"] is True
|
|
47
|
+
# Check that we got one of the expected page IDs (set order is not guaranteed)
|
|
48
|
+
assert result["page_id"] in ["987654321", "111222333"]
|
|
49
|
+
assert result["page_name"] == "Test Page"
|
|
50
|
+
assert result["source"] == "tracking_specs"
|
|
51
|
+
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_discover_pages_from_client_pages(self):
|
|
54
|
+
"""Test page discovery from client_pages endpoint."""
|
|
55
|
+
mock_client_pages_data = {
|
|
56
|
+
"data": [
|
|
57
|
+
{
|
|
58
|
+
"id": "555666777",
|
|
59
|
+
"name": "Client Page",
|
|
60
|
+
"username": "clientpage"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Mock empty ads data, then client_pages data
|
|
66
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
67
|
+
mock_api.side_effect = [
|
|
68
|
+
{"data": []}, # No ads found
|
|
69
|
+
mock_client_pages_data # Client pages found
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
73
|
+
|
|
74
|
+
assert result["success"] is True
|
|
75
|
+
assert result["page_id"] == "555666777"
|
|
76
|
+
assert result["page_name"] == "Client Page"
|
|
77
|
+
assert result["source"] == "client_pages"
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_discover_pages_from_assigned_pages(self):
|
|
81
|
+
"""Test page discovery from assigned_pages endpoint."""
|
|
82
|
+
mock_assigned_pages_data = {
|
|
83
|
+
"data": [
|
|
84
|
+
{
|
|
85
|
+
"id": "888999000",
|
|
86
|
+
"name": "Assigned Page"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Mock empty responses for first two methods, then assigned_pages
|
|
92
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
93
|
+
mock_api.side_effect = [
|
|
94
|
+
{"data": []}, # No ads found
|
|
95
|
+
{"data": []}, # No client pages found
|
|
96
|
+
mock_assigned_pages_data # Assigned pages found
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
100
|
+
|
|
101
|
+
assert result["success"] is True
|
|
102
|
+
assert result["page_id"] == "888999000"
|
|
103
|
+
assert result["page_name"] == "Assigned Page"
|
|
104
|
+
assert result["source"] == "assigned_pages"
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_discover_pages_no_pages_found(self):
|
|
108
|
+
"""Test page discovery when no pages are found."""
|
|
109
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
110
|
+
mock_api.side_effect = [
|
|
111
|
+
{"data": []}, # No ads found
|
|
112
|
+
{"data": []}, # No client pages found
|
|
113
|
+
{"data": []} # No assigned pages found
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
117
|
+
|
|
118
|
+
assert result["success"] is False
|
|
119
|
+
assert "No suitable pages found" in result["message"]
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_discover_pages_with_invalid_page_ids(self):
|
|
123
|
+
"""Test page discovery with invalid page IDs in tracking_specs."""
|
|
124
|
+
mock_ads_data = {
|
|
125
|
+
"data": [
|
|
126
|
+
{
|
|
127
|
+
"id": "123456789",
|
|
128
|
+
"tracking_specs": [
|
|
129
|
+
{
|
|
130
|
+
"page": ["invalid_id", "not_numeric", "123abc"]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
138
|
+
mock_api.side_effect = [
|
|
139
|
+
mock_ads_data, # Ads with invalid page IDs
|
|
140
|
+
{"data": []}, # No client pages
|
|
141
|
+
{"data": []} # No assigned pages
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
145
|
+
|
|
146
|
+
assert result["success"] is False
|
|
147
|
+
assert "No suitable pages found" in result["message"]
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_discover_pages_api_error_handling(self):
|
|
151
|
+
"""Test page discovery with API errors."""
|
|
152
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
153
|
+
mock_api.side_effect = Exception("API Error")
|
|
154
|
+
|
|
155
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
156
|
+
|
|
157
|
+
assert result["success"] is False
|
|
158
|
+
assert "Error during page discovery" in result["message"]
|
|
159
|
+
|
|
160
|
+
@pytest.mark.asyncio
|
|
161
|
+
async def test_search_pages_by_name_logic(self):
|
|
162
|
+
"""Test the core search logic without authentication interference."""
|
|
163
|
+
# Test the filtering logic directly
|
|
164
|
+
mock_pages_data = {
|
|
165
|
+
"data": [
|
|
166
|
+
{"id": "111", "name": "Test Page 1"},
|
|
167
|
+
{"id": "222", "name": "Another Test Page"},
|
|
168
|
+
{"id": "333", "name": "Different Page"}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Test filtering with search term
|
|
173
|
+
search_term_lower = "test"
|
|
174
|
+
filtered_pages = []
|
|
175
|
+
|
|
176
|
+
for page in mock_pages_data["data"]:
|
|
177
|
+
page_name = page.get("name", "").lower()
|
|
178
|
+
if search_term_lower in page_name:
|
|
179
|
+
filtered_pages.append(page)
|
|
180
|
+
|
|
181
|
+
assert len(filtered_pages) == 2
|
|
182
|
+
assert filtered_pages[0]["name"] == "Test Page 1"
|
|
183
|
+
assert filtered_pages[1]["name"] == "Another Test Page"
|
|
184
|
+
|
|
185
|
+
@pytest.mark.asyncio
|
|
186
|
+
async def test_search_pages_by_name_no_matches(self):
|
|
187
|
+
"""Test search logic with no matching results."""
|
|
188
|
+
mock_pages_data = {
|
|
189
|
+
"data": [
|
|
190
|
+
{"id": "111", "name": "Test Page 1"},
|
|
191
|
+
{"id": "222", "name": "Another Test Page"}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Test filtering with non-matching search term
|
|
196
|
+
search_term_lower = "nonexistent"
|
|
197
|
+
filtered_pages = []
|
|
198
|
+
|
|
199
|
+
for page in mock_pages_data["data"]:
|
|
200
|
+
page_name = page.get("name", "").lower()
|
|
201
|
+
if search_term_lower in page_name:
|
|
202
|
+
filtered_pages.append(page)
|
|
203
|
+
|
|
204
|
+
assert len(filtered_pages) == 0
|
|
205
|
+
|
|
206
|
+
@pytest.mark.asyncio
|
|
207
|
+
async def test_search_pages_by_name_core_success(self):
|
|
208
|
+
"""Test the core search function with successful page discovery."""
|
|
209
|
+
mock_discovery_result = {
|
|
210
|
+
"success": True,
|
|
211
|
+
"page_id": "123456789",
|
|
212
|
+
"page_name": "Test Page",
|
|
213
|
+
"source": "tracking_specs"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
217
|
+
mock_discover.return_value = mock_discovery_result
|
|
218
|
+
|
|
219
|
+
result = await _search_pages_by_name_core("test_token", "act_123456789", "test")
|
|
220
|
+
result_data = json.loads(result)
|
|
221
|
+
|
|
222
|
+
assert len(result_data["data"]) == 1
|
|
223
|
+
assert result_data["data"][0]["id"] == "123456789"
|
|
224
|
+
assert result_data["data"][0]["name"] == "Test Page"
|
|
225
|
+
assert result_data["search_term"] == "test"
|
|
226
|
+
assert result_data["total_found"] == 1
|
|
227
|
+
assert result_data["total_available"] == 1
|
|
228
|
+
|
|
229
|
+
@pytest.mark.asyncio
|
|
230
|
+
async def test_search_pages_by_name_core_no_pages(self):
|
|
231
|
+
"""Test the core search function when no pages are found."""
|
|
232
|
+
mock_discovery_result = {
|
|
233
|
+
"success": False,
|
|
234
|
+
"message": "No pages found"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
238
|
+
mock_discover.return_value = mock_discovery_result
|
|
239
|
+
|
|
240
|
+
result = await _search_pages_by_name_core("test_token", "act_123456789", "test")
|
|
241
|
+
result_data = json.loads(result)
|
|
242
|
+
|
|
243
|
+
assert len(result_data["data"]) == 0
|
|
244
|
+
assert "No pages found" in result_data["message"]
|
|
245
|
+
|
|
246
|
+
@pytest.mark.asyncio
|
|
247
|
+
async def test_search_pages_by_name_core_no_search_term(self):
|
|
248
|
+
"""Test the core search function without search term."""
|
|
249
|
+
mock_discovery_result = {
|
|
250
|
+
"success": True,
|
|
251
|
+
"page_id": "123456789",
|
|
252
|
+
"page_name": "Test Page",
|
|
253
|
+
"source": "tracking_specs"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
257
|
+
mock_discover.return_value = mock_discovery_result
|
|
258
|
+
|
|
259
|
+
result = await _search_pages_by_name_core("test_token", "act_123456789")
|
|
260
|
+
result_data = json.loads(result)
|
|
261
|
+
|
|
262
|
+
assert len(result_data["data"]) == 1
|
|
263
|
+
assert result_data["total_available"] == 1
|
|
264
|
+
assert "note" in result_data
|
|
265
|
+
|
|
266
|
+
@pytest.mark.asyncio
|
|
267
|
+
async def test_search_pages_by_name_core_exception_handling(self):
|
|
268
|
+
"""Test the core search function with exception handling."""
|
|
269
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
270
|
+
mock_discover.side_effect = Exception("Test exception")
|
|
271
|
+
|
|
272
|
+
result = await _search_pages_by_name_core("test_token", "act_123456789", "test")
|
|
273
|
+
result_data = json.loads(result)
|
|
274
|
+
|
|
275
|
+
assert "error" in result_data
|
|
276
|
+
assert "Failed to search pages by name" in result_data["error"]
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_discover_pages_with_multiple_page_ids(self):
|
|
280
|
+
"""Test page discovery with multiple page IDs in tracking_specs."""
|
|
281
|
+
mock_ads_data = {
|
|
282
|
+
"data": [
|
|
283
|
+
{
|
|
284
|
+
"id": "123456789",
|
|
285
|
+
"tracking_specs": [
|
|
286
|
+
{
|
|
287
|
+
"page": ["111222333", "444555666", "777888999"]
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
mock_page_data = {
|
|
295
|
+
"id": "111222333",
|
|
296
|
+
"name": "First Page",
|
|
297
|
+
"username": "firstpage"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
301
|
+
mock_api.side_effect = [
|
|
302
|
+
mock_ads_data, # First call for ads
|
|
303
|
+
mock_page_data # Second call for page details
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
307
|
+
|
|
308
|
+
assert result["success"] is True
|
|
309
|
+
# Should get the first page ID from the set
|
|
310
|
+
assert result["page_id"] in ["111222333", "444555666", "777888999"]
|
|
311
|
+
assert result["page_name"] == "First Page"
|
|
312
|
+
|
|
313
|
+
@pytest.mark.asyncio
|
|
314
|
+
async def test_discover_pages_with_mixed_valid_invalid_ids(self):
|
|
315
|
+
"""Test page discovery with mixed valid and invalid page IDs."""
|
|
316
|
+
mock_ads_data = {
|
|
317
|
+
"data": [
|
|
318
|
+
{
|
|
319
|
+
"id": "123456789",
|
|
320
|
+
"tracking_specs": [
|
|
321
|
+
{
|
|
322
|
+
"page": ["invalid", "123456789", "not_numeric", "987654321"]
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
mock_page_data = {
|
|
330
|
+
"id": "123456789",
|
|
331
|
+
"name": "Valid Page",
|
|
332
|
+
"username": "validpage"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api:
|
|
336
|
+
mock_api.side_effect = [
|
|
337
|
+
mock_ads_data, # First call for ads
|
|
338
|
+
mock_page_data # Second call for page details
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
result = await _discover_pages_for_account("act_123456789", "test_token")
|
|
342
|
+
|
|
343
|
+
assert result["success"] is True
|
|
344
|
+
# Should get one of the valid numeric IDs
|
|
345
|
+
assert result["page_id"] in ["123456789", "987654321"]
|
|
346
|
+
assert result["page_name"] == "Valid Page"
|
|
347
|
+
|
|
348
|
+
@pytest.mark.asyncio
|
|
349
|
+
async def test_search_pages_by_name_case_insensitive(self):
|
|
350
|
+
"""Test search function with case insensitive matching."""
|
|
351
|
+
mock_discovery_result = {
|
|
352
|
+
"success": True,
|
|
353
|
+
"page_id": "123456789",
|
|
354
|
+
"page_name": "Test Page",
|
|
355
|
+
"source": "tracking_specs"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
359
|
+
mock_discover.return_value = mock_discovery_result
|
|
360
|
+
|
|
361
|
+
# Test with uppercase search term
|
|
362
|
+
result = await _search_pages_by_name_core("test_token", "act_123456789", "TEST")
|
|
363
|
+
result_data = json.loads(result)
|
|
364
|
+
|
|
365
|
+
assert len(result_data["data"]) == 1
|
|
366
|
+
assert result_data["total_found"] == 1
|
|
367
|
+
|
|
368
|
+
# Test with lowercase search term
|
|
369
|
+
result = await _search_pages_by_name_core("test_token", "act_123456789", "test")
|
|
370
|
+
result_data = json.loads(result)
|
|
371
|
+
|
|
372
|
+
assert len(result_data["data"]) == 1
|
|
373
|
+
assert result_data["total_found"] == 1
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
if __name__ == "__main__":
|
|
377
|
+
pytest.main([__file__])
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for page discovery functionality.
|
|
3
|
+
Tests the complete workflow from page discovery to ad creative creation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import json
|
|
8
|
+
from unittest.mock import AsyncMock, patch
|
|
9
|
+
from meta_ads_mcp.core.ads import create_ad_creative, search_pages_by_name, get_account_pages
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPageDiscoveryIntegration:
|
|
13
|
+
"""Integration tests for page discovery functionality."""
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_end_to_end_page_discovery_in_create_ad_creative(self):
|
|
17
|
+
"""Test that create_ad_creative automatically discovers pages when no page_id is provided."""
|
|
18
|
+
# Mock the page discovery to return a successful result
|
|
19
|
+
mock_discovery_result = {
|
|
20
|
+
"success": True,
|
|
21
|
+
"page_id": "123456789",
|
|
22
|
+
"page_name": "Test Page",
|
|
23
|
+
"source": "tracking_specs"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Mock the API request for creating the creative (will fail due to invalid image, but that's expected)
|
|
27
|
+
mock_creative_response = {
|
|
28
|
+
"error": {
|
|
29
|
+
"message": "Invalid parameter",
|
|
30
|
+
"type": "OAuthException",
|
|
31
|
+
"code": 100,
|
|
32
|
+
"error_subcode": 2446386,
|
|
33
|
+
"error_user_title": "Image Not Found",
|
|
34
|
+
"error_user_msg": "The image you selected is not available."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover, \
|
|
39
|
+
patch('meta_ads_mcp.core.ads.make_api_request') as mock_api, \
|
|
40
|
+
patch('meta_ads_mcp.core.api.meta_api_tool') as mock_decorator:
|
|
41
|
+
|
|
42
|
+
# Make the decorator just return the function
|
|
43
|
+
mock_decorator.side_effect = lambda func: func
|
|
44
|
+
|
|
45
|
+
mock_discover.return_value = mock_discovery_result
|
|
46
|
+
mock_api.return_value = mock_creative_response
|
|
47
|
+
|
|
48
|
+
# Call create_ad_creative without providing page_id
|
|
49
|
+
result = await create_ad_creative(
|
|
50
|
+
account_id="act_123456789",
|
|
51
|
+
name="Test Creative",
|
|
52
|
+
image_hash="test_hash_123",
|
|
53
|
+
message="Test message",
|
|
54
|
+
headline="Test Headline",
|
|
55
|
+
description="Test description"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
result_data = json.loads(result)
|
|
59
|
+
|
|
60
|
+
# Verify that the function attempted to create a creative (even though it failed due to invalid image)
|
|
61
|
+
assert "error" in result_data["data"]
|
|
62
|
+
assert "Image Not Found" in result_data["data"]["error"]["error_user_title"]
|
|
63
|
+
|
|
64
|
+
# Verify that page discovery was called
|
|
65
|
+
mock_discover.assert_called_once_with("act_123456789", None)
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_search_pages_by_name_integration(self):
|
|
69
|
+
"""Test the complete search_pages_by_name function with real-like data."""
|
|
70
|
+
# Mock the page discovery to return a successful result
|
|
71
|
+
mock_discovery_result = {
|
|
72
|
+
"success": True,
|
|
73
|
+
"page_id": "123456789",
|
|
74
|
+
"page_name": "Injury Payouts",
|
|
75
|
+
"source": "tracking_specs"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover, \
|
|
79
|
+
patch('meta_ads_mcp.core.api.meta_api_tool') as mock_decorator:
|
|
80
|
+
|
|
81
|
+
# Make the decorator just return the function
|
|
82
|
+
mock_decorator.side_effect = lambda func: func
|
|
83
|
+
|
|
84
|
+
mock_discover.return_value = mock_discovery_result
|
|
85
|
+
|
|
86
|
+
# Test searching for pages
|
|
87
|
+
result = await search_pages_by_name(
|
|
88
|
+
account_id="act_123456789",
|
|
89
|
+
search_term="Injury"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result_data = json.loads(result)
|
|
93
|
+
|
|
94
|
+
# Verify the search results
|
|
95
|
+
assert len(result_data["data"]) == 1
|
|
96
|
+
assert result_data["data"][0]["id"] == "123456789"
|
|
97
|
+
assert result_data["data"][0]["name"] == "Injury Payouts"
|
|
98
|
+
assert result_data["search_term"] == "Injury"
|
|
99
|
+
assert result_data["total_found"] == 1
|
|
100
|
+
assert result_data["total_available"] == 1
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_create_ad_creative_with_manual_page_id(self):
|
|
104
|
+
"""Test that create_ad_creative works with manually provided page_id."""
|
|
105
|
+
# Mock the API request for creating the creative
|
|
106
|
+
mock_creative_response = {
|
|
107
|
+
"error": {
|
|
108
|
+
"message": "Invalid parameter",
|
|
109
|
+
"type": "OAuthException",
|
|
110
|
+
"code": 100,
|
|
111
|
+
"error_subcode": 2446386,
|
|
112
|
+
"error_user_title": "Image Not Found",
|
|
113
|
+
"error_user_msg": "The image you selected is not available."
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api, \
|
|
118
|
+
patch('meta_ads_mcp.core.api.meta_api_tool') as mock_decorator:
|
|
119
|
+
|
|
120
|
+
# Make the decorator just return the function
|
|
121
|
+
mock_decorator.side_effect = lambda func: func
|
|
122
|
+
|
|
123
|
+
mock_api.return_value = mock_creative_response
|
|
124
|
+
|
|
125
|
+
# Call create_ad_creative with a manual page_id
|
|
126
|
+
result = await create_ad_creative(
|
|
127
|
+
account_id="act_123456789",
|
|
128
|
+
name="Test Creative",
|
|
129
|
+
image_hash="test_hash_123",
|
|
130
|
+
page_id="123456789", # Manual page ID
|
|
131
|
+
message="Test message",
|
|
132
|
+
headline="Test Headline",
|
|
133
|
+
description="Test description"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
result_data = json.loads(result)
|
|
137
|
+
|
|
138
|
+
# Verify that the function attempted to create a creative
|
|
139
|
+
assert "error" in result_data["data"]
|
|
140
|
+
assert "Image Not Found" in result_data["data"]["error"]["error_user_title"]
|
|
141
|
+
|
|
142
|
+
# Verify that make_api_request was called for creating the creative
|
|
143
|
+
mock_api.assert_called_once()
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_create_ad_creative_no_pages_found(self):
|
|
147
|
+
"""Test create_ad_creative when no pages are found."""
|
|
148
|
+
# Mock the page discovery to return no pages
|
|
149
|
+
mock_discovery_result = {
|
|
150
|
+
"success": False,
|
|
151
|
+
"message": "No suitable pages found for this account"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover, \
|
|
155
|
+
patch('meta_ads_mcp.core.api.meta_api_tool') as mock_decorator:
|
|
156
|
+
|
|
157
|
+
# Make the decorator just return the function
|
|
158
|
+
mock_decorator.side_effect = lambda func: func
|
|
159
|
+
|
|
160
|
+
mock_discover.return_value = mock_discovery_result
|
|
161
|
+
|
|
162
|
+
# Call create_ad_creative without providing page_id
|
|
163
|
+
result = await create_ad_creative(
|
|
164
|
+
account_id="act_123456789",
|
|
165
|
+
name="Test Creative",
|
|
166
|
+
image_hash="test_hash_123",
|
|
167
|
+
message="Test message"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
result_data = json.loads(result)
|
|
171
|
+
|
|
172
|
+
# Verify that the function returned an error about no pages found
|
|
173
|
+
assert "error" in result_data["data"]
|
|
174
|
+
assert "No page ID provided and no suitable pages found" in result_data["data"]["error"]
|
|
175
|
+
assert "suggestions" in result_data["data"]["error"]
|
|
176
|
+
|
|
177
|
+
@pytest.mark.asyncio
|
|
178
|
+
async def test_search_pages_by_name_no_search_term(self):
|
|
179
|
+
"""Test search_pages_by_name without a search term (should return all pages)."""
|
|
180
|
+
# Mock the page discovery to return a successful result
|
|
181
|
+
mock_discovery_result = {
|
|
182
|
+
"success": True,
|
|
183
|
+
"page_id": "123456789",
|
|
184
|
+
"page_name": "Test Page",
|
|
185
|
+
"source": "tracking_specs"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover, \
|
|
189
|
+
patch('meta_ads_mcp.core.api.meta_api_tool') as mock_decorator:
|
|
190
|
+
|
|
191
|
+
# Make the decorator just return the function
|
|
192
|
+
mock_decorator.side_effect = lambda func: func
|
|
193
|
+
|
|
194
|
+
mock_discover.return_value = mock_discovery_result
|
|
195
|
+
|
|
196
|
+
# Test searching without a search term
|
|
197
|
+
result = await search_pages_by_name(
|
|
198
|
+
account_id="act_123456789"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
result_data = json.loads(result)
|
|
202
|
+
|
|
203
|
+
# Verify the results
|
|
204
|
+
assert len(result_data["data"]) == 1
|
|
205
|
+
assert result_data["data"][0]["id"] == "123456789"
|
|
206
|
+
assert result_data["data"][0]["name"] == "Test Page"
|
|
207
|
+
assert result_data["total_available"] == 1
|
|
208
|
+
assert "note" in result_data
|
|
209
|
+
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_search_pages_by_name_no_matches(self):
|
|
212
|
+
"""Test search_pages_by_name when no pages match the search term."""
|
|
213
|
+
# Mock the page discovery to return a successful result
|
|
214
|
+
mock_discovery_result = {
|
|
215
|
+
"success": True,
|
|
216
|
+
"page_id": "123456789",
|
|
217
|
+
"page_name": "Test Page",
|
|
218
|
+
"source": "tracking_specs"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
with patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover, \
|
|
222
|
+
patch('meta_ads_mcp.core.api.meta_api_tool') as mock_decorator:
|
|
223
|
+
|
|
224
|
+
# Make the decorator just return the function
|
|
225
|
+
mock_decorator.side_effect = lambda func: func
|
|
226
|
+
|
|
227
|
+
mock_discover.return_value = mock_discovery_result
|
|
228
|
+
|
|
229
|
+
# Test searching for a term that doesn't match
|
|
230
|
+
result = await search_pages_by_name(
|
|
231
|
+
account_id="act_123456789",
|
|
232
|
+
search_term="Nonexistent"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result_data = json.loads(result)
|
|
236
|
+
|
|
237
|
+
# Verify that no pages were found
|
|
238
|
+
assert len(result_data["data"]) == 0
|
|
239
|
+
assert result_data["search_term"] == "Nonexistent"
|
|
240
|
+
assert result_data["total_found"] == 0
|
|
241
|
+
assert result_data["total_available"] == 1
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
pytest.main([__file__])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|