meta-ads-mcp 1.0.0__tar.gz → 1.0.1__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-1.0.0 → meta_ads_mcp-1.0.1}/PKG-INFO +2 -2
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/README.md +1 -1
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/insights.py +8 -2
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/pyproject.toml +1 -1
- meta_ads_mcp-1.0.1/tests/test_insights_pagination.py +257 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/.gitignore +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/Dockerfile +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/LICENSE +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/RELEASE.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/examples/README.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/future_improvements.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/ads.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/openai_deep_research.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/requirements.txt +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/setup.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/smithery.yaml +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/README.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/__init__.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/conftest.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/e2e_account_info_search_issue.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_account_info_access_fix.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_dsa_beneficiary.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_dsa_integration.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_dynamic_creatives.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_estimate_audience_size.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_estimate_audience_size_e2e.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_account_pages.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_insights_actions_and_values_e2e.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_is_dynamic_creative_adset.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_mobile_app_adset_creation.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_mobile_app_adset_issue.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_openai.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_page_discovery.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_page_discovery_integration.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_targeting.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_targeting_search_e2e.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_update_ad_creative_id.py +0 -0
- {meta_ads_mcp-1.0.0 → meta_ads_mcp-1.0.1}/tests/test_upload_ad_image.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Model Context Protocol (MCP) server 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
|
|
@@ -34,7 +34,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
|
|
|
34
34
|
## Community & Support
|
|
35
35
|
|
|
36
36
|
- [Discord](https://discord.gg/YzMwQ8zrjr). Join the community.
|
|
37
|
-
- [Email Support](info@pipeboard.co). Email us for support.
|
|
37
|
+
- [Email Support](mailto:info@pipeboard.co). Email us for support.
|
|
38
38
|
|
|
39
39
|
## Table of Contents
|
|
40
40
|
|
|
@@ -9,7 +9,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
|
|
|
9
9
|
## Community & Support
|
|
10
10
|
|
|
11
11
|
- [Discord](https://discord.gg/YzMwQ8zrjr). Join the community.
|
|
12
|
-
- [Email Support](info@pipeboard.co). Email us for support.
|
|
12
|
+
- [Email Support](mailto:info@pipeboard.co). Email us for support.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -13,7 +13,7 @@ import datetime
|
|
|
13
13
|
@meta_api_tool
|
|
14
14
|
async def get_insights(object_id: str, access_token: Optional[str] = None,
|
|
15
15
|
time_range: Union[str, Dict[str, str]] = "maximum", breakdown: str = "",
|
|
16
|
-
level: str = "ad") -> str:
|
|
16
|
+
level: str = "ad", limit: int = 25, after: str = "") -> str:
|
|
17
17
|
"""
|
|
18
18
|
Get performance insights for a campaign, ad set, ad or account.
|
|
19
19
|
|
|
@@ -49,6 +49,8 @@ async def get_insights(object_id: str, access_token: Optional[str] = None,
|
|
|
49
49
|
marketing_messages_btn_name, impression_view_time_advertiser_hour_v2, comscore_market,
|
|
50
50
|
comscore_market_code
|
|
51
51
|
level: Level of aggregation (ad, adset, campaign, account)
|
|
52
|
+
limit: Maximum number of results to return per page (default: 25, Meta API allows much higher values)
|
|
53
|
+
after: Pagination cursor to get the next set of results. Use the 'after' cursor from previous response's paging.next field.
|
|
52
54
|
"""
|
|
53
55
|
if not object_id:
|
|
54
56
|
return json.dumps({"error": "No object ID provided"}, indent=2)
|
|
@@ -56,7 +58,8 @@ async def get_insights(object_id: str, access_token: Optional[str] = None,
|
|
|
56
58
|
endpoint = f"{object_id}/insights"
|
|
57
59
|
params = {
|
|
58
60
|
"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",
|
|
59
|
-
"level": level
|
|
61
|
+
"level": level,
|
|
62
|
+
"limit": limit
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
# Handle time range based on type
|
|
@@ -73,6 +76,9 @@ async def get_insights(object_id: str, access_token: Optional[str] = None,
|
|
|
73
76
|
if breakdown:
|
|
74
77
|
params["breakdowns"] = breakdown
|
|
75
78
|
|
|
79
|
+
if after:
|
|
80
|
+
params["after"] = after
|
|
81
|
+
|
|
76
82
|
data = await make_api_request(endpoint, access_token, params)
|
|
77
83
|
|
|
78
84
|
return json.dumps(data, indent=2)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Test pagination functionality for insights endpoint."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import json
|
|
5
|
+
from unittest.mock import AsyncMock, patch
|
|
6
|
+
from meta_ads_mcp.core.insights import get_insights
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestInsightsPagination:
|
|
10
|
+
"""Test suite for pagination functionality in get_insights"""
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_auth_manager(self):
|
|
14
|
+
"""Mock for the authentication manager"""
|
|
15
|
+
with patch('meta_ads_mcp.core.api.auth_manager') as mock, \
|
|
16
|
+
patch('meta_ads_mcp.core.api.get_current_access_token') as mock_get_token:
|
|
17
|
+
# Mock a valid access token
|
|
18
|
+
mock.get_current_access_token.return_value = "test_access_token"
|
|
19
|
+
mock.is_token_valid.return_value = True
|
|
20
|
+
mock.app_id = "test_app_id"
|
|
21
|
+
mock_get_token.return_value = "test_access_token"
|
|
22
|
+
yield mock
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def valid_account_id(self):
|
|
26
|
+
return "act_701351919139047"
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mock_paginated_response_page1(self):
|
|
30
|
+
"""Mock first page of paginated response"""
|
|
31
|
+
return {
|
|
32
|
+
"data": [
|
|
33
|
+
{
|
|
34
|
+
"campaign_id": "campaign_1",
|
|
35
|
+
"campaign_name": "Test Campaign 1",
|
|
36
|
+
"spend": "100.50",
|
|
37
|
+
"impressions": "1000",
|
|
38
|
+
"clicks": "50"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"campaign_id": "campaign_2",
|
|
42
|
+
"campaign_name": "Test Campaign 2",
|
|
43
|
+
"spend": "200.75",
|
|
44
|
+
"impressions": "2000",
|
|
45
|
+
"clicks": "100"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"paging": {
|
|
49
|
+
"cursors": {
|
|
50
|
+
"before": "before_cursor_1",
|
|
51
|
+
"after": "after_cursor_1"
|
|
52
|
+
},
|
|
53
|
+
"next": "https://graph.facebook.com/v20.0/act_123/insights?after=after_cursor_1&limit=2"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def mock_paginated_response_page2(self):
|
|
59
|
+
"""Mock second page of paginated response"""
|
|
60
|
+
return {
|
|
61
|
+
"data": [
|
|
62
|
+
{
|
|
63
|
+
"campaign_id": "campaign_3",
|
|
64
|
+
"campaign_name": "Test Campaign 3",
|
|
65
|
+
"spend": "150.25",
|
|
66
|
+
"impressions": "1500",
|
|
67
|
+
"clicks": "75"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"paging": {
|
|
71
|
+
"cursors": {
|
|
72
|
+
"before": "before_cursor_2",
|
|
73
|
+
"after": "after_cursor_2"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_insights_with_limit_parameter(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
80
|
+
"""Test that limit parameter is properly passed to API"""
|
|
81
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
82
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
83
|
+
|
|
84
|
+
result = await get_insights(
|
|
85
|
+
object_id=valid_account_id,
|
|
86
|
+
level="campaign",
|
|
87
|
+
time_range="last_30d",
|
|
88
|
+
limit=2
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Verify the API was called with correct parameters
|
|
92
|
+
mock_api_request.assert_called_once()
|
|
93
|
+
call_args = mock_api_request.call_args
|
|
94
|
+
|
|
95
|
+
# Check that limit is included in params
|
|
96
|
+
params = call_args[0][2]
|
|
97
|
+
assert params["limit"] == 2
|
|
98
|
+
assert params["level"] == "campaign"
|
|
99
|
+
assert params["date_preset"] == "last_30d"
|
|
100
|
+
|
|
101
|
+
# Verify the response structure
|
|
102
|
+
result_data = json.loads(result)
|
|
103
|
+
assert "data" in result_data
|
|
104
|
+
assert len(result_data["data"]) == 2
|
|
105
|
+
assert "paging" in result_data
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
async def test_insights_with_after_cursor(self, mock_auth_manager, valid_account_id, mock_paginated_response_page2):
|
|
109
|
+
"""Test that after cursor is properly passed to API for pagination"""
|
|
110
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
111
|
+
mock_api_request.return_value = mock_paginated_response_page2
|
|
112
|
+
|
|
113
|
+
after_cursor = "after_cursor_1"
|
|
114
|
+
result = await get_insights(
|
|
115
|
+
object_id=valid_account_id,
|
|
116
|
+
level="campaign",
|
|
117
|
+
time_range="last_30d",
|
|
118
|
+
limit=10,
|
|
119
|
+
after=after_cursor
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Verify the API was called with correct parameters
|
|
123
|
+
mock_api_request.assert_called_once()
|
|
124
|
+
call_args = mock_api_request.call_args
|
|
125
|
+
|
|
126
|
+
# Check that after cursor is included in params
|
|
127
|
+
params = call_args[0][2]
|
|
128
|
+
assert params["after"] == after_cursor
|
|
129
|
+
assert params["limit"] == 10
|
|
130
|
+
|
|
131
|
+
# Verify the response structure
|
|
132
|
+
result_data = json.loads(result)
|
|
133
|
+
assert "data" in result_data
|
|
134
|
+
assert len(result_data["data"]) == 1
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
async def test_insights_default_limit(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
138
|
+
"""Test that default limit is 25 when not specified"""
|
|
139
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
140
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
141
|
+
|
|
142
|
+
result = await get_insights(
|
|
143
|
+
object_id=valid_account_id,
|
|
144
|
+
level="campaign"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Verify the API was called with default limit
|
|
148
|
+
mock_api_request.assert_called_once()
|
|
149
|
+
call_args = mock_api_request.call_args
|
|
150
|
+
|
|
151
|
+
params = call_args[0][2]
|
|
152
|
+
assert params["limit"] == 25 # Default value
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_insights_without_after_cursor(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
156
|
+
"""Test that after parameter is not included when empty"""
|
|
157
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
158
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
159
|
+
|
|
160
|
+
result = await get_insights(
|
|
161
|
+
object_id=valid_account_id,
|
|
162
|
+
level="campaign",
|
|
163
|
+
after="" # Empty after cursor
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Verify the API was called without after parameter
|
|
167
|
+
mock_api_request.assert_called_once()
|
|
168
|
+
call_args = mock_api_request.call_args
|
|
169
|
+
|
|
170
|
+
params = call_args[0][2]
|
|
171
|
+
assert "after" not in params # Should not be included when empty
|
|
172
|
+
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_insights_pagination_with_custom_time_range(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
175
|
+
"""Test pagination works with custom time range"""
|
|
176
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
177
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
178
|
+
|
|
179
|
+
custom_time_range = {"since": "2024-01-01", "until": "2024-01-31"}
|
|
180
|
+
result = await get_insights(
|
|
181
|
+
object_id=valid_account_id,
|
|
182
|
+
level="campaign",
|
|
183
|
+
time_range=custom_time_range,
|
|
184
|
+
limit=5,
|
|
185
|
+
after="test_cursor"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Verify the API was called with correct parameters
|
|
189
|
+
mock_api_request.assert_called_once()
|
|
190
|
+
call_args = mock_api_request.call_args
|
|
191
|
+
|
|
192
|
+
params = call_args[0][2]
|
|
193
|
+
assert params["limit"] == 5
|
|
194
|
+
assert params["after"] == "test_cursor"
|
|
195
|
+
assert params["time_range"] == json.dumps(custom_time_range)
|
|
196
|
+
|
|
197
|
+
@pytest.mark.asyncio
|
|
198
|
+
async def test_insights_pagination_with_breakdown(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
199
|
+
"""Test pagination works with breakdown parameter"""
|
|
200
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
201
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
202
|
+
|
|
203
|
+
result = await get_insights(
|
|
204
|
+
object_id=valid_account_id,
|
|
205
|
+
level="campaign",
|
|
206
|
+
breakdown="age",
|
|
207
|
+
limit=10,
|
|
208
|
+
after="test_cursor_2"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Verify the API was called with correct parameters
|
|
212
|
+
mock_api_request.assert_called_once()
|
|
213
|
+
call_args = mock_api_request.call_args
|
|
214
|
+
|
|
215
|
+
params = call_args[0][2]
|
|
216
|
+
assert params["limit"] == 10
|
|
217
|
+
assert params["after"] == "test_cursor_2"
|
|
218
|
+
assert params["breakdowns"] == "age"
|
|
219
|
+
|
|
220
|
+
@pytest.mark.asyncio
|
|
221
|
+
async def test_insights_large_limit_value(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
222
|
+
"""Test that large limit values are accepted (API will enforce its own limits)"""
|
|
223
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
224
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
225
|
+
|
|
226
|
+
result = await get_insights(
|
|
227
|
+
object_id=valid_account_id,
|
|
228
|
+
level="campaign",
|
|
229
|
+
limit=1000 # Large limit - API will enforce its own max
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Verify the API was called with the large limit
|
|
233
|
+
mock_api_request.assert_called_once()
|
|
234
|
+
call_args = mock_api_request.call_args
|
|
235
|
+
|
|
236
|
+
params = call_args[0][2]
|
|
237
|
+
assert params["limit"] == 1000
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_insights_paging_response_structure(self, mock_auth_manager, valid_account_id, mock_paginated_response_page1):
|
|
241
|
+
"""Test that paging information is preserved in the response"""
|
|
242
|
+
with patch('meta_ads_mcp.core.insights.make_api_request', new_callable=AsyncMock) as mock_api_request:
|
|
243
|
+
mock_api_request.return_value = mock_paginated_response_page1
|
|
244
|
+
|
|
245
|
+
result = await get_insights(
|
|
246
|
+
object_id=valid_account_id,
|
|
247
|
+
level="campaign",
|
|
248
|
+
limit=2
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Verify the response includes paging information
|
|
252
|
+
result_data = json.loads(result)
|
|
253
|
+
assert "data" in result_data
|
|
254
|
+
assert "paging" in result_data
|
|
255
|
+
assert "cursors" in result_data["paging"]
|
|
256
|
+
assert "after" in result_data["paging"]["cursors"]
|
|
257
|
+
assert "next" in result_data["paging"]
|
|
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
|
|
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
|