from unittest import mock import pytest from anydi import Container from freezegun import freeze_time from starlette.testclient import TestClient from dmp.adapters.fivetran.models import ConnectCard from dmp.app_connections.enums import AppConnectionStatus from dmp.config import Settings from dmp.meta.dtos import ( MetaAdAccountOverview, MetaUser, MetaUserAdAccountLabel, MetaUserConnectionAdAccount, MetaUserConnectionResult, ) from dmp.meta.enums import MetaUserConnectionStatus from dmp.meta.handlers import ( AssignLabelsToMetaUserAdAccountsHandler, ConnectMetaAdReportingHandler, ConnectMetaUserHandler, DeleteMetaAdAccountsHandler, DeleteMetaUserAdAccountHandler, GetMetaAdAccountsHandler, GetMetaAdAccountsV2Handler, GetMetaAdAccountsV2Request, GetMetaAdAccountsV2Response, GetMetaAdReportingConnectionHandler, GetMetaAdReportingConnectionResponse, GetMetaAudienceShareHandler, GetMetaAudienceShareResponse, GetMetaAudienceShareStatusHandler, GetMetaAudienceShareStatusResponse, GetMetaUserAdAccountsHandler, GetMetaUserConnectionHandler, NotifyMetaAdReportingSyncCompletedHandler, RefreshMetaAdReportingConnectionHandler, ShareMetaAudienceHandler, SyncMetaFivetranTablesStateHandler, ) from dmp.meta.models import MetaAdAccount, MetaAdReportingConnection, MetaAudience from tests.unit.equals import IsISODatetime from tests.unit.faker import FakerTyped from tests.unit.types import BuildModel, CreateModel def test_connect_meta_user( client: TestClient, build_model: BuildModel, fake: FakerTyped, container: Container, settings: Settings, ) -> None: user_id = fake.uuid4_string() access_token = fake.pystr() user = build_model(MetaUser) user_connection_ad_account = build_model( MetaUserConnectionAdAccount, custom_audience_tos_accepted=True ) handler_mock = mock.MagicMock(spec=ConnectMetaUserHandler) handler_mock.handle.return_value = MetaUserConnectionResult( user=user, status=MetaUserConnectionStatus.CONNECTED, required_scopes=settings.meta_user_required_scopes, ad_accounts=[user_connection_ad_account], ) with container.override(ConnectMetaUserHandler, handler_mock): response = client.post( "/meta/user/connection", json={ "userId": user_id, "accessToken": access_token, }, ) assert response.status_code == 200 assert response.json() == { "user": { "name": user.name, "picture": user.picture, "grantedScopes": user.granted_scopes, }, "status": "CONNECTED", "requiredScopes": settings.meta_user_required_scopes, "adAccounts": [ { "id": user_connection_ad_account.id, "externalId": user_connection_ad_account.external_id, "name": user_connection_ad_account.name, "businessAccountName": user_connection_ad_account.business_account_name, "businessAccountPicture": ( user_connection_ad_account.business_account_picture ), "customAudienceTosAccepted": ( user_connection_ad_account.custom_audience_tos_accepted ), "customAudienceTosUrl": ( user_connection_ad_account.custom_audience_tos_url ), } ], "adAccountsCount": 1, } def test_get_meta_user_connection( client: TestClient, build_model: BuildModel, container: Container, settings: Settings, ) -> None: user = build_model(MetaUser) user_connection_ad_account = build_model( MetaUserConnectionAdAccount, custom_audience_tos_accepted=True ) handler_mock = mock.MagicMock(spec=GetMetaUserConnectionHandler) handler_mock.handle.return_value = MetaUserConnectionResult( user=user, status=MetaUserConnectionStatus.CONNECTED, required_scopes=settings.meta_user_required_scopes, ad_accounts=[user_connection_ad_account], ) with container.override(GetMetaUserConnectionHandler, handler_mock): response = client.get("/meta/user/connection") assert response.status_code == 200 assert response.json() == { "user": { "name": user.name, "picture": user.picture, "grantedScopes": user.granted_scopes, }, "status": "CONNECTED", "requiredScopes": settings.meta_user_required_scopes, "adAccounts": [ { "id": user_connection_ad_account.id, "externalId": user_connection_ad_account.external_id, "name": user_connection_ad_account.name, "businessAccountName": user_connection_ad_account.business_account_name, "businessAccountPicture": ( user_connection_ad_account.business_account_picture ), "customAudienceTosAccepted": ( user_connection_ad_account.custom_audience_tos_accepted ), "customAudienceTosUrl": ( user_connection_ad_account.custom_audience_tos_url ), } ], "adAccountsCount": 1, } def test_get_meta_user_ad_accounts( client: TestClient, build_model: BuildModel, container: Container, ) -> None: ad_account_1 = build_model(MetaUserAdAccountLabel) ad_account_2 = build_model( MetaUserAdAccountLabel, vendor_id=None, subaccount_id=None ) handler_mock = mock.MagicMock(spec=GetMetaUserAdAccountsHandler) handler_mock.handle.return_value = [ad_account_1, ad_account_2] with container.override(GetMetaUserAdAccountsHandler, handler_mock): response = client.get("/meta/user/ad-accounts") assert response.status_code == 200 assert response.json() == [ { "id": ad_account_1.id, "name": ad_account_1.name, "businessAccountName": ad_account_1.business_account_name, "businessAccountPicture": ad_account_1.business_account_picture, "vendorId": ad_account_1.vendor_id, "subaccountId": ad_account_1.subaccount_id, }, { "id": ad_account_2.id, "name": ad_account_2.name, "businessAccountName": ad_account_2.business_account_name, "businessAccountPicture": ad_account_2.business_account_picture, "vendorId": ad_account_2.vendor_id, "subaccountId": ad_account_2.subaccount_id, }, ] def test_get_meta_ad_accounts( client: TestClient, build_model: BuildModel, container: Container, ) -> None: ad_account_1 = build_model(MetaAdAccountOverview) ad_account_2 = build_model(MetaAdAccountOverview) handler_mock = mock.MagicMock(spec=GetMetaAdAccountsHandler) handler_mock.handle.return_value = [ad_account_1, ad_account_2] with container.override(GetMetaAdAccountsHandler, handler_mock): response = client.get("/meta/ad-accounts") assert response.status_code == 200 assert response.json() == [ { "id": ad_account_1.id, "userAdAccountId": ad_account_1.user_ad_account_id, "externalId": ad_account_1.external_id, "name": ad_account_1.name, "businessAccountName": ad_account_1.business_account_name, "businessAccountPicture": ad_account_1.business_account_picture, "vendorId": ad_account_1.vendor_id, "subaccountId": ad_account_1.subaccount_id, "isSelfConnected": ad_account_1.is_self_connected, "userConnectionStatus": ad_account_1.user_connection_status, "userSelfConnected": ad_account_1.user_self_connected, "adReportingConnectionStatus": ad_account_1.ad_reporting_connection_status, "adReportingSelfConnected": ad_account_1.ad_reporting_self_connected, "campaignsCount": ad_account_1.campaigns_count, }, { "id": ad_account_2.id, "userAdAccountId": ad_account_2.user_ad_account_id, "externalId": ad_account_2.external_id, "name": ad_account_2.name, "businessAccountName": ad_account_2.business_account_name, "businessAccountPicture": ad_account_2.business_account_picture, "vendorId": ad_account_2.vendor_id, "subaccountId": ad_account_2.subaccount_id, "isSelfConnected": ad_account_2.is_self_connected, "userConnectionStatus": ad_account_2.user_connection_status, "userSelfConnected": ad_account_2.user_self_connected, "adReportingConnectionStatus": ad_account_2.ad_reporting_connection_status, "adReportingSelfConnected": ad_account_2.ad_reporting_self_connected, "campaignsCount": ad_account_2.campaigns_count, }, ] def test_get_meta_ad_accounts_v2( client: TestClient, build_model: BuildModel, container: Container, ) -> None: ad_account_1 = build_model(MetaAdAccountOverview) ad_account_2 = build_model(MetaAdAccountOverview) handler_mock = mock.MagicMock(spec=GetMetaAdAccountsV2Handler) handler_mock.handle.return_value = GetMetaAdAccountsV2Response( total=2, items=[ad_account_1, ad_account_2], ) with container.override(GetMetaAdAccountsV2Handler, handler_mock): response = client.get("/meta/ad-accounts-v2") assert response.status_code == 200 assert response.json() == { "total": 2, "limit": 10, "offset": 0, "items": [ { "id": ad_account_1.id, "userAdAccountId": ad_account_1.user_ad_account_id, "externalId": ad_account_1.external_id, "name": ad_account_1.name, "businessAccountName": ad_account_1.business_account_name, "businessAccountPicture": ad_account_1.business_account_picture, "vendorId": ad_account_1.vendor_id, "subaccountId": ad_account_1.subaccount_id, "isSelfConnected": ad_account_1.is_self_connected, "userConnectionStatus": ad_account_1.user_connection_status, "userSelfConnected": ad_account_1.user_self_connected, "adReportingConnectionStatus": ad_account_1.ad_reporting_connection_status, "adReportingSelfConnected": ad_account_1.ad_reporting_self_connected, "campaignsCount": ad_account_1.campaigns_count, }, { "id": ad_account_2.id, "userAdAccountId": ad_account_2.user_ad_account_id, "externalId": ad_account_2.external_id, "name": ad_account_2.name, "businessAccountName": ad_account_2.business_account_name, "businessAccountPicture": ad_account_2.business_account_picture, "vendorId": ad_account_2.vendor_id, "subaccountId": ad_account_2.subaccount_id, "isSelfConnected": ad_account_2.is_self_connected, "userConnectionStatus": ad_account_2.user_connection_status, "userSelfConnected": ad_account_2.user_self_connected, "adReportingConnectionStatus": ad_account_2.ad_reporting_connection_status, "adReportingSelfConnected": ad_account_2.ad_reporting_self_connected, "campaignsCount": ad_account_2.campaigns_count, }, ], } def test_get_meta_ad_accounts_v2_with_filters( client: TestClient, build_model: BuildModel, fake: FakerTyped, container: Container, ) -> None: ad_account = build_model(MetaAdAccountOverview) vendor_id = fake.integer() subaccount_id = fake.integer() handler_mock = mock.MagicMock(spec=GetMetaAdAccountsV2Handler) handler_mock.handle.return_value = GetMetaAdAccountsV2Response( total=1, items=[ad_account], ) with container.override(GetMetaAdAccountsV2Handler, handler_mock): response = client.get( "/meta/ad-accounts-v2", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "limit": 5, "offset": 10, }, ) assert response.status_code == 200 body = response.json() assert body["total"] == 1 assert body["limit"] == 5 assert body["offset"] == 10 assert len(body["items"]) == 1 call_request = handler_mock.handle.call_args[0][0] assert call_request.vendor_id == vendor_id assert call_request.subaccount_id == subaccount_id assert call_request.limit == 5 assert call_request.offset == 10 def test_get_meta_ad_accounts_v2_with_order_by( client: TestClient, build_model: BuildModel, container: Container, ) -> None: ad_account = build_model(MetaAdAccountOverview) handler_mock = mock.MagicMock(spec=GetMetaAdAccountsV2Handler) handler_mock.handle.return_value = GetMetaAdAccountsV2Response( total=1, items=[ad_account], ) with container.override(GetMetaAdAccountsV2Handler, handler_mock): response = client.get( "/meta/ad-accounts-v2", params={"orderBy": ["campaignsCount.desc", "name.asc"]}, ) assert response.status_code == 200 call_request = handler_mock.handle.call_args[0][0] assert call_request.order_by == ["campaignsCount.desc", "name.asc"] def test_get_meta_ad_accounts_v2_defaults_order_by( client: TestClient, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=GetMetaAdAccountsV2Handler) handler_mock.handle.return_value = GetMetaAdAccountsV2Response(total=0, items=[]) with container.override(GetMetaAdAccountsV2Handler, handler_mock): response = client.get("/meta/ad-accounts-v2") assert response.status_code == 200 call_request = handler_mock.handle.call_args[0][0] assert call_request.order_by == GetMetaAdAccountsV2Request.DEFAULT_ORDER_BY @pytest.mark.db def test_connect_meta_ad_reporting_connection( client: TestClient, fake: FakerTyped, container: Container, build_model: BuildModel, ) -> None: connection = build_model( MetaAdReportingConnection, status=AppConnectionStatus.CONNECTED, ) connect_card = build_model(ConnectCard) handler_mock = mock.MagicMock(spec=ConnectMetaAdReportingHandler) handler_mock.handle.return_value = connection, connect_card with container.override(ConnectMetaAdReportingHandler, handler_mock): response = client.post( "/meta/ad-reporting/connection", json={ "redirectUri": fake.url(), }, ) assert response.status_code == 200 assert response.json() == { "connectionId": connection.id, "connectCardUri": connect_card.uri, } @pytest.mark.db def test_get_meta_ad_reporting_connection_status( client: TestClient, create_model: CreateModel, container: Container, identity_id: str, ) -> None: connection = create_model( MetaAdReportingConnection, status=AppConnectionStatus.CONNECTED, identity_id=identity_id, ad_accounts=[create_model(MetaAdAccount)], ) handler_mock = mock.MagicMock(spec=GetMetaAdReportingConnectionHandler) handler_mock.handle.return_value = GetMetaAdReportingConnectionResponse( status=connection.status, ad_accounts_count=connection.ad_accounts_count, ) with container.override(GetMetaAdReportingConnectionHandler, handler_mock): response = client.get("/meta/ad-reporting/connection") assert response.status_code == 200 assert response.json() == { "status": connection.status, "adAccountsCount": 1, } @pytest.mark.db def test_refresh_meta_ad_reporting_connection( client: TestClient, container: Container, create_model: CreateModel ) -> None: connection = create_model( MetaAdReportingConnection, status=AppConnectionStatus.CONNECTED, ) handler_mock = mock.MagicMock(spec=RefreshMetaAdReportingConnectionHandler) handler_mock.handle.return_value = connection with container.override(RefreshMetaAdReportingConnectionHandler, handler_mock): response = client.put( "/meta/ad-reporting/connection/refresh", json={"fivetranConnectorId": connection.fivetran_connector_id}, ) assert response.status_code == 204 @pytest.mark.db @freeze_time("2020-01-01") def test_share_meta_audience( client: TestClient, create_model: CreateModel, container: Container ) -> None: ad_account = create_model(MetaAdAccount) meta_audience = create_model(MetaAudience, ad_account=ad_account) handler_mock = mock.MagicMock(spec=ShareMetaAudienceHandler) handler_mock.handle.return_value = meta_audience with container.override(ShareMetaAudienceHandler, handler_mock): response = client.post( "/meta/audiences", json={ "audienceId": meta_audience.audience_id, "adAccountId": meta_audience.ad_account_id, "justification": meta_audience.share.justification, "reasonNotes": meta_audience.share.reason_notes, }, ) assert response.status_code == 201 assert response.json() == { "id": meta_audience.id, "externalId": meta_audience.external_id, "name": meta_audience.name, "fansCount": meta_audience.fans_count, "createdAt": IsISODatetime(meta_audience.created_at), } @pytest.mark.db def test_get_meta_audience_share_status( client: TestClient, create_model: CreateModel, container: Container, ) -> None: meta_audience = create_model(MetaAudience) handler_mock = mock.MagicMock(spec=GetMetaAudienceShareStatusHandler) handler_mock.handle.return_value = GetMetaAudienceShareStatusResponse( audience_id=meta_audience.id, audience_name=meta_audience.name, ad_account_name=meta_audience.ad_account.name or "", status=meta_audience.share.status, fans_count=meta_audience.fans_count, processed_fans=meta_audience.share.processed_fans, progress=0.0, seconds_remaining=None, url=meta_audience.url, ) with container.override(GetMetaAudienceShareStatusHandler, handler_mock): response = client.get(f"/meta/audiences/{meta_audience.id}/share-status") assert response.status_code == 200 assert response.json() == { "audienceId": meta_audience.id, "audienceName": meta_audience.name, "adAccountName": meta_audience.ad_account.name, "status": meta_audience.share.status, "fansCount": meta_audience.fans_count, "processedFans": meta_audience.share.processed_fans, "progress": 0.0, "secondsRemaining": None, "url": meta_audience.url, } @pytest.mark.db def test_get_meta_audience_share( client: TestClient, fake: FakerTyped, create_model: CreateModel, container: Container, ) -> None: meta_audience = create_model(MetaAudience) access_token = fake.encrypted_token() handler_mock = mock.MagicMock(spec=GetMetaAudienceShareHandler) handler_mock.handle.return_value = GetMetaAudienceShareResponse( audience_id=meta_audience.id, audience_name=meta_audience.name, audience_external_id=meta_audience.external_id, fans_count=meta_audience.fans_count, ad_account_external_id=meta_audience.ad_account.external_id, user_access_token=access_token, ) with container.override(GetMetaAudienceShareHandler, handler_mock): response = client.get(f"/meta/audience-shares/{meta_audience.share_id}") assert response.status_code == 200 assert response.json() == { "audienceId": meta_audience.id, "audienceName": meta_audience.name, "audienceExternalId": meta_audience.external_id, "fansCount": meta_audience.fans_count, "adAccountExternalId": meta_audience.ad_account.external_id, "userAccessToken": access_token, } @pytest.mark.db def test_assign_meta_user_ad_account_labels( client: TestClient, fake: FakerTyped, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=AssignLabelsToMetaUserAdAccountsHandler) handler_mock.handle.return_value = None with container.override(AssignLabelsToMetaUserAdAccountsHandler, handler_mock): response = client.post( "/meta/user/ad-accounts", json=[ { "adAccountId": fake.pystr(), "vendorId": 7123, "subaccountId": 0, }, { "adAccountId": fake.pystr(), "vendorId": 7123, "subaccountId": 0, }, ], ) assert response.status_code == 204 @pytest.mark.db def test_assign_meta_user_ad_account_labels_validate_unique_ad_account_ids( client: TestClient, fake: FakerTyped, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=AssignLabelsToMetaUserAdAccountsHandler) handler_mock.handle.return_value = None ad_account_id = fake.pystr() with container.override(AssignLabelsToMetaUserAdAccountsHandler, handler_mock): response = client.post( "/meta/user/ad-accounts", json=[ { "adAccountId": ad_account_id, "vendorId": 7123, "subaccountId": 0, }, { "adAccountId": ad_account_id, "vendorId": 7123, "subaccountId": 0, }, ], ) assert response.status_code == 422 def test_delete_meta_user_ad_account( client: TestClient, fake: FakerTyped, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=DeleteMetaUserAdAccountHandler) handler_mock.handle.return_value = None with container.override(DeleteMetaUserAdAccountHandler, handler_mock): response = client.delete(f"/meta/user/ad-accounts/{fake.pystr()}") assert response.status_code == 204 def test_handle_dbt_sync( client: TestClient, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=NotifyMetaAdReportingSyncCompletedHandler) handler_mock.handle.return_value = None with container.override(NotifyMetaAdReportingSyncCompletedHandler, handler_mock): response = client.post( "/meta/ad-reporting/handle-dbt-sync", ) assert response.status_code == 204 def test_delete_meta_ad_accounts( client: TestClient, fake: FakerTyped, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=DeleteMetaAdAccountsHandler) handler_mock.handle.return_value = None with container.override(DeleteMetaAdAccountsHandler, handler_mock): response = client.delete( f"/meta/ad-accounts/{fake.pystr()}", params={ "identityId": fake.uuid4_string(), "vendorId": 7123, "subaccountId": 0, }, ) assert response.status_code == 204 def test_sync_fivetran_tables_state( client: TestClient, fake: FakerTyped, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=SyncMetaFivetranTablesStateHandler) handler_mock.handle.return_value = [] with container.override(SyncMetaFivetranTablesStateHandler, handler_mock): response = client.post( "/meta/sync-fivetran-tables-state", json={ "fivetranConnectorId": fake.pystr(), }, ) assert response.status_code == 200 def test_sync_fivetran_tables_state_all_connections( client: TestClient, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=SyncMetaFivetranTablesStateHandler) handler_mock.handle.return_value = [] with container.override(SyncMetaFivetranTablesStateHandler, handler_mock): response = client.post( "/meta/sync-fivetran-tables-state", json={ "fivetranConnectorId": None, }, ) assert response.status_code == 200