Files
maskanx_cm_backend/tests/test_meta_client.py
T
AFFAANh 985527a451 fix(meta): cover access-token env handling, JSON-encoded creative, and previews fail-fast contract
Address code review findings on the read-only Meta Graph client: add
monkeypatch-based tests for access_token_from_env/MetaNotConfiguredError,
assert generate_previews JSON-encodes the creative param, pin the
existing fail-fast-on-partial-error contract with a dedicated test, and
declare httpx as an explicit dependency since it was only resolving
transitively.
2026-08-01 13:21:57 +05:30

141 lines
4.5 KiB
Python

# -*- coding: utf-8 -*-
"""Read-only Meta Graph client behaviour."""
import json
import pytest
from adclaw.meta.client import (
ACCESS_TOKEN_ENV,
MetaClient,
MetaError,
MetaNotConfiguredError,
access_token_from_env,
)
class FakeTransport:
"""Records calls and returns queued responses."""
def __init__(self, responses):
self.responses = list(responses)
self.calls = []
async def __call__(self, method, url, params):
self.calls.append({"method": method, "url": url, "params": params})
return self.responses.pop(0)
@pytest.mark.asyncio
async def test_get_ad_account_requests_expected_fields():
transport = FakeTransport([{"id": "act_1", "balance": "0"}])
client = MetaClient(access_token="tok", transport=transport)
result = await client.get_ad_account("act_1")
assert result["balance"] == "0"
call = transport.calls[0]
assert call["method"] == "GET"
assert call["url"].endswith("/v23.0/act_1")
assert "min_daily_budget" in call["params"]["fields"]
assert call["params"]["access_token"] == "tok"
@pytest.mark.asyncio
async def test_graph_error_is_raised_with_code_and_subcode():
transport = FakeTransport([
{"error": {"message": "Bad thing", "code": 100, "error_subcode": 33}},
])
client = MetaClient(access_token="tok", transport=transport)
with pytest.raises(MetaError) as excinfo:
await client.get_ad_account("act_1")
assert excinfo.value.code == 100
assert excinfo.value.subcode == 33
assert "Bad thing" in str(excinfo.value)
@pytest.mark.asyncio
async def test_generate_previews_returns_body_per_format():
transport = FakeTransport([
{"data": [{"body": "<iframe src='desktop'></iframe>"}]},
{"data": [{"body": "<iframe src='story'></iframe>"}]},
])
client = MetaClient(access_token="tok", transport=transport)
previews = await client.generate_previews(
"act_1",
creative={"object_story_spec": {}},
ad_formats=["DESKTOP_FEED_STANDARD", "INSTAGRAM_STORY"],
)
assert previews["DESKTOP_FEED_STANDARD"] == "<iframe src='desktop'></iframe>"
assert previews["INSTAGRAM_STORY"] == "<iframe src='story'></iframe>"
assert transport.calls[0]["params"]["ad_format"] == "DESKTOP_FEED_STANDARD"
# The creative must be sent as a JSON-encoded string in the query
# params, not as a raw dict, since Graph expects `creative` as JSON text.
sent = transport.calls[0]["params"]["creative"]
assert isinstance(sent, str)
assert json.loads(sent) == {"object_story_spec": {}}
@pytest.mark.asyncio
async def test_generate_previews_skips_formats_with_no_body():
transport = FakeTransport([{"data": []}])
client = MetaClient(access_token="tok", transport=transport)
previews = await client.generate_previews(
"act_1",
creative={},
ad_formats=["DESKTOP_FEED_STANDARD"],
)
assert previews == {}
@pytest.mark.asyncio
async def test_generate_previews_fails_fast_on_partial_error():
# Deliberate contract (not accidental): if any requested ad format's
# Graph call errors out, generate_previews propagates that MetaError
# immediately instead of returning a partial dict. The creative is
# identical across formats, so a creative-level error would fail every
# format anyway, and surfacing Meta's real message beats a silently
# incomplete preview set.
transport = FakeTransport([
{"data": [{"body": "<iframe src='desktop'></iframe>"}]},
{"error": {"message": "Bad creative", "code": 100, "error_subcode": 33}},
])
client = MetaClient(access_token="tok", transport=transport)
with pytest.raises(MetaError):
await client.generate_previews(
"act_1",
creative={"object_story_spec": {}},
ad_formats=["DESKTOP_FEED_STANDARD", "INSTAGRAM_STORY"],
)
def test_access_token_from_env_returns_token(monkeypatch):
monkeypatch.setenv(ACCESS_TOKEN_ENV, "tok123")
assert access_token_from_env() == "tok123"
def test_access_token_from_env_raises_when_whitespace_only(monkeypatch):
monkeypatch.setenv(ACCESS_TOKEN_ENV, " ")
with pytest.raises(MetaNotConfiguredError):
access_token_from_env()
def test_access_token_from_env_raises_when_unset(monkeypatch):
monkeypatch.delenv(ACCESS_TOKEN_ENV, raising=False)
with pytest.raises(MetaNotConfiguredError):
access_token_from_env()
def test_meta_not_configured_error_is_a_meta_error():
assert issubclass(MetaNotConfiguredError, MetaError)