import json import traceback from collections.abc import Iterator import httpx import pytest import respx from dmp.adapters.meta.client import FacebookClient from dmp.adapters.meta.exceptions import FacebookClientError from dmp.adapters.meta.models import ( AccessToken, CustomAudience, DebugToken, DisconnectUserResponse, User, UserAdAccount, ) from tests.unit.faker import FakerTyped from tests.unit.types import BuildModel class TestFacebookClient: @pytest.fixture def client(self) -> Iterator[FacebookClient]: with FacebookClient( client_id="client", client_secret="secret", ) as client: yield client def test_get_user( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: user = build_model(User) user_access_token = fake.plain_token() respx_mock.get( f"https://graph.facebook.com/v25.0/{user.id}", params={ "access_token": user_access_token, "fields": "id,name,picture", }, ).mock( return_value=httpx.Response( status_code=200, json=user.model_dump(), ) ) result = client.get_user( user_id=user.id, user_access_token=user_access_token, ) assert result == user def test_disconnect_user( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: user = build_model(User) user_access_token = fake.plain_token() response = DisconnectUserResponse(success=True) respx_mock.delete( f"https://graph.facebook.com/v25.0/{user.id}/permissions", params={ "access_token": user_access_token, }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(), ) ) result = client.disconnect_user( user_id=user.id, user_access_token=user_access_token, ) assert result == response.success def test_get_oauth_access_token( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: access_token = build_model(AccessToken) exchange_token = fake.plain_token() respx_mock.post( "https://graph.facebook.com/v25.0/oauth/access_token", json={ "grant_type": "fb_exchange_token", "client_id": client.client_id, "client_secret": client.client_secret, "fb_exchange_token": exchange_token, }, ).mock( return_value=httpx.Response( status_code=200, json=access_token.model_dump(), ) ) result = client.get_oauth_access_token(exchange_token) assert result == access_token def test_debug_token( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: debug_token = build_model(DebugToken) access_token = fake.plain_token() respx_mock.get( "https://graph.facebook.com/v25.0/debug_token", params={ "input_token": access_token, "access_token": f"{client.client_id}|{client.client_secret}", }, ).mock( return_value=httpx.Response( status_code=200, json={"data": debug_token.model_dump()}, ) ) result = client.debug_token(access_token) assert result == debug_token def test_get_user_ad_accounts( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: ad_account = build_model( UserAdAccount, tos_accepted={ "custom_audience_tos": True, }, ) user_id = fake.pystr() access_token = fake.plain_token() respx_mock.get( f"https://graph.facebook.com/v25.0/{user_id}/adaccounts", params={ "access_token": access_token, "fields": ( "id,name,user_tasks,business{id,name,picture{url}}," "campaigns.summary(total_count),tos_accepted" ), }, ).mock( return_value=httpx.Response( status_code=200, json={ "data": [ ad_account.model_dump(), ], "paging": { "cursors": { "before": fake.pystr(), "after": fake.pystr(), } }, }, ) ) result = client.get_user_ad_accounts( user_id=user_id, user_access_token=access_token, ) assert result == [ad_account] def test_get_user_ad_accounts_with_different_tos( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: ad_account = build_model( UserAdAccount, tos_accepted={ "custom_audience_tos": None, }, ) user_id = fake.pystr() access_token = fake.plain_token() ad_account_obj = ad_account.model_dump() ad_account_obj["tos_accepted"] = {"web_custom_audience_tos": 1} respx_mock.get( f"https://graph.facebook.com/v25.0/{user_id}/adaccounts", params={ "access_token": access_token, "fields": ( "id,name,user_tasks,business{id,name,picture{url}}," "campaigns.summary(total_count),tos_accepted" ), }, ).mock( return_value=httpx.Response( status_code=200, json={ "data": [ ad_account_obj, ], "paging": { "cursors": { "before": fake.pystr(), "after": fake.pystr(), } }, }, ) ) result = client.get_user_ad_accounts( user_id=user_id, user_access_token=access_token, ) assert result == [ad_account] def test_create_custom_audience( self, client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: ad_account_external_id = fake.pystr() access_token = fake.plain_token() name = fake.pystr() description = fake.pystr() custom_audience = build_model(CustomAudience) respx_mock.post( f"https://graph.facebook.com/v25.0/{ad_account_external_id}/customaudiences", json={ "name": name, "subtype": "CUSTOM", "description": description, "customer_file_source": "USER_PROVIDED_ONLY", "access_token": access_token, }, ).mock( return_value=httpx.Response( status_code=200, json=custom_audience.model_dump(), ) ) result = client.create_custom_audience( user_access_token=access_token, ad_account_external_id=ad_account_external_id, name=name, description=description, ) assert result == custom_audience def test_request_error( self, client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: path = "https://graph.facebook.com/v25.0/test" access_token = fake.uuid4_string() client_secret = fake.uuid4_string() params = { "access_token": access_token, "client_secret": client_secret, } respx_mock.get(path, params=params).mock( return_value=httpx.Response(status_code=500) ) try: client.request("GET", path, params=params) output = "" except FacebookClientError as exc: output = "".join(traceback.format_exception(exc)) assert access_token not in output assert client_secret not in output @pytest.mark.parametrize( "code,subcode", [ (190, 460), (190, 458), (190, 463), (190, 464), ], ) def test_request_is_token_validation_error( self, client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, code: int, subcode: int, ) -> None: path = "https://graph.facebook.com/v25.0/test" access_token = fake.uuid4_string() client_secret = fake.uuid4_string() params = { "access_token": access_token, "client_secret": client_secret, } response = {"error": {"code": code, "error_subcode": subcode}} respx_mock.get(path, params=params).mock( return_value=httpx.Response(status_code=500, content=json.dumps(response)) ) with pytest.raises(FacebookClientError) as exc_info: client.request("GET", path, params=params) assert exc_info.value.is_token_validation_error