meta-ads-mcp 0.10.1__tar.gz → 0.10.5__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.10.1 → meta_ads_mcp-0.10.5}/PKG-INFO +7 -17
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/README.md +6 -16
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/ads.py +77 -26
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/adsets.py +4 -3
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/pyproject.toml +1 -1
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_mobile_app_adset_creation.py +51 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/.gitignore +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/Dockerfile +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/LICENSE +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/RELEASE.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/examples/README.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/future_improvements.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/api.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/authentication.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/duplication.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/openai_deep_research.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/requirements.txt +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/setup.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/smithery.yaml +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/README.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/__init__.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/conftest.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/e2e_account_info_search_issue.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_account_info_access_fix.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_budget_update.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_dsa_beneficiary.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_dsa_integration.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_duplication.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_duplication_regression.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_dynamic_creatives.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_estimate_audience_size.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_estimate_audience_size_e2e.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_get_account_pages.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_insights_actions_and_values.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_mobile_app_adset_issue.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_openai.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_page_discovery.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_page_discovery_integration.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_targeting.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_targeting_search_e2e.py +0 -0
- {meta_ads_mcp-0.10.1 → meta_ads_mcp-0.10.5}/tests/test_update_ad_creative_id.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.5
|
|
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
|
|
@@ -410,24 +410,14 @@ For local installation configuration, authentication options, and advanced techn
|
|
|
410
410
|
|
|
411
411
|
## Licensing
|
|
412
412
|
|
|
413
|
-
Meta Ads MCP is licensed under the [Business Source License 1.1](LICENSE)
|
|
413
|
+
Meta Ads MCP is licensed under the [Business Source License 1.1](LICENSE), which means:
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
- ✅ **
|
|
417
|
-
- ✅ **
|
|
418
|
-
- ✅ **
|
|
419
|
-
- ✅ **Redistribute** - Share the software with others
|
|
420
|
-
- ✅ **Create derivative works** - Build upon the codebase
|
|
415
|
+
- ✅ **Free to use** for individual and business purposes
|
|
416
|
+
- ✅ **Modify and customize** as needed
|
|
417
|
+
- ✅ **Redistribute** to others
|
|
418
|
+
- ✅ **Becomes fully open source** (Apache 2.0) on January 1, 2029
|
|
421
419
|
|
|
422
|
-
|
|
423
|
-
- ❌ **Offer as competing SaaS** - Cannot offer this as a hosted service that competes with ARTELL SOLUÇÕES TECNOLÓGICAS LTDA's commercial offerings
|
|
424
|
-
|
|
425
|
-
### 🔄 **Future Open Source:**
|
|
426
|
-
- **Change Date**: January 1, 2029
|
|
427
|
-
- **After Change Date**: Automatically converts to Apache License 2.0 (fully open source)
|
|
428
|
-
- **No restrictions**: After the change date, you can use it for any purpose, including competing services
|
|
429
|
-
|
|
430
|
-
This licensing model ensures the software remains accessible while protecting the commercial interests of the original developers. For questions about commercial licensing or use cases, please contact us.
|
|
420
|
+
The only restriction is that you cannot offer this as a competing hosted service. For questions about commercial licensing, please contact us.
|
|
431
421
|
|
|
432
422
|
## Privacy and Security
|
|
433
423
|
|
|
@@ -385,24 +385,14 @@ For local installation configuration, authentication options, and advanced techn
|
|
|
385
385
|
|
|
386
386
|
## Licensing
|
|
387
387
|
|
|
388
|
-
Meta Ads MCP is licensed under the [Business Source License 1.1](LICENSE)
|
|
388
|
+
Meta Ads MCP is licensed under the [Business Source License 1.1](LICENSE), which means:
|
|
389
389
|
|
|
390
|
-
|
|
391
|
-
- ✅ **
|
|
392
|
-
- ✅ **
|
|
393
|
-
- ✅ **
|
|
394
|
-
- ✅ **Redistribute** - Share the software with others
|
|
395
|
-
- ✅ **Create derivative works** - Build upon the codebase
|
|
390
|
+
- ✅ **Free to use** for individual and business purposes
|
|
391
|
+
- ✅ **Modify and customize** as needed
|
|
392
|
+
- ✅ **Redistribute** to others
|
|
393
|
+
- ✅ **Becomes fully open source** (Apache 2.0) on January 1, 2029
|
|
396
394
|
|
|
397
|
-
|
|
398
|
-
- ❌ **Offer as competing SaaS** - Cannot offer this as a hosted service that competes with ARTELL SOLUÇÕES TECNOLÓGICAS LTDA's commercial offerings
|
|
399
|
-
|
|
400
|
-
### 🔄 **Future Open Source:**
|
|
401
|
-
- **Change Date**: January 1, 2029
|
|
402
|
-
- **After Change Date**: Automatically converts to Apache License 2.0 (fully open source)
|
|
403
|
-
- **No restrictions**: After the change date, you can use it for any purpose, including competing services
|
|
404
|
-
|
|
405
|
-
This licensing model ensures the software remains accessible while protecting the commercial interests of the original developers. For questions about commercial licensing or use cases, please contact us.
|
|
395
|
+
The only restriction is that you cannot offer this as a competing hosted service. For questions about commercial licensing, please contact us.
|
|
406
396
|
|
|
407
397
|
## Privacy and Security
|
|
408
398
|
|
|
@@ -564,7 +564,8 @@ async def update_ad(
|
|
|
564
564
|
async def upload_ad_image(
|
|
565
565
|
access_token: str = None,
|
|
566
566
|
account_id: str = None,
|
|
567
|
-
|
|
567
|
+
file: str = None,
|
|
568
|
+
image_url: str = None,
|
|
568
569
|
name: str = None
|
|
569
570
|
) -> str:
|
|
570
571
|
"""
|
|
@@ -573,7 +574,8 @@ async def upload_ad_image(
|
|
|
573
574
|
Args:
|
|
574
575
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
575
576
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
576
|
-
|
|
577
|
+
file: Data URL or raw base64 string of the image (e.g., "data:image/png;base64,iVBORw0KG...")
|
|
578
|
+
image_url: Direct URL to an image to fetch and upload
|
|
577
579
|
name: Optional name for the image (default: filename)
|
|
578
580
|
|
|
579
581
|
Returns:
|
|
@@ -583,45 +585,94 @@ async def upload_ad_image(
|
|
|
583
585
|
if not account_id:
|
|
584
586
|
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
585
587
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
+
# Ensure we have image data
|
|
589
|
+
if not file and not image_url:
|
|
590
|
+
return json.dumps({"error": "Provide either 'file' (data URL or base64) or 'image_url'"}, indent=2)
|
|
588
591
|
|
|
589
592
|
# Ensure account_id has the 'act_' prefix for API compatibility
|
|
590
593
|
if not account_id.startswith("act_"):
|
|
591
594
|
account_id = f"act_{account_id}"
|
|
592
595
|
|
|
593
|
-
# Check if image file exists
|
|
594
|
-
if not os.path.exists(image_path):
|
|
595
|
-
return json.dumps({"error": f"Image file not found: {image_path}"}, indent=2)
|
|
596
|
-
|
|
597
596
|
try:
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
597
|
+
# Determine encoded_image (base64 string without data URL prefix) and a sensible name
|
|
598
|
+
encoded_image: str = ""
|
|
599
|
+
inferred_name: str = name or ""
|
|
600
|
+
|
|
601
|
+
if file:
|
|
602
|
+
# Support data URL (e.g., data:image/png;base64,...) and raw base64
|
|
603
|
+
data_url_prefix = "data:"
|
|
604
|
+
base64_marker = "base64,"
|
|
605
|
+
if file.startswith(data_url_prefix) and base64_marker in file:
|
|
606
|
+
header, base64_payload = file.split(base64_marker, 1)
|
|
607
|
+
encoded_image = base64_payload.strip()
|
|
608
|
+
|
|
609
|
+
# Infer file extension from MIME type if name not provided
|
|
610
|
+
if not inferred_name:
|
|
611
|
+
# Example header: data:image/png;...
|
|
612
|
+
mime_type = header[len(data_url_prefix):].split(";")[0].strip()
|
|
613
|
+
extension_map = {
|
|
614
|
+
"image/png": ".png",
|
|
615
|
+
"image/jpeg": ".jpg",
|
|
616
|
+
"image/jpg": ".jpg",
|
|
617
|
+
"image/webp": ".webp",
|
|
618
|
+
"image/gif": ".gif",
|
|
619
|
+
"image/bmp": ".bmp",
|
|
620
|
+
"image/tiff": ".tiff",
|
|
621
|
+
}
|
|
622
|
+
ext = extension_map.get(mime_type, ".png")
|
|
623
|
+
inferred_name = f"upload{ext}"
|
|
624
|
+
else:
|
|
625
|
+
# Assume it's already raw base64
|
|
626
|
+
encoded_image = file.strip()
|
|
627
|
+
if not inferred_name:
|
|
628
|
+
inferred_name = "upload.png"
|
|
629
|
+
else:
|
|
630
|
+
# Download image from URL
|
|
631
|
+
try:
|
|
632
|
+
image_bytes = await try_multiple_download_methods(image_url)
|
|
633
|
+
except Exception as download_error:
|
|
634
|
+
return json.dumps({
|
|
635
|
+
"error": "Failed to download image from URL",
|
|
636
|
+
"details": str(download_error),
|
|
637
|
+
"image_url": image_url,
|
|
638
|
+
}, indent=2)
|
|
639
|
+
|
|
640
|
+
if not image_bytes:
|
|
641
|
+
return json.dumps({
|
|
642
|
+
"error": "No data returned when downloading image from URL",
|
|
643
|
+
"image_url": image_url,
|
|
644
|
+
}, indent=2)
|
|
645
|
+
|
|
646
|
+
import base64 # Local import
|
|
647
|
+
encoded_image = base64.b64encode(image_bytes).decode("utf-8")
|
|
648
|
+
|
|
649
|
+
# Infer name from URL if not provided
|
|
650
|
+
if not inferred_name:
|
|
651
|
+
try:
|
|
652
|
+
path_no_query = image_url.split("?")[0]
|
|
653
|
+
filename_from_url = os.path.basename(path_no_query)
|
|
654
|
+
inferred_name = filename_from_url if filename_from_url else "upload.jpg"
|
|
655
|
+
except Exception:
|
|
656
|
+
inferred_name = "upload.jpg"
|
|
657
|
+
|
|
658
|
+
# Final name resolution
|
|
659
|
+
final_name = name or inferred_name or "upload.png"
|
|
660
|
+
|
|
606
661
|
# Prepare the API endpoint for uploading images
|
|
607
662
|
endpoint = f"{account_id}/adimages"
|
|
608
|
-
|
|
609
|
-
#
|
|
610
|
-
import base64
|
|
611
|
-
encoded_image = base64.b64encode(image_bytes).decode('utf-8')
|
|
612
|
-
|
|
613
|
-
# Prepare POST parameters
|
|
663
|
+
|
|
664
|
+
# Prepare POST parameters expected by Meta API
|
|
614
665
|
params = {
|
|
615
666
|
"bytes": encoded_image,
|
|
616
|
-
"name":
|
|
667
|
+
"name": final_name,
|
|
617
668
|
}
|
|
618
|
-
|
|
669
|
+
|
|
619
670
|
# Make API request to upload the image
|
|
620
671
|
print(f"Uploading image to Facebook Ad Account {account_id}")
|
|
621
672
|
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
622
|
-
|
|
673
|
+
|
|
623
674
|
return json.dumps(data, indent=2)
|
|
624
|
-
|
|
675
|
+
|
|
625
676
|
except Exception as e:
|
|
626
677
|
return json.dumps({
|
|
627
678
|
"error": "Failed to upload image",
|
|
@@ -130,8 +130,9 @@ async def create_adset(
|
|
|
130
130
|
promoted_object: Mobile app configuration for APP_INSTALLS campaigns. Required fields: application_id, object_store_url.
|
|
131
131
|
Optional fields: custom_event_type, pixel_id, page_id.
|
|
132
132
|
Example: {"application_id": "123456789012345", "object_store_url": "https://apps.apple.com/app/id123456789"}
|
|
133
|
-
destination_type: Where users are directed after clicking the ad (e.g., 'APP_STORE', 'DEEPLINK', 'APP_INSTALL').
|
|
134
|
-
Required for mobile app campaigns.
|
|
133
|
+
destination_type: Where users are directed after clicking the ad (e.g., 'APP_STORE', 'DEEPLINK', 'APP_INSTALL', 'ON_AD').
|
|
134
|
+
Required for mobile app campaigns and lead generation campaigns.
|
|
135
|
+
Use 'ON_AD' for lead generation campaigns where user interaction happens within the ad.
|
|
135
136
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
136
137
|
"""
|
|
137
138
|
# Check required parameters
|
|
@@ -196,7 +197,7 @@ async def create_adset(
|
|
|
196
197
|
|
|
197
198
|
# Validate destination_type if provided
|
|
198
199
|
if destination_type:
|
|
199
|
-
valid_destination_types = ["APP_STORE", "DEEPLINK", "APP_INSTALL"]
|
|
200
|
+
valid_destination_types = ["APP_STORE", "DEEPLINK", "APP_INSTALL", "ON_AD"]
|
|
200
201
|
if destination_type not in valid_destination_types:
|
|
201
202
|
return json.dumps({
|
|
202
203
|
"error": f"Invalid destination_type: {destination_type}",
|
|
@@ -409,6 +409,57 @@ class TestMobileAppAdsetCreation:
|
|
|
409
409
|
params = call_args[0][2]
|
|
410
410
|
assert params['destination_type'] == "APP_INSTALL"
|
|
411
411
|
|
|
412
|
+
@pytest.mark.asyncio
|
|
413
|
+
async def test_on_ad_destination_type_for_lead_generation(
|
|
414
|
+
self, mock_api_request, mock_auth_manager, valid_mobile_app_params
|
|
415
|
+
):
|
|
416
|
+
"""Test ON_AD destination_type for lead generation campaigns (Issue #009 fix)"""
|
|
417
|
+
|
|
418
|
+
# Create a lead generation adset configuration (without promoted_object since it's for lead gen, not mobile apps)
|
|
419
|
+
lead_gen_params = valid_mobile_app_params.copy()
|
|
420
|
+
lead_gen_params.update({
|
|
421
|
+
"optimization_goal": "LEAD_GENERATION",
|
|
422
|
+
"billing_event": "IMPRESSIONS"
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
result = await create_adset(
|
|
426
|
+
**lead_gen_params,
|
|
427
|
+
destination_type="ON_AD"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Should pass validation and include destination_type in API call
|
|
431
|
+
call_args = mock_api_request.call_args
|
|
432
|
+
params = call_args[0][2]
|
|
433
|
+
assert params['destination_type'] == "ON_AD"
|
|
434
|
+
|
|
435
|
+
@pytest.mark.asyncio
|
|
436
|
+
async def test_on_ad_validation_passes(
|
|
437
|
+
self, mock_api_request, mock_auth_manager, valid_mobile_app_params
|
|
438
|
+
):
|
|
439
|
+
"""Test that ON_AD destination_type passes validation (Issue #009 regression test)"""
|
|
440
|
+
|
|
441
|
+
# Use parameters that work with ON_AD (lead generation, not mobile app)
|
|
442
|
+
lead_gen_params = valid_mobile_app_params.copy()
|
|
443
|
+
lead_gen_params.update({
|
|
444
|
+
"optimization_goal": "LEAD_GENERATION",
|
|
445
|
+
"billing_event": "IMPRESSIONS"
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
result = await create_adset(
|
|
449
|
+
**lead_gen_params,
|
|
450
|
+
destination_type="ON_AD"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
result_data = json.loads(result)
|
|
454
|
+
|
|
455
|
+
# Should NOT return a validation error about destination_type
|
|
456
|
+
# Before the fix, this would return: "Invalid destination_type: ON_AD"
|
|
457
|
+
if "data" in result_data:
|
|
458
|
+
error_data = json.loads(result_data["data"])
|
|
459
|
+
assert "error" not in error_data or "destination_type" not in error_data.get("error", "").lower()
|
|
460
|
+
else:
|
|
461
|
+
assert "error" not in result_data or "destination_type" not in result_data.get("error", "").lower()
|
|
462
|
+
|
|
412
463
|
# Test: Error Handling
|
|
413
464
|
@pytest.mark.asyncio
|
|
414
465
|
async def test_meta_api_error_handling(
|
|
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
|