# -*- coding: utf-8 -*- """Sync one campaign into the real ad account, then delete it. Opt-in: set MASKANX_LIVE_TESTS=1. Every object is created PAUSED, so the test cannot spend money, and the objects are deleted in a finally block so the account is left as it was found. This exists because the unit tests all run against a fake transport. They prove the code sends what we think it sends; only this proves Meta accepts it and that what lands in the account is paused. """ from __future__ import annotations import os import sys import uuid import pytest from adclaw.campaigns.models import CampaignSpec from adclaw.campaigns.sync import sync_campaign, unsync_campaign from adclaw.envs.store import load_envs_into_environ from adclaw.meta.client import CREATE_STATUS, MetaClient, access_token_from_env if sys.platform == "win32": import asyncio asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) LIVE = os.environ.get("MASKANX_LIVE_TESTS") == "1" AD_ACCOUNT_ENV = "MASKANX_LIVE_AD_ACCOUNT_ID" pytestmark = pytest.mark.skipif( not LIVE, reason=( "Live Meta tests are opt-in. Set MASKANX_LIVE_TESTS=1 and " f"{AD_ACCOUNT_ENV}=act_... to run them. They create and delete " "real (paused) objects in that ad account." ), ) class _MemoryRepo: """Stands in for the database so the live test needs only Meta.""" def __init__(self): self.events: list[str] = [] async def update_campaign_with_event( self, spec, event_type, actor=None, reason=None, payload=None, ): self.events.append(event_type) return spec def _live_campaign(ad_account_id: str) -> CampaignSpec: return CampaignSpec( id=f"live_{uuid.uuid4().hex[:12]}", name=f"MaskanX live smoke {uuid.uuid4().hex[:6]}", status="approved", origin="maskanx", objective="OUTCOME_TRAFFIC", ad_account_id=ad_account_id, approved_by="live-smoke-test", # Comfortably above the account minimum; never spent, since the # objects stay paused for their whole (short) life. budget={"daily_budget": 50000}, targeting={"countries": ["IN"], "age_min": 25, "age_max": 55}, advanced={ "message": "MaskanX live smoke test. Paused; delete on sight.", "headline": "MaskanX smoke test", "link": "https://maskan.technology", }, channels=["facebook"], ) @pytest.fixture() def live_account() -> str: load_envs_into_environ() account = (os.environ.get(AD_ACCOUNT_ENV) or "").strip() if not account: pytest.skip(f"{AD_ACCOUNT_ENV} is not set.") return account async def test_sync_creates_paused_objects_and_delete_removes_them(live_account): meta = MetaClient(access_token=access_token_from_env()) campaign = _live_campaign(live_account) synced = None try: synced = await sync_campaign( _MemoryRepo(), meta, campaign, actor="live-smoke-test", ) assert synced.sync_status == "synced" assert synced.status == "synced" assert synced.meta_campaign_id state = synced.advanced["meta_sync"] assert state["adset_id"] and state["creative_id"] and state["ad_id"] # What actually landed in the account is what matters, so read it # back from Graph rather than trusting the create responses. for object_id in ( synced.meta_campaign_id, state["adset_id"], state["ad_id"], ): live = await meta._get(f"/{object_id}", {"fields": "id,status"}) assert live["status"] == CREATE_STATUS, ( f"{object_id} is {live['status']}, not {CREATE_STATUS} — " f"this object could be spending money." ) finally: if synced and synced.meta_campaign_id: await unsync_campaign(meta, synced) # Deleting the campaign cascades, so nothing from this test should be # left anywhere in the account. remaining = {c["id"] for c in await meta.list_campaigns(live_account)} assert synced.meta_campaign_id not in remaining ad_sets = {a["id"] for a in await meta.list_ad_sets(live_account)} assert synced.advanced["meta_sync"]["adset_id"] not in ad_sets ads = {a["id"] for a in await meta.list_ads(live_account)} assert synced.advanced["meta_sync"]["ad_id"] not in ads