Files
maskanx_cm_backend/tests/test_campaign_delete.py
T

167 lines
5.1 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""Deleting a campaign must not leave objects spending on Meta.
The dangerous case is a campaign forgotten in MaskanX but still live on
Meta: it keeps drawing budget with nothing left here to show it exists.
So Meta is deleted first, and a failure there keeps the local row.
The opposite mistake matters too: deleting an imported campaign here must
not destroy work the operator did in Ads Manager.
"""
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from adclaw.app.routers import campaigns as campaigns_router
from adclaw.campaigns.models import CampaignSpec
from adclaw.campaigns.sync import unsync_campaign
from adclaw.meta.client import MetaClient, MetaError
class FakeRepo:
def __init__(self):
self.items: dict[str, CampaignSpec] = {}
self.deleted: list[str] = []
async def get_campaign(self, campaign_id):
return self.items.get(campaign_id)
async def delete_campaign(self, campaign_id):
self.deleted.append(campaign_id)
self.items.pop(campaign_id, None)
class FakeMeta:
def __init__(self, error: MetaError | None = None):
self.error = error
self.deleted: list[str] = []
async def delete_object(self, object_id):
if self.error:
raise self.error
self.deleted.append(object_id)
def _campaign(**overrides) -> CampaignSpec:
data = {
"id": "camp_1",
"name": "Q3 lead gen",
"status": "live",
"origin": "maskanx",
"ad_account_id": "act_1",
"meta_campaign_id": "meta_camp_1",
"budget": {"daily_budget": 10000},
}
data.update(overrides)
return CampaignSpec(**data)
@pytest.fixture()
def env(monkeypatch):
repo, meta = FakeRepo(), FakeMeta()
monkeypatch.setattr(campaigns_router, "get_repository", lambda: repo)
monkeypatch.setattr(campaigns_router, "get_meta_client", lambda: meta)
app = FastAPI()
app.include_router(campaigns_router.router, prefix="/api")
return TestClient(app), repo, meta
def test_delete_removes_the_campaign_on_meta_as_well(env):
client, repo, meta = env
repo.items["camp_1"] = _campaign()
response = client.delete("/api/campaigns/camp_1")
assert response.status_code == 204
assert meta.deleted == ["meta_camp_1"]
assert repo.deleted == ["camp_1"]
def test_delete_keeps_the_local_row_when_meta_delete_fails(env):
"""Forgetting it here while it still spends there is the worst outcome."""
client, repo, meta = env
meta.error = MetaError("Permission denied", code=200)
repo.items["camp_1"] = _campaign()
response = client.delete("/api/campaigns/camp_1")
assert response.status_code == 502
assert "Ads Manager" in response.json()["detail"]
assert repo.deleted == []
assert "camp_1" in repo.items
def test_delete_of_an_imported_campaign_is_refused(env):
"""Deleting it on Meta would destroy Ads Manager work; deleting only the
local row would achieve nothing, since the reconciler re-imports it."""
client, repo, meta = env
repo.items["camp_1"] = _campaign(origin="imported")
response = client.delete("/api/campaigns/camp_1")
assert response.status_code == 409
assert "Ads Manager" in response.json()["detail"]
assert meta.deleted == []
assert repo.deleted == []
def test_delete_of_an_unsynced_campaign_does_not_call_meta(env):
client, repo, meta = env
repo.items["camp_1"] = _campaign(status="draft", meta_campaign_id=None)
response = client.delete("/api/campaigns/camp_1")
assert response.status_code == 204
assert meta.deleted == []
assert repo.deleted == ["camp_1"]
def test_delete_of_a_missing_campaign_is_404(env):
client, repo, meta = env
response = client.delete("/api/campaigns/nope")
assert response.status_code == 404
assert meta.deleted == []
async def test_unsync_reports_whether_it_deleted():
meta = FakeMeta()
assert await unsync_campaign(meta, _campaign()) is True
assert await unsync_campaign(meta, _campaign(origin="imported")) is False
assert await unsync_campaign(meta, _campaign(meta_campaign_id=None)) is False
assert meta.deleted == ["meta_camp_1"]
# --- client transport ---
async def test_delete_object_issues_a_graph_delete():
calls = []
async def transport(method, url, params):
calls.append((method, url))
return {"success": True}
await MetaClient("tok", transport=transport).delete_object("meta_camp_1")
assert calls == [("DELETE", "https://graph.facebook.com/v23.0/meta_camp_1")]
async def test_delete_object_treats_an_absent_object_as_deleted():
"""Retrying a partially-failed cleanup must not fail on the done part."""
async def transport(method, url, params):
return {"error": {"message": "Unsupported get request.", "code": 100}}
await MetaClient("tok", transport=transport).delete_object("gone")
async def test_delete_object_raises_on_any_other_meta_error():
async def transport(method, url, params):
return {"error": {"message": "Permission denied", "code": 200}}
with pytest.raises(MetaError):
await MetaClient("tok", transport=transport).delete_object("meta_camp_1")