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.
This commit is contained in:
@@ -22,6 +22,7 @@ dependencies = [
|
|||||||
"google-genai>=1.0.0",
|
"google-genai>=1.0.0",
|
||||||
"Pillow>=10.0.0",
|
"Pillow>=10.0.0",
|
||||||
"psycopg[binary]>=3.2.0",
|
"psycopg[binary]>=3.2.0",
|
||||||
|
"httpx>=0.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Read-only Meta Graph client behaviour."""
|
"""Read-only Meta Graph client behaviour."""
|
||||||
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from adclaw.meta.client import MetaClient, MetaError
|
from adclaw.meta.client import (
|
||||||
|
ACCESS_TOKEN_ENV,
|
||||||
|
MetaClient,
|
||||||
|
MetaError,
|
||||||
|
MetaNotConfiguredError,
|
||||||
|
access_token_from_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeTransport:
|
class FakeTransport:
|
||||||
@@ -65,6 +73,12 @@ async def test_generate_previews_returns_body_per_format():
|
|||||||
assert previews["INSTAGRAM_STORY"] == "<iframe src='story'></iframe>"
|
assert previews["INSTAGRAM_STORY"] == "<iframe src='story'></iframe>"
|
||||||
assert transport.calls[0]["params"]["ad_format"] == "DESKTOP_FEED_STANDARD"
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_generate_previews_skips_formats_with_no_body():
|
async def test_generate_previews_skips_formats_with_no_body():
|
||||||
@@ -78,3 +92,49 @@ async def test_generate_previews_skips_formats_with_no_body():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert previews == {}
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user