@@ -1683,3 +1683,93 @@ async def test_videos_array_proceeds_when_thumbnail_fetch_fails():
16831683 # No thumbnail_url on the entry — graceful degradation.
16841684 assert "thumbnail_url" not in videos_out [0 ]
16851685 assert videos_out [0 ]["video_id" ] == "vid_no_thumb"
1686+
1687+
1688+ @pytest .mark .asyncio
1689+ async def test_video_creative_with_display_url_uses_asset_feed_link_urls ():
1690+ """display_url routes a video creative through asset_feed_spec.link_urls.
1691+
1692+ Meta only accepts display_url on asset_feed_spec.link_urls[].display_url, not
1693+ inside object_story_spec.video_data, so setting it forces the asset_feed_spec
1694+ path.
1695+ """
1696+
1697+ with patch ('meta_ads_mcp.core.ads.make_api_request' ) as mock_api , \
1698+ patch ('meta_ads_mcp.core.ads._discover_pages_for_account' ) as mock_discover :
1699+
1700+ mock_discover .return_value = {
1701+ "success" : True ,
1702+ "page_id" : "123456789" ,
1703+ "page_name" : "Test Page" ,
1704+ }
1705+
1706+ mock_api .side_effect = [
1707+ # 1) Auto-fetch video thumbnail (no thumbnail_url provided)
1708+ {"picture" : "https://example.com/auto-thumb.jpg" },
1709+ # 2) POST create creative
1710+ {"id" : "creative_display_url_1" },
1711+ # 3) GET creative details
1712+ {"id" : "creative_display_url_1" , "name" : "Display URL Creative" , "status" : "ACTIVE" },
1713+ ]
1714+
1715+ result = await create_ad_creative (
1716+ account_id = "act_123456" ,
1717+ video_id = "vid_987654" ,
1718+ name = "Display URL Ad" ,
1719+ link_url = "https://example.com/landing" ,
1720+ display_url = "brand.example" ,
1721+ message = "Check out this video" ,
1722+ headline = "Watch Now" ,
1723+ call_to_action_type = "LEARN_MORE" ,
1724+ access_token = "test_token" ,
1725+ )
1726+
1727+ creative_data = mock_api .call_args_list [1 ][0 ][2 ]
1728+
1729+ # Routed through asset_feed_spec, not the simple video_data path.
1730+ assert "asset_feed_spec" in creative_data
1731+ assert "video_data" not in creative_data .get ("object_story_spec" , {})
1732+
1733+ link_urls = creative_data ["asset_feed_spec" ]["link_urls" ]
1734+ assert link_urls == [
1735+ {"website_url" : "https://example.com/landing" , "display_url" : "brand.example" }
1736+ ]
1737+
1738+
1739+ @pytest .mark .asyncio
1740+ async def test_image_creative_with_display_url_uses_asset_feed_link_urls ():
1741+ """display_url is also supported for image creatives via asset_feed_spec.link_urls."""
1742+
1743+ with patch ('meta_ads_mcp.core.ads.make_api_request' ) as mock_api , \
1744+ patch ('meta_ads_mcp.core.ads._discover_pages_for_account' ) as mock_discover :
1745+
1746+ mock_discover .return_value = {
1747+ "success" : True ,
1748+ "page_id" : "123456789" ,
1749+ "page_name" : "Test Page" ,
1750+ }
1751+
1752+ mock_api .side_effect = [
1753+ # 1) POST create creative
1754+ {"id" : "creative_display_url_img" },
1755+ # 2) GET creative details
1756+ {"id" : "creative_display_url_img" , "name" : "Display URL Image" , "status" : "ACTIVE" },
1757+ ]
1758+
1759+ result = await create_ad_creative (
1760+ account_id = "act_123456" ,
1761+ image_hash = "abc123" ,
1762+ name = "Display URL Image Ad" ,
1763+ link_url = "https://example.com/landing" ,
1764+ display_url = "brand.example" ,
1765+ message = "Body" ,
1766+ access_token = "test_token" ,
1767+ )
1768+
1769+ creative_data = mock_api .call_args_list [0 ][0 ][2 ]
1770+ assert "asset_feed_spec" in creative_data
1771+ link_urls = creative_data ["asset_feed_spec" ]["link_urls" ]
1772+ assert link_urls == [
1773+ {"website_url" : "https://example.com/landing" , "display_url" : "brand.example" }
1774+ ]
1775+ assert creative_data ["asset_feed_spec" ]["images" ] == [{"hash" : "abc123" }]
0 commit comments