meta-ads-mcp 1.0.15__tar.gz → 1.0.16__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.
Potentially problematic release.
This version of meta-ads-mcp might be problematic. Click here for more details.
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/PKG-INFO +37 -1
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/README.md +36 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/STREAMABLE_HTTP_SETUP.md +10 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/campaigns.py +35 -1
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/pyproject.toml +1 -1
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/server.json +2 -2
- meta_ads_mcp-1.0.16/tests/test_campaign_objective_filter.py +518 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/.github/workflows/publish-mcp.yml +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/.gitignore +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/Dockerfile +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/LICENSE +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/RELEASE.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/examples/README.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/future_improvements.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/ads.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/openai_deep_research.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/requirements.txt +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/setup.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/smithery.yaml +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/README.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/__init__.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/conftest.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/e2e_account_info_search_issue.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_account_info_access_fix.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_create_ad_creative_simple.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_create_simple_creative_e2e.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_dsa_beneficiary.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_dsa_integration.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_dynamic_creatives.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_estimate_audience_size.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_estimate_audience_size_e2e.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_get_account_pages.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_insights_actions_and_values_e2e.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_insights_pagination.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_is_dynamic_creative_adset.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_mobile_app_adset_creation.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_mobile_app_adset_issue.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_openai.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_page_discovery.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_page_discovery_integration.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_targeting.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_targeting_search_e2e.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/tests/test_update_ad_creative_id.py +0 -0
- {meta_ads_mcp-1.0.15 → meta_ads_mcp-1.0.16}/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.16
|
|
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
|
|
@@ -68,6 +68,16 @@ The fastest and most reliable way to get started is to **[🚀 Get started with
|
|
|
68
68
|
|
|
69
69
|
That's it! You can now ask Claude to analyze your Meta ad campaigns, get performance insights, and manage your advertising.
|
|
70
70
|
|
|
71
|
+
#### Advanced: Direct Token Authentication (Claude)
|
|
72
|
+
|
|
73
|
+
For direct token-based authentication without the interactive flow, use this URL format when adding the integration:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
|
|
80
|
+
|
|
71
81
|
### For Cursor Users
|
|
72
82
|
|
|
73
83
|
Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP, click on "Needs login" to finish the login process.
|
|
@@ -83,12 +93,38 @@ Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP,
|
|
|
83
93
|
}
|
|
84
94
|
```
|
|
85
95
|
|
|
96
|
+
#### Advanced: Direct Token Authentication (Cursor)
|
|
97
|
+
|
|
98
|
+
If you prefer to authenticate without the interactive login flow, you can include your Pipeboard API token directly in the URL:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"meta-ads-remote": {
|
|
104
|
+
"url": "https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
|
|
111
|
+
|
|
86
112
|
### For Other MCP Clients
|
|
87
113
|
|
|
88
114
|
Use the Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
|
|
89
115
|
|
|
90
116
|
**[📖 Get detailed setup instructions for your AI client here](https://pipeboard.co)**
|
|
91
117
|
|
|
118
|
+
#### Advanced: Direct Token Authentication (Other Clients)
|
|
119
|
+
|
|
120
|
+
For MCP clients that support token-based authentication, you can append your Pipeboard API token to the URL:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This bypasses the interactive login flow and authenticates immediately. Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
|
|
127
|
+
|
|
92
128
|
## Local Installation (Technical Users Only)
|
|
93
129
|
|
|
94
130
|
If you're a developer or need to customize the installation, you can run Meta Ads MCP locally. **Most marketers should use the Remote MCP above instead!** For complete technical setup instructions, see our **[Local Installation Guide](LOCAL_INSTALLATION.md)**.
|
|
@@ -43,6 +43,16 @@ The fastest and most reliable way to get started is to **[🚀 Get started with
|
|
|
43
43
|
|
|
44
44
|
That's it! You can now ask Claude to analyze your Meta ad campaigns, get performance insights, and manage your advertising.
|
|
45
45
|
|
|
46
|
+
#### Advanced: Direct Token Authentication (Claude)
|
|
47
|
+
|
|
48
|
+
For direct token-based authentication without the interactive flow, use this URL format when adding the integration:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
|
|
55
|
+
|
|
46
56
|
### For Cursor Users
|
|
47
57
|
|
|
48
58
|
Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP, click on "Needs login" to finish the login process.
|
|
@@ -58,12 +68,38 @@ Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP,
|
|
|
58
68
|
}
|
|
59
69
|
```
|
|
60
70
|
|
|
71
|
+
#### Advanced: Direct Token Authentication (Cursor)
|
|
72
|
+
|
|
73
|
+
If you prefer to authenticate without the interactive login flow, you can include your Pipeboard API token directly in the URL:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"meta-ads-remote": {
|
|
79
|
+
"url": "https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
|
|
86
|
+
|
|
61
87
|
### For Other MCP Clients
|
|
62
88
|
|
|
63
89
|
Use the Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
|
|
64
90
|
|
|
65
91
|
**[📖 Get detailed setup instructions for your AI client here](https://pipeboard.co)**
|
|
66
92
|
|
|
93
|
+
#### Advanced: Direct Token Authentication (Other Clients)
|
|
94
|
+
|
|
95
|
+
For MCP clients that support token-based authentication, you can append your Pipeboard API token to the URL:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This bypasses the interactive login flow and authenticates immediately. Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
|
|
102
|
+
|
|
67
103
|
## Local Installation (Technical Users Only)
|
|
68
104
|
|
|
69
105
|
If you're a developer or need to customize the installation, you can run Meta Ads MCP locally. **Most marketers should use the Remote MCP above instead!** For complete technical setup instructions, see our **[Local Installation Guide](LOCAL_INSTALLATION.md)**.
|
|
@@ -83,6 +83,16 @@ curl -H "Authorization: Bearer your_pipeboard_token" \
|
|
|
83
83
|
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
#### Remote MCP: Token in URL
|
|
87
|
+
|
|
88
|
+
When using the hosted Remote MCP at `https://mcp.pipeboard.co/meta-ads-mcp`, you can alternatively authenticate by including the token as a URL parameter:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This is particularly useful for MCP clients that don't support interactive authentication flows.
|
|
95
|
+
|
|
86
96
|
### Alternative Method: Direct Meta Token
|
|
87
97
|
|
|
88
98
|
If you have a Meta Developer App, you can use a direct access token via the `X-META-ACCESS-TOKEN` header. This is less common.
|
|
@@ -9,7 +9,14 @@ from .server import mcp_server
|
|
|
9
9
|
|
|
10
10
|
@mcp_server.tool()
|
|
11
11
|
@meta_api_tool
|
|
12
|
-
async def get_campaigns(
|
|
12
|
+
async def get_campaigns(
|
|
13
|
+
account_id: str,
|
|
14
|
+
access_token: Optional[str] = None,
|
|
15
|
+
limit: int = 10,
|
|
16
|
+
status_filter: str = "",
|
|
17
|
+
objective_filter: Union[str, List[str]] = "",
|
|
18
|
+
after: str = ""
|
|
19
|
+
) -> str:
|
|
13
20
|
"""
|
|
14
21
|
Get campaigns for a Meta Ads account with optional filtering.
|
|
15
22
|
|
|
@@ -26,6 +33,11 @@ async def get_campaigns(account_id: str, access_token: Optional[str] = None, lim
|
|
|
26
33
|
status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
|
|
27
34
|
Maps to the 'effective_status' API parameter, which expects an array
|
|
28
35
|
(this function handles the required JSON formatting). Leave empty for all statuses.
|
|
36
|
+
objective_filter: Filter by campaign objective(s). Can be a single objective string or a list of objectives.
|
|
37
|
+
Valid objectives: 'OUTCOME_AWARENESS', 'OUTCOME_TRAFFIC', 'OUTCOME_ENGAGEMENT',
|
|
38
|
+
'OUTCOME_LEADS', 'OUTCOME_SALES', 'OUTCOME_APP_PROMOTION'.
|
|
39
|
+
Examples: 'OUTCOME_LEADS' or ['OUTCOME_LEADS', 'OUTCOME_SALES'].
|
|
40
|
+
Leave empty for all objectives.
|
|
29
41
|
after: Pagination cursor to get the next set of results
|
|
30
42
|
"""
|
|
31
43
|
# Require explicit account_id
|
|
@@ -38,10 +50,32 @@ async def get_campaigns(account_id: str, access_token: Optional[str] = None, lim
|
|
|
38
50
|
"limit": limit
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
# Build filtering array for complex filtering
|
|
54
|
+
filters = []
|
|
55
|
+
|
|
41
56
|
if status_filter:
|
|
42
57
|
# API expects an array, encode it as a JSON string
|
|
43
58
|
params["effective_status"] = json.dumps([status_filter])
|
|
44
59
|
|
|
60
|
+
# Handle objective filtering - supports both single string and list of objectives
|
|
61
|
+
if objective_filter:
|
|
62
|
+
# Convert single string to list for consistent handling
|
|
63
|
+
objectives = [objective_filter] if isinstance(objective_filter, str) else objective_filter
|
|
64
|
+
|
|
65
|
+
# Filter out empty strings
|
|
66
|
+
objectives = [obj for obj in objectives if obj]
|
|
67
|
+
|
|
68
|
+
if objectives:
|
|
69
|
+
filters.append({
|
|
70
|
+
"field": "objective",
|
|
71
|
+
"operator": "IN",
|
|
72
|
+
"value": objectives
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Add filtering parameter if we have filters
|
|
76
|
+
if filters:
|
|
77
|
+
params["filtering"] = json.dumps(filters)
|
|
78
|
+
|
|
45
79
|
if after:
|
|
46
80
|
params["after"] = after
|
|
47
81
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
|
|
3
3
|
"name": "co.pipeboard/meta-ads-mcp",
|
|
4
4
|
"description": "Facebook / Meta Ads automation with AI: analyze performance, test creatives, optimize spend.",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.16",
|
|
6
6
|
"remotes": [
|
|
7
7
|
{
|
|
8
8
|
"type": "streamable-http",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "pypi",
|
|
15
15
|
"identifier": "meta-ads-mcp",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.16",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Campaign Objective Filtering Tests for Meta Ads MCP
|
|
4
|
+
|
|
5
|
+
This test validates that the get_campaigns function correctly filters
|
|
6
|
+
campaigns by objective type, supporting both single and multiple objectives.
|
|
7
|
+
|
|
8
|
+
Test scenarios:
|
|
9
|
+
1. Single objective filtering (e.g., OUTCOME_LEADS)
|
|
10
|
+
2. Multiple objective filtering (e.g., [OUTCOME_LEADS, OUTCOME_SALES])
|
|
11
|
+
3. Combined status and objective filtering
|
|
12
|
+
4. Empty/no filtering (returns all campaigns)
|
|
13
|
+
5. Edge cases (empty strings, invalid types)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
import requests
|
|
18
|
+
import json
|
|
19
|
+
from typing import Dict, Any, List
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CampaignObjectiveFilterTester:
|
|
23
|
+
"""Test suite for campaign objective filtering functionality"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, base_url: str = "http://localhost:8080"):
|
|
26
|
+
self.base_url = base_url.rstrip('/')
|
|
27
|
+
self.endpoint = f"{self.base_url}/mcp/"
|
|
28
|
+
self.request_id = 1
|
|
29
|
+
|
|
30
|
+
def _make_request(self, method: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
31
|
+
"""Make a JSON-RPC request to the MCP server"""
|
|
32
|
+
|
|
33
|
+
headers = {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
"Accept": "application/json, text/event-stream",
|
|
36
|
+
"User-Agent": "Campaign-Filter-Test-Client/1.0"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
payload = {
|
|
40
|
+
"jsonrpc": "2.0",
|
|
41
|
+
"method": method,
|
|
42
|
+
"id": self.request_id
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if params:
|
|
46
|
+
payload["params"] = params
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
response = requests.post(
|
|
50
|
+
self.endpoint,
|
|
51
|
+
headers=headers,
|
|
52
|
+
json=payload,
|
|
53
|
+
timeout=30
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.request_id += 1
|
|
57
|
+
|
|
58
|
+
result = {
|
|
59
|
+
"status_code": response.status_code,
|
|
60
|
+
"headers": dict(response.headers),
|
|
61
|
+
"json": response.json() if response.status_code == 200 else None,
|
|
62
|
+
"text": response.text,
|
|
63
|
+
"success": response.status_code == 200
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Parse the content if successful
|
|
67
|
+
if result["success"] and result["json"]:
|
|
68
|
+
response_data = result["json"].get("result", {})
|
|
69
|
+
content = response_data.get("content", [{}])[0].get("text", "")
|
|
70
|
+
try:
|
|
71
|
+
result["parsed_content"] = json.loads(content)
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
result["parsed_content"] = None
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
except requests.exceptions.RequestException as e:
|
|
78
|
+
return {
|
|
79
|
+
"status_code": 0,
|
|
80
|
+
"headers": {},
|
|
81
|
+
"json": None,
|
|
82
|
+
"text": str(e),
|
|
83
|
+
"success": False,
|
|
84
|
+
"error": str(e)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def get_campaigns(self, account_id: str, **filters) -> Dict[str, Any]:
|
|
88
|
+
"""Get campaigns with optional filters"""
|
|
89
|
+
|
|
90
|
+
arguments = {"account_id": account_id}
|
|
91
|
+
arguments.update(filters)
|
|
92
|
+
|
|
93
|
+
return self._make_request("tools/call", {
|
|
94
|
+
"name": "get_campaigns",
|
|
95
|
+
"arguments": arguments
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
def test_no_filtering(self, account_id: str) -> Dict[str, Any]:
|
|
99
|
+
"""Test getting campaigns without any filtering"""
|
|
100
|
+
|
|
101
|
+
print(f"\n🔍 Test 1: Get campaigns without filtering")
|
|
102
|
+
|
|
103
|
+
result = self.get_campaigns(account_id)
|
|
104
|
+
|
|
105
|
+
if not result["success"]:
|
|
106
|
+
return {
|
|
107
|
+
"test": "no_filtering",
|
|
108
|
+
"success": False,
|
|
109
|
+
"error": result.get("text", "Unknown error")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
campaigns = result.get("parsed_content", {}).get("data", [])
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"test": "no_filtering",
|
|
116
|
+
"success": True,
|
|
117
|
+
"campaign_count": len(campaigns),
|
|
118
|
+
"campaigns": campaigns,
|
|
119
|
+
"objectives": [c.get("objective") for c in campaigns if "objective" in c]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def test_single_objective_filter(self, account_id: str, objective: str) -> Dict[str, Any]:
|
|
123
|
+
"""Test filtering by a single objective"""
|
|
124
|
+
|
|
125
|
+
print(f"\n🔍 Test 2: Filter by single objective: {objective}")
|
|
126
|
+
|
|
127
|
+
result = self.get_campaigns(account_id, objective_filter=objective)
|
|
128
|
+
|
|
129
|
+
if not result["success"]:
|
|
130
|
+
return {
|
|
131
|
+
"test": "single_objective",
|
|
132
|
+
"objective": objective,
|
|
133
|
+
"success": False,
|
|
134
|
+
"error": result.get("text", "Unknown error")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
campaigns = result.get("parsed_content", {}).get("data", [])
|
|
138
|
+
objectives_found = [c.get("objective") for c in campaigns if "objective" in c]
|
|
139
|
+
|
|
140
|
+
# Verify all campaigns match the filter
|
|
141
|
+
all_match = all(obj == objective for obj in objectives_found) if objectives_found else True
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"test": "single_objective",
|
|
145
|
+
"objective": objective,
|
|
146
|
+
"success": True,
|
|
147
|
+
"all_match_filter": all_match,
|
|
148
|
+
"campaign_count": len(campaigns),
|
|
149
|
+
"objectives": objectives_found
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def test_multiple_objectives_filter(self, account_id: str, objectives: List[str]) -> Dict[str, Any]:
|
|
153
|
+
"""Test filtering by multiple objectives"""
|
|
154
|
+
|
|
155
|
+
print(f"\n🔍 Test 3: Filter by multiple objectives: {objectives}")
|
|
156
|
+
|
|
157
|
+
result = self.get_campaigns(account_id, objective_filter=objectives)
|
|
158
|
+
|
|
159
|
+
if not result["success"]:
|
|
160
|
+
return {
|
|
161
|
+
"test": "multiple_objectives",
|
|
162
|
+
"objectives": objectives,
|
|
163
|
+
"success": False,
|
|
164
|
+
"error": result.get("text", "Unknown error")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
campaigns = result.get("parsed_content", {}).get("data", [])
|
|
168
|
+
objectives_found = [c.get("objective") for c in campaigns if "objective" in c]
|
|
169
|
+
|
|
170
|
+
# Verify all campaigns match one of the filter objectives
|
|
171
|
+
all_match = all(obj in objectives for obj in objectives_found) if objectives_found else True
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"test": "multiple_objectives",
|
|
175
|
+
"objectives": objectives,
|
|
176
|
+
"success": True,
|
|
177
|
+
"all_match_filter": all_match,
|
|
178
|
+
"campaign_count": len(campaigns),
|
|
179
|
+
"objectives_found": objectives_found,
|
|
180
|
+
"unique_objectives": list(set(objectives_found))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def test_combined_status_and_objective_filter(
|
|
184
|
+
self,
|
|
185
|
+
account_id: str,
|
|
186
|
+
status: str,
|
|
187
|
+
objective: str
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""Test filtering by both status and objective"""
|
|
190
|
+
|
|
191
|
+
print(f"\n🔍 Test 4: Filter by status '{status}' and objective '{objective}'")
|
|
192
|
+
|
|
193
|
+
result = self.get_campaigns(
|
|
194
|
+
account_id,
|
|
195
|
+
status_filter=status,
|
|
196
|
+
objective_filter=objective
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if not result["success"]:
|
|
200
|
+
return {
|
|
201
|
+
"test": "combined_filters",
|
|
202
|
+
"status": status,
|
|
203
|
+
"objective": objective,
|
|
204
|
+
"success": False,
|
|
205
|
+
"error": result.get("text", "Unknown error")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
campaigns = result.get("parsed_content", {}).get("data", [])
|
|
209
|
+
|
|
210
|
+
# Check if campaigns match both filters
|
|
211
|
+
objectives_match = all(
|
|
212
|
+
c.get("objective") == objective
|
|
213
|
+
for c in campaigns if "objective" in c
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"test": "combined_filters",
|
|
218
|
+
"status": status,
|
|
219
|
+
"objective": objective,
|
|
220
|
+
"success": True,
|
|
221
|
+
"objectives_match": objectives_match,
|
|
222
|
+
"campaign_count": len(campaigns),
|
|
223
|
+
"campaigns": [
|
|
224
|
+
{
|
|
225
|
+
"id": c.get("id"),
|
|
226
|
+
"name": c.get("name"),
|
|
227
|
+
"status": c.get("status"),
|
|
228
|
+
"objective": c.get("objective")
|
|
229
|
+
}
|
|
230
|
+
for c in campaigns
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def test_empty_string_filter(self, account_id: str) -> Dict[str, Any]:
|
|
235
|
+
"""Test that empty string filter returns all campaigns"""
|
|
236
|
+
|
|
237
|
+
print(f"\n🔍 Test 5: Empty string filter (should return all)")
|
|
238
|
+
|
|
239
|
+
result = self.get_campaigns(account_id, objective_filter="")
|
|
240
|
+
|
|
241
|
+
if not result["success"]:
|
|
242
|
+
return {
|
|
243
|
+
"test": "empty_string_filter",
|
|
244
|
+
"success": False,
|
|
245
|
+
"error": result.get("text", "Unknown error")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
campaigns = result.get("parsed_content", {}).get("data", [])
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"test": "empty_string_filter",
|
|
252
|
+
"success": True,
|
|
253
|
+
"campaign_count": len(campaigns),
|
|
254
|
+
"note": "Empty filter should return all campaigns"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def test_empty_list_filter(self, account_id: str) -> Dict[str, Any]:
|
|
258
|
+
"""Test that empty list filter returns all campaigns"""
|
|
259
|
+
|
|
260
|
+
print(f"\n🔍 Test 6: Empty list filter (should return all)")
|
|
261
|
+
|
|
262
|
+
result = self.get_campaigns(account_id, objective_filter=[])
|
|
263
|
+
|
|
264
|
+
if not result["success"]:
|
|
265
|
+
return {
|
|
266
|
+
"test": "empty_list_filter",
|
|
267
|
+
"success": False,
|
|
268
|
+
"error": result.get("text", "Unknown error")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
campaigns = result.get("parsed_content", {}).get("data", [])
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"test": "empty_list_filter",
|
|
275
|
+
"success": True,
|
|
276
|
+
"campaign_count": len(campaigns),
|
|
277
|
+
"note": "Empty list should return all campaigns"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
def run_all_tests(self, account_id: str) -> bool:
|
|
281
|
+
"""Run comprehensive campaign objective filtering tests"""
|
|
282
|
+
|
|
283
|
+
print("🚀 Campaign Objective Filtering Test Suite")
|
|
284
|
+
print("="*60)
|
|
285
|
+
|
|
286
|
+
# Check server availability
|
|
287
|
+
try:
|
|
288
|
+
response = requests.get(f"{self.base_url}/", timeout=5)
|
|
289
|
+
server_running = response.status_code in [200, 404]
|
|
290
|
+
except:
|
|
291
|
+
server_running = False
|
|
292
|
+
|
|
293
|
+
if not server_running:
|
|
294
|
+
print("❌ Server is not running at", self.base_url)
|
|
295
|
+
print(" Please start the server with:")
|
|
296
|
+
print(" python3 -m meta_ads_mcp --transport streamable-http --port 8080")
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
print("✅ Server is running")
|
|
300
|
+
print(f"🏢 Testing with account: {account_id}")
|
|
301
|
+
|
|
302
|
+
test_results = []
|
|
303
|
+
|
|
304
|
+
# Test 1: No filtering (get all campaigns to see what objectives exist)
|
|
305
|
+
result1 = self.test_no_filtering(account_id)
|
|
306
|
+
test_results.append(result1)
|
|
307
|
+
if result1["success"]:
|
|
308
|
+
print(f"✅ Found {result1['campaign_count']} campaigns")
|
|
309
|
+
print(f" Objectives: {set(result1.get('objectives', []))}")
|
|
310
|
+
available_objectives = list(set(result1.get('objectives', [])))
|
|
311
|
+
else:
|
|
312
|
+
print(f"❌ Failed: {result1.get('error')}")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
# If we have campaigns with objectives, test filtering
|
|
316
|
+
if available_objectives:
|
|
317
|
+
# Test 2: Single objective filter
|
|
318
|
+
test_objective = available_objectives[0]
|
|
319
|
+
result2 = self.test_single_objective_filter(account_id, test_objective)
|
|
320
|
+
test_results.append(result2)
|
|
321
|
+
if result2["success"]:
|
|
322
|
+
if result2["all_match_filter"]:
|
|
323
|
+
print(f"✅ Single objective filter works correctly")
|
|
324
|
+
print(f" Found {result2['campaign_count']} campaigns with objective '{test_objective}'")
|
|
325
|
+
else:
|
|
326
|
+
print(f"⚠️ Filter returned campaigns with wrong objectives")
|
|
327
|
+
print(f" Expected: {test_objective}")
|
|
328
|
+
print(f" Found: {set(result2['objectives'])}")
|
|
329
|
+
else:
|
|
330
|
+
print(f"❌ Single objective filter failed: {result2.get('error')}")
|
|
331
|
+
|
|
332
|
+
# Test 3: Multiple objectives filter (if we have at least 2 objectives)
|
|
333
|
+
if len(available_objectives) >= 2:
|
|
334
|
+
test_objectives = available_objectives[:2]
|
|
335
|
+
result3 = self.test_multiple_objectives_filter(account_id, test_objectives)
|
|
336
|
+
test_results.append(result3)
|
|
337
|
+
if result3["success"]:
|
|
338
|
+
if result3["all_match_filter"]:
|
|
339
|
+
print(f"✅ Multiple objectives filter works correctly")
|
|
340
|
+
print(f" Found {result3['campaign_count']} campaigns")
|
|
341
|
+
print(f" Unique objectives: {result3['unique_objectives']}")
|
|
342
|
+
else:
|
|
343
|
+
print(f"⚠️ Filter returned campaigns with wrong objectives")
|
|
344
|
+
print(f" Expected: {test_objectives}")
|
|
345
|
+
print(f" Found: {result3['unique_objectives']}")
|
|
346
|
+
else:
|
|
347
|
+
print(f"❌ Multiple objectives filter failed: {result3.get('error')}")
|
|
348
|
+
else:
|
|
349
|
+
print(f"ℹ️ Skipping multiple objectives test (only {len(available_objectives)} objective found)")
|
|
350
|
+
|
|
351
|
+
# Test 4: Combined status and objective filter
|
|
352
|
+
result4 = self.test_combined_status_and_objective_filter(
|
|
353
|
+
account_id,
|
|
354
|
+
"ACTIVE",
|
|
355
|
+
test_objective
|
|
356
|
+
)
|
|
357
|
+
test_results.append(result4)
|
|
358
|
+
if result4["success"]:
|
|
359
|
+
if result4["objectives_match"]:
|
|
360
|
+
print(f"✅ Combined filters work correctly")
|
|
361
|
+
print(f" Found {result4['campaign_count']} ACTIVE campaigns with objective '{test_objective}'")
|
|
362
|
+
else:
|
|
363
|
+
print(f"⚠️ Combined filter returned campaigns with wrong objectives")
|
|
364
|
+
else:
|
|
365
|
+
print(f"❌ Combined filter failed: {result4.get('error')}")
|
|
366
|
+
else:
|
|
367
|
+
print("ℹ️ No campaigns found, skipping filter tests")
|
|
368
|
+
|
|
369
|
+
# Test 5: Empty string filter
|
|
370
|
+
result5 = self.test_empty_string_filter(account_id)
|
|
371
|
+
test_results.append(result5)
|
|
372
|
+
if result5["success"]:
|
|
373
|
+
print(f"✅ Empty string filter works correctly")
|
|
374
|
+
print(f" Returned {result5['campaign_count']} campaigns (same as no filter)")
|
|
375
|
+
else:
|
|
376
|
+
print(f"❌ Empty string filter failed: {result5.get('error')}")
|
|
377
|
+
|
|
378
|
+
# Test 6: Empty list filter
|
|
379
|
+
result6 = self.test_empty_list_filter(account_id)
|
|
380
|
+
test_results.append(result6)
|
|
381
|
+
if result6["success"]:
|
|
382
|
+
print(f"✅ Empty list filter works correctly")
|
|
383
|
+
print(f" Returned {result6['campaign_count']} campaigns (same as no filter)")
|
|
384
|
+
else:
|
|
385
|
+
print(f"❌ Empty list filter failed: {result6.get('error')}")
|
|
386
|
+
|
|
387
|
+
# Final assessment
|
|
388
|
+
print("\n" + "="*60)
|
|
389
|
+
print("📊 FINAL RESULTS")
|
|
390
|
+
print("="*60)
|
|
391
|
+
|
|
392
|
+
successful_tests = sum(1 for r in test_results if r.get("success", False))
|
|
393
|
+
total_tests = len(test_results)
|
|
394
|
+
|
|
395
|
+
if successful_tests == total_tests:
|
|
396
|
+
print(f"✅ All {total_tests} tests passed!")
|
|
397
|
+
return True
|
|
398
|
+
else:
|
|
399
|
+
print(f"⚠️ {successful_tests}/{total_tests} tests passed")
|
|
400
|
+
failed_tests = [r for r in test_results if not r.get("success", False)]
|
|
401
|
+
print(f" Failed tests: {[r.get('test') for r in failed_tests]}")
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# Pytest-compatible test functions
|
|
406
|
+
@pytest.fixture
|
|
407
|
+
def tester(server_url):
|
|
408
|
+
"""Create a tester instance"""
|
|
409
|
+
return CampaignObjectiveFilterTester(server_url)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@pytest.fixture
|
|
413
|
+
def account_id():
|
|
414
|
+
"""Default test account ID"""
|
|
415
|
+
return "act_701351919139047"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_server_running(check_server_running):
|
|
419
|
+
"""Verify the server is running before tests"""
|
|
420
|
+
assert check_server_running
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_no_filtering(tester, account_id, check_server_running):
|
|
424
|
+
"""Test getting campaigns without filtering"""
|
|
425
|
+
result = tester.test_no_filtering(account_id)
|
|
426
|
+
assert result["success"], f"Failed: {result.get('error')}"
|
|
427
|
+
assert isinstance(result["campaign_count"], int)
|
|
428
|
+
print(f"Found {result['campaign_count']} campaigns")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_single_objective_filter(tester, account_id, check_server_running):
|
|
432
|
+
"""Test filtering by a single objective"""
|
|
433
|
+
# First get available objectives
|
|
434
|
+
no_filter_result = tester.test_no_filtering(account_id)
|
|
435
|
+
assert no_filter_result["success"]
|
|
436
|
+
|
|
437
|
+
objectives = no_filter_result.get("objectives", [])
|
|
438
|
+
if not objectives:
|
|
439
|
+
pytest.skip("No campaigns with objectives found")
|
|
440
|
+
|
|
441
|
+
test_objective = objectives[0]
|
|
442
|
+
result = tester.test_single_objective_filter(account_id, test_objective)
|
|
443
|
+
assert result["success"], f"Failed: {result.get('error')}"
|
|
444
|
+
assert result["all_match_filter"], "Filter returned campaigns with wrong objectives"
|
|
445
|
+
print(f"Single objective filter: {result['campaign_count']} campaigns with {test_objective}")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_multiple_objectives_filter(tester, account_id, check_server_running):
|
|
449
|
+
"""Test filtering by multiple objectives"""
|
|
450
|
+
# First get available objectives
|
|
451
|
+
no_filter_result = tester.test_no_filtering(account_id)
|
|
452
|
+
assert no_filter_result["success"]
|
|
453
|
+
|
|
454
|
+
available_objectives = list(set(no_filter_result.get("objectives", [])))
|
|
455
|
+
if len(available_objectives) < 2:
|
|
456
|
+
pytest.skip("Need at least 2 different objectives to test")
|
|
457
|
+
|
|
458
|
+
test_objectives = available_objectives[:2]
|
|
459
|
+
result = tester.test_multiple_objectives_filter(account_id, test_objectives)
|
|
460
|
+
assert result["success"], f"Failed: {result.get('error')}"
|
|
461
|
+
assert result["all_match_filter"], "Filter returned campaigns with wrong objectives"
|
|
462
|
+
print(f"Multiple objectives filter: {result['campaign_count']} campaigns")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_combined_filters(tester, account_id, check_server_running):
|
|
466
|
+
"""Test filtering by both status and objective"""
|
|
467
|
+
# First get available objectives
|
|
468
|
+
no_filter_result = tester.test_no_filtering(account_id)
|
|
469
|
+
assert no_filter_result["success"]
|
|
470
|
+
|
|
471
|
+
objectives = no_filter_result.get("objectives", [])
|
|
472
|
+
if not objectives:
|
|
473
|
+
pytest.skip("No campaigns with objectives found")
|
|
474
|
+
|
|
475
|
+
test_objective = objectives[0]
|
|
476
|
+
result = tester.test_combined_status_and_objective_filter(
|
|
477
|
+
account_id,
|
|
478
|
+
"ACTIVE",
|
|
479
|
+
test_objective
|
|
480
|
+
)
|
|
481
|
+
assert result["success"], f"Failed: {result.get('error')}"
|
|
482
|
+
print(f"Combined filters: {result['campaign_count']} campaigns")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_empty_string_filter(tester, account_id, check_server_running):
|
|
486
|
+
"""Test that empty string filter returns all campaigns"""
|
|
487
|
+
result = tester.test_empty_string_filter(account_id)
|
|
488
|
+
assert result["success"], f"Failed: {result.get('error')}"
|
|
489
|
+
print(f"Empty string filter: {result['campaign_count']} campaigns")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_empty_list_filter(tester, account_id, check_server_running):
|
|
493
|
+
"""Test that empty list filter returns all campaigns"""
|
|
494
|
+
result = tester.test_empty_list_filter(account_id)
|
|
495
|
+
assert result["success"], f"Failed: {result.get('error')}"
|
|
496
|
+
print(f"Empty list filter: {result['campaign_count']} campaigns")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def main():
|
|
500
|
+
"""Main test execution for standalone running"""
|
|
501
|
+
import sys
|
|
502
|
+
|
|
503
|
+
account_id = "act_701351919139047" # Default test account
|
|
504
|
+
|
|
505
|
+
tester = CampaignObjectiveFilterTester()
|
|
506
|
+
success = tester.run_all_tests(account_id)
|
|
507
|
+
|
|
508
|
+
if success:
|
|
509
|
+
print("\n🎉 All campaign objective filtering tests passed!")
|
|
510
|
+
sys.exit(0)
|
|
511
|
+
else:
|
|
512
|
+
print("\n⚠️ Some tests failed - see details above")
|
|
513
|
+
sys.exit(1)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
if __name__ == "__main__":
|
|
517
|
+
main()
|
|
518
|
+
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|