Every write went out as a query string. Graph accepts that for small payloads, but upload_ad_image base64-encodes an entire image file, and no real image fits in a URL — image uploads could not have worked. POSTs now use a form body and get a longer timeout, since an upload is the one Meta call that carries real weight. Reads and deletes keep their parameters in the query string, where Graph expects them. Adds the first tests over the default transport: every other Meta test injects a fake one, so nothing covered this path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
2.8 KiB
Python
92 lines
2.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The default HTTP transport. Still no live calls: httpx itself is faked.
|
|
|
|
Every other Meta test injects a transport, so nothing exercised the real
|
|
one. The property that matters here is that POST payloads go in the body:
|
|
`upload_ad_image` base64-encodes a whole image file, which cannot fit in a
|
|
query string for any real image.
|
|
"""
|
|
import httpx
|
|
import pytest
|
|
|
|
from adclaw.meta import client as meta_client
|
|
|
|
|
|
class _Recorder:
|
|
"""Stands in for httpx.AsyncClient, capturing where params landed."""
|
|
|
|
def __init__(self, calls, **kwargs):
|
|
self.calls = calls
|
|
self.init_kwargs = kwargs
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *exc):
|
|
return False
|
|
|
|
def _response(self):
|
|
return httpx.Response(200, json={"id": "obj_1"})
|
|
|
|
async def post(self, url, data=None):
|
|
self.calls.append({"method": "POST", "url": url, "data": data})
|
|
return self._response()
|
|
|
|
async def request(self, method, url, params=None):
|
|
self.calls.append({"method": method, "url": url, "params": params})
|
|
return self._response()
|
|
|
|
|
|
@pytest.fixture()
|
|
def calls(monkeypatch):
|
|
recorded: list[dict] = []
|
|
monkeypatch.setattr(
|
|
httpx,
|
|
"AsyncClient",
|
|
lambda **kwargs: _Recorder(recorded, **kwargs),
|
|
)
|
|
return recorded
|
|
|
|
|
|
async def test_post_sends_its_payload_in_the_body(calls):
|
|
"""A base64 image cannot fit in a URL, so POST must not use the query."""
|
|
await meta_client._httpx_transport(
|
|
"POST", "https://graph.facebook.com/v23.0/act_1/adimages", {"bytes": "x" * 5000},
|
|
)
|
|
|
|
assert calls[0]["method"] == "POST"
|
|
assert calls[0]["data"] == {"bytes": "x" * 5000}
|
|
assert "?" not in calls[0]["url"]
|
|
|
|
|
|
async def test_get_keeps_its_params_in_the_query_string(calls):
|
|
await meta_client._httpx_transport(
|
|
"GET", "https://graph.facebook.com/v23.0/act_1", {"fields": "id"},
|
|
)
|
|
|
|
assert calls[0] == {
|
|
"method": "GET",
|
|
"url": "https://graph.facebook.com/v23.0/act_1",
|
|
"params": {"fields": "id"},
|
|
}
|
|
|
|
|
|
async def test_delete_keeps_its_params_in_the_query_string(calls):
|
|
await meta_client._httpx_transport(
|
|
"DELETE", "https://graph.facebook.com/v23.0/obj_1", {"access_token": "t"},
|
|
)
|
|
|
|
assert calls[0]["method"] == "DELETE"
|
|
assert calls[0]["params"] == {"access_token": "t"}
|
|
|
|
|
|
async def test_a_non_json_response_becomes_a_readable_error(monkeypatch):
|
|
class _Html(_Recorder):
|
|
def _response(self):
|
|
return httpx.Response(502, text="<html>bad gateway</html>")
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda **kwargs: _Html([], **kwargs))
|
|
|
|
with pytest.raises(meta_client.MetaError, match="non-JSON"):
|
|
await meta_client._httpx_transport("GET", "https://graph.facebook.com/x", {})
|