import pathlib import tempfile import traceback from collections.abc import AsyncIterator import httpx import pytest import respx from campaigns.connectors.facebook.arguments import ( AdCreativeArgs, CreateAdsetArgs, CreateImageAdCreativeArgs, CreatePostAdCreativeArgs, CreateVideoAdCreativeArgs, GetAdPreviewArgs, GetAudienceSummaryArgs, UpdateAdArgs, UpdateAdsetArgs, UpdateCampaignArgs, ) from campaigns.connectors.facebook.base import FacebookClient from campaigns.connectors.facebook.enums import ( AdCampaignObjective, InstagramMediaProductType, ) from campaigns.connectors.facebook.exceptions import FacebookClientError from campaigns.connectors.facebook.models import ( Ad, AdCampaign, AdCreative, AdCreativeImageStorySpec, AdCreativeLinkData, AdImage, AdPreviewData, AdPreviewResponse, AdSet, AudienceSummary, InstagramBusinessAccount, InstagramBusinessAccountWithId, InstagramMedia, InstagramUser, Page, PagePost, ) from campaigns.core.types import PlainToken from tests.unit.faker import FakerTyped from tests.unit.types import BuildModel @pytest.fixture async def client() -> AsyncIterator[FacebookClient]: async with FacebookClient( client_id="client", client_secret="secret", app_system_user_token=PlainToken("token"), ad_account_id="99999", app_system_user_id="123456", admin_system_user_token=PlainToken("token"), business_manager_id="123456", ) as client: yield client async def test_make_request_error( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: access_token = fake.uuid4_string() client_secret = fake.uuid4_string() params = { "access_token": access_token, "client_secret": client_secret, } respx_mock.get("https://graph.facebook.com/v15.0/test", params=params).mock( return_value=httpx.Response(status_code=500) ) try: await client.make_request("GET", "/test", params=params) output = "" except Exception as exc: output = "".join(traceback.format_exception(exc)) assert access_token not in output assert client_secret not in output async def test_create_campaign( client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, fake: FakerTyped, ) -> None: campaign = build_model(AdCampaign) respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/campaigns" ).mock(return_value=httpx.Response(status_code=200, json=campaign.dict())) result = await client.create_campaign( name=fake.pystr(), objective=AdCampaignObjective.OUTCOME_ENGAGEMENT, lifetime_budget=fake.integer(), ) assert result == campaign async def test_update_campaign( client: FacebookClient, build_model: BuildModel, respx_mock: respx.MockRouter ) -> None: campaign = build_model(AdCampaign) mocked_request = respx_mock.post( f"https://graph.facebook.com/v15.0/{campaign.id}" ).mock(return_value=httpx.Response(status_code=200, json=campaign.dict())) result = await client.update_campaign( campaign.id, args=build_model(UpdateCampaignArgs) ) assert mocked_request.called assert result == campaign async def test_delete_campaign( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: campaign_id = str(fake.integer()) mocked_response = respx_mock.delete( f"https://graph.facebook.com/v15.0/{campaign_id}" ).mock(return_value=httpx.Response(status_code=200)) await client.delete_campaign(campaign_id=campaign_id) assert mocked_response.called async def test_create_adset( client: FacebookClient, build_model: BuildModel, fake: FakerTyped, respx_mock: respx.MockRouter, ) -> None: adset = build_model(AdSet) campaign_id = fake.pystr() respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adsets" ).mock(return_value=httpx.Response(status_code=200, json=adset.dict())) result = await client.create_ad_set( campaign_id=campaign_id, args=build_model(CreateAdsetArgs, targeting=None) ) assert adset == result async def test_update_adset( client: FacebookClient, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: adset = build_model(AdSet) mocked_request = respx_mock.post( f"https://graph.facebook.com/v15.0/{adset.id}" ).mock(return_value=httpx.Response(status_code=200, content=adset.json())) result = await client.update_adset( adset_id=adset.id, args=UpdateAdsetArgs(), ) assert mocked_request.called assert adset == result async def test_delete_adset( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: adset_id = str(fake.integer()) mocked_response = respx_mock.delete( f"https://graph.facebook.com/v15.0/{adset_id}" ).mock(return_value=httpx.Response(status_code=200)) await client.delete_adset(adset_id) assert mocked_response.called async def test_create_adcreative( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: adcreative = build_model(AdCreative) respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adcreatives" ).mock(return_value=httpx.Response(status_code=200, content=adcreative.json())) result = await client.create_ad_creative( args=AdCreativeArgs( object_story_id=fake.pystr(), ) ) assert adcreative == result async def test_create_video_adcreative( client: FacebookClient, build_model: BuildModel, fake: FakerTyped, respx_mock: respx.MockRouter, ) -> None: adcreative = build_model(AdCreative) respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adcreatives" ).mock(return_value=httpx.Response(status_code=200, content=adcreative.json())) result = await client.create_video_ad_creative( args=build_model(CreateVideoAdCreativeArgs) ) assert adcreative == result async def test_create_image_adcreative( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: adcreative = build_model(AdCreative) respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adcreatives" ).mock(return_value=httpx.Response(status_code=200, content=adcreative.json())) result = await client.create_image_ad_creative( args=CreateImageAdCreativeArgs( name=fake.pystr(), object_story_spec=AdCreativeImageStorySpec( page_id=fake.pystr(), link_data=AdCreativeLinkData( name=fake.pystr(), description=fake.pystr(), message=fake.pystr(), link=fake.image_url(), image_hash=fake.pystr(), ), ), ) ) assert adcreative == result async def test_create_post_adcreative( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: adcreative = build_model(AdCreative) respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adcreatives" ).mock(return_value=httpx.Response(status_code=200, content=adcreative.json())) result = await client.create_post_adcreative( args=CreatePostAdCreativeArgs( name=fake.pystr(), page_id=fake.pystr(), post_id=fake.pystr(), ) ) assert adcreative == result async def test_delete_adcreatice( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: adcreative_id = str(fake.integer()) mocked_response = respx_mock.delete( f"https://graph.facebook.com/v15.0/{adcreative_id}" ).mock(return_value=httpx.Response(status_code=200)) await client.delete_adcreative(adcreative_id=adcreative_id) assert mocked_response.called async def test_create_ad( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: ad = build_model(Ad) respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/ads" ).mock(return_value=httpx.Response(status_code=200, content=ad.json(by_alias=True))) result = await client.create_ad( name=fake.pystr(), ad_set_id=fake.pystr(), ad_creative_id=fake.pystr(), ) assert result == ad async def test_delete_ad( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: ad_id = str(fake.integer()) mocked_response = respx_mock.delete( f"https://graph.facebook.com/v15.0/{ad_id}" ).mock(return_value=httpx.Response(status_code=200)) await client.delete_ad(ad_id=ad_id) assert mocked_response.called async def test_update_ad( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: ad = build_model(Ad) respx_mock.post(f"https://graph.facebook.com/v15.0/{ad.id}").mock( return_value=httpx.Response(status_code=200, content=ad.json(by_alias=True)) ) result = await client.update_ad( ad.id, args=UpdateAdArgs( name=fake.pystr(), ), ) assert ad == result async def test_get_ad( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter, build_model: BuildModel, ) -> None: ad = build_model(Ad) respx_mock.get(f"https://graph.facebook.com/v15.0/{ad.id}").mock( return_value=httpx.Response(status_code=200, content=ad.json(by_alias=True)) ) result = await client.get_ad(ad_id=ad.id) assert ad == result async def test_assign_agency_to_business_manager( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: page_id = fake.pystr() page_access_token = fake.plain_token() mocked_response = respx_mock.post( f"https://graph.facebook.com/v15.0/{page_id}/agencies" ).mock(return_value=httpx.Response(status_code=200)) await client.assign_agency_to_business_manager( page_id=page_id, page_access_token=page_access_token ) assert mocked_response.called async def test_assign_agency_to_business_manager_error( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: page_id = fake.pystr() page_access_token = fake.plain_token() respx_mock.post(f"https://graph.facebook.com/v15.0/{page_id}/agencies").mock( return_value=httpx.Response(status_code=400, json="") ) with pytest.raises(FacebookClientError): await client.assign_agency_to_business_manager( page_id=page_id, page_access_token=page_access_token ) async def test_assign_agency_to_app_system_user( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: page_id = fake.pystr() mocked_response = respx_mock.post( f"https://graph.facebook.com/v15.0/{page_id}/assigned_users" ).mock(return_value=httpx.Response(status_code=200)) await client.assign_agency_to_app_system_user(page_id=page_id) assert mocked_response.called async def test_assign_agency_to_app_system_user_error( client: FacebookClient, fake: FakerTyped, respx_mock: respx.MockRouter ) -> None: page_id = fake.pystr() respx_mock.post(f"https://graph.facebook.com/v15.0/{page_id}/assigned_users").mock( return_value=httpx.Response(status_code=400, json="") ) with pytest.raises(FacebookClientError): await client.assign_agency_to_app_system_user(page_id=page_id) async def test_create_ad_image( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: image_hash = fake.pystr() image_url = fake.image_url() url_256 = fake.image_url() respx_mock.post( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adimages" ).mock( return_value=httpx.Response( status_code=200, json={ "images": { "bytes": { "hash": image_hash, "url": image_url, "url_256": url_256, } } }, ) ) with tempfile.NamedTemporaryFile() as tmpfile: image = await client.create_ad_image(pathlib.Path(tmpfile.name)) assert image.hash == image_hash assert image.url == image_url assert image.url_256 == url_256 async def test_get_ad_image( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, build_model: BuildModel, ) -> None: ad_image = build_model(AdImage) respx_mock.get( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adimages", ).mock( return_value=httpx.Response( status_code=200, json={"data": [ad_image.dict()]}, ) ) result = await client.get_ad_image(ad_image.hash) assert result == ad_image async def test_get_ad_image_not_exist( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: image_hash = fake.pystr() respx_mock.get( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adimages", ).mock(return_value=httpx.Response(status_code=200, json={"data": []})) image = await client.get_ad_image(image_hash) assert not image async def test_delete_ad_image_not_exist( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: image_hash = fake.pystr() success = True respx_mock.delete( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/adimages", ).mock(return_value=httpx.Response(status_code=200, json={"success": success})) result = await client.delete_ad_image(image_hash) assert result == success async def test_get_post( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, build_model: BuildModel, ) -> None: page_post = build_model(PagePost) page_access_token = fake.plain_token() respx_mock.get(f"https://graph.facebook.com/v15.0/{page_post.id}/").mock( return_value=httpx.Response( status_code=200, content=page_post.json(), ) ) result = await client.get_post( post_id=page_post.id, page_access_token=page_access_token ) assert result == page_post async def test_get_ad_preview( client: FacebookClient, build_model: BuildModel, respx_mock: respx.MockRouter, fake: FakerTyped, ) -> None: ad_preview_data = AdPreviewData(body=fake.pystr()) ad_preview_response = AdPreviewResponse(data=[ad_preview_data]) respx_mock.get( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/generatepreviews" ).mock( return_value=httpx.Response( status_code=200, content=ad_preview_response.json(), ) ) result = await client.get_ad_preview(args=build_model(GetAdPreviewArgs)) assert result == ad_preview_data async def test_get_instagram_business_account_for_page( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, build_model: BuildModel, ) -> None: page = build_model(Page) page_access_token = fake.plain_token() account_id = fake.uuid4_string() respx_mock.get( f"https://graph.facebook.com/v15.0/{page.id}", params={ "access_token": page_access_token, "fields": "instagram_business_account", }, ).mock( return_value=httpx.Response( status_code=200, json={ "id": page.id, "instagram_business_account": { "id": account_id, }, }, ) ) result = await client.get_instagram_business_account_for_page( page_id=page.id, page_access_token=page_access_token ) assert result assert result.id == account_id async def test_get_instagram_business_account_for_page_not_exist( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, build_model: BuildModel, ) -> None: page = build_model(Page) page_access_token = fake.plain_token() respx_mock.get( f"https://graph.facebook.com/v15.0/{page.id}", params={ "access_token": page_access_token, "fields": "instagram_business_account", }, ).mock( return_value=httpx.Response( status_code=200, json={ "id": page.id, }, ) ) result = await client.get_instagram_business_account_for_page( page_id=page.id, page_access_token=page_access_token ) assert result is None async def test_get_instagram_business_account( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, build_model: BuildModel, ) -> None: page_access_token = fake.plain_token() account = build_model(InstagramBusinessAccount) respx_mock.get( f"https://graph.facebook.com/v15.0/{account.id}", params={ "access_token": page_access_token, "fields": "id,name,username,profile_picture_url", }, ).mock( return_value=httpx.Response( status_code=200, json={ "id": account.id, "name": account.name, "username": account.username, "profile_picture_url": account.profile_picture_url, }, ) ) result = await client.get_instagram_business_account( instagram_business_account_id=account.id, page_access_token=page_access_token ) assert result == account async def test_get_instagram_media( client: FacebookClient, build_model: BuildModel, respx_mock: respx.MockRouter, fake: FakerTyped, ) -> None: page_access_token = fake.plain_token() account = build_model(InstagramBusinessAccountWithId) media = build_model( InstagramMedia, media_product_type=InstagramMediaProductType.REELS ) respx_mock.get( f"https://graph.facebook.com/v15.0/{account.id}/media", params={ "access_token": page_access_token, "fields": ( "id,permalink,media_url,media_type,media_product_type," "thumbnail_url,caption,timestamp" ), }, ).mock( return_value=httpx.Response( status_code=200, json={ "data": [ { "id": media.id, "permalink": media.permalink, "media_url": media.media_url, "media_type": media.media_type, "media_product_type": media.media_product_type, "thumbnail_url": media.thumbnail_url, "caption": media.caption, "timestamp": str(media.timestamp), }, ], "paging": { "cursors": { "before": fake.pystr(), "after": fake.pystr(), } }, }, ) ) result = [] async for item in client.get_instagram_media( instagram_account_id=account.id, page_access_token=page_access_token ): result.append(item) assert result == [media] async def test_get_instagram_stories( client: FacebookClient, build_model: BuildModel, respx_mock: respx.MockRouter, fake: FakerTyped, ) -> None: page_access_token = fake.plain_token() account = build_model(InstagramBusinessAccountWithId) story = build_model( InstagramMedia, media_product_type=InstagramMediaProductType.STORY ) respx_mock.get( f"https://graph.facebook.com/v15.0/{account.id}/stories", params={ "access_token": page_access_token, "fields": ( "id,permalink,media_url,media_type,media_product_type," "thumbnail_url,caption,timestamp" ), }, ).mock( return_value=httpx.Response( status_code=200, json={ "data": [ { "id": story.id, "permalink": story.permalink, "media_url": story.media_url, "media_type": story.media_type, "media_product_type": story.media_product_type, "thumbnail_url": story.thumbnail_url, "caption": story.caption, "timestamp": str(story.timestamp), }, ], "paging": { "cursors": { "before": fake.pystr(), "after": fake.pystr(), } }, }, ) ) result = [] async for item in client.get_instagram_stories( instagram_account_id=account.id, page_access_token=page_access_token ): result.append(item) assert result == [story] async def test_get_instagram_post( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped, build_model: BuildModel, ) -> None: page_access_token = fake.plain_token() story = build_model( InstagramMedia, media_product_type=InstagramMediaProductType.STORY ) respx_mock.get( f"https://graph.facebook.com/v15.0/{story.id}", params={ "access_token": page_access_token, "fields": ( "id,permalink,media_url,media_type,media_product_type," "thumbnail_url,caption,timestamp" ), }, ).mock( return_value=httpx.Response( status_code=200, json={ "id": story.id, "permalink": story.permalink, "media_url": story.media_url, "media_type": story.media_type, "media_product_type": story.media_product_type, "thumbnail_url": story.thumbnail_url, "caption": story.caption, "timestamp": str(story.timestamp), }, ) ) result = await client.get_instagram_media_item( post_id=story.id, page_access_token=page_access_token ) assert result == story async def test_get_audience_summary( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: args = GetAudienceSummaryArgs(genders=[fake.choice([1, 2])]) summary = AudienceSummary( users_lower_bound=fake.integer(length=6), users_upper_bound=fake.integer(length=7), estimate_ready=True, ) respx_mock.get( f"https://graph.facebook.com/v15.0/{client.ad_account_id}/reachestimate", ).mock( return_value=httpx.Response( status_code=200, json={ "data": { "users_lower_bound": summary.users_lower_bound, "users_upper_bound": summary.users_upper_bound, "estimate_ready": summary.estimate_ready, } }, ) ) result = await client.get_audience_summary(args=args) assert result == summary async def test_get_instagram_user_for_page( client: FacebookClient, respx_mock: respx.MockRouter, fake: FakerTyped ) -> None: page_id = fake.pystr() user_id = fake.pystr() respx_mock.get( f"https://graph.facebook.com/v15.0/{page_id}/instagram_accounts", ).mock( return_value=httpx.Response( status_code=200, json={ "data": [ { "id": user_id, } ] }, ) ) result = await client.get_instagram_user_for_page( page_id=page_id, page_access_token=PlainToken(fake.pystr()) ) assert result == InstagramUser(id=user_id)