from collections.abc import AsyncIterator, Callable from typing import Any from unittest import mock import httpx import pytest import respx from audience_common.auth.account import Account, AccountAccess from campaigns.connectors.aws.kms import BaseKMS from campaigns.connectors.db import Database from campaigns.connectors.facebook import models as fb_models from campaigns.connectors.facebook.enums import ErrorCode from campaigns.connectors.facebook.exceptions import FacebookClientError from campaigns.meta.dtos import FacebookConnection, FacebookUser from campaigns.meta.enums import FacebookConnectionStatus from campaigns.meta.handlers import ( ConnectFacebookUserHandler, ConnectFacebookUserRequest, ) from campaigns.meta.models import FacebookAssociation, FacebookPage from campaigns.meta.repositories import ( FacebookAssociationRepository, FacebookPageLabelRepository, FacebookPageRepository, ) from campaigns.meta.services import FacebookPageService from tests.unit.faker import FakerTyped from tests.unit.types import BuildModel, CreateModel @pytest.fixture def handler( db: Database, dummy_kms: BaseKMS, auth_service_mock: mock.MagicMock, facebook_client_mock: mock.MagicMock, facebook_association_repository: FacebookAssociationRepository, facebook_page_label_repository: FacebookPageLabelRepository, facebook_page_service: FacebookPageService, ) -> ConnectFacebookUserHandler: handler = ConnectFacebookUserHandler( db=db, kms=dummy_kms, auth_service=auth_service_mock, facebook_client=facebook_client_mock, facebook_association_repository=facebook_association_repository, facebook_page_label_repository=facebook_page_label_repository, facebook_page_service=facebook_page_service, ) return handler @pytest.fixture(autouse=True) def api_token( facebook_client_mock: mock.MagicMock, build_model: BuildModel ) -> fb_models.AccessToken: api_token = build_model(fb_models.AccessToken) facebook_client_mock.get_oauth_access_token.return_value = api_token return api_token @pytest.fixture def api_user( facebook_client_mock: mock.MagicMock, build_model: BuildModel ) -> fb_models.User: api_user = build_model(fb_models.User) facebook_client_mock.get_user.return_value = api_user return api_user @pytest.fixture def handler_request( identity_id: str, profile_id: int, api_user: fb_models.User, fake: FakerTyped ) -> ConnectFacebookUserRequest: return ConnectFacebookUserRequest( identity_id=identity_id, profile_id=profile_id, user_id=api_user.id, access_token=fake.plain_token(), ) MockUserPages = Callable[[list[fb_models.Page]], None] @pytest.fixture def mock_user_pages( facebook_client_mock: mock.MagicMock, ) -> MockUserPages: def fixture(api_pages: list[fb_models.Page]) -> None: async def _get_user_pages( *args: Any, **kwargs: Any ) -> AsyncIterator[fb_models.Page]: for item in api_pages: yield item facebook_client_mock.get_user_pages.side_effect = _get_user_pages return fixture async def test_connect_facebook_user_no_pages( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, facebook_association_repository: FacebookAssociationRepository, mock_user_pages: MockUserPages, api_user: fb_models.User, ) -> None: mock_user_pages([]) connection = await handler.handle( request=handler_request, ) assert connection == FacebookConnection( user=FacebookUser.from_api_obj(api_user), status=FacebookConnectionStatus.CONNECTED, ) association = await facebook_association_repository.first() assert association assert association.is_connected assert association.user_id == api_user.id assert association.user_name == api_user.name assert association.orchard_identity_id == handler_request.identity_id assert not association.pages async def test_connect_existing_facebook_user_no_pages( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, facebook_association_repository: FacebookAssociationRepository, mock_user_pages: MockUserPages, create_model: CreateModel, api_user: fb_models.User, ) -> None: await create_model( FacebookAssociation, orchard_identity_id=handler_request.identity_id, user_id=api_user.id, user_name=api_user.name, ) mock_user_pages([]) connection = await handler.handle(handler_request) assert connection.is_connected association = await facebook_association_repository.first() assert association assert not association.pages async def test_connect_facebook_user_created_page( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, facebook_client_mock: mock.MagicMock, facebook_page_repository: FacebookPageRepository, build_model: BuildModel, mock_user_pages: MockUserPages, respx_mock: respx.MockRouter, fake: FakerTyped, ) -> None: api_page = build_model(fb_models.Page) mock_user_pages([api_page]) picture_content = fake.bytes() respx_mock.get(api_page.picture.data.url).mock( return_value=httpx.Response(status_code=200, content=picture_content) ) connection = await handler.handle(handler_request) assert connection.is_connected facebook_client_mock.assign_agency_to_business_manager.assert_not_called() facebook_client_mock.assign_agency_to_app_system_user.assert_not_called() page = await facebook_page_repository.first() assert page assert page.external_id == api_page.id assert page.is_valid assert not page.is_manageable assert not page.is_agency_provider async def test_connect_facebook_user_updated_page( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, build_model: BuildModel, create_model: CreateModel, facebook_client_mock: mock.MagicMock, facebook_page_repository: FacebookPageRepository, mock_user_pages: MockUserPages, api_user: fb_models.User, respx_mock: respx.MockRouter, fake: FakerTyped, ) -> None: api_page = build_model(fb_models.Page) mock_user_pages([api_page]) picture_content = fake.bytes() respx_mock.get(api_page.picture.data.url).mock( return_value=httpx.Response(status_code=200, content=picture_content) ) association = await create_model( FacebookAssociation, orchard_identity_id=handler_request.identity_id, user_id=api_user.id, user_name=api_user.name, ) await create_model(FacebookPage, association=association, external_id=api_page.id) connection = await handler.handle(handler_request) assert connection.is_connected assert await facebook_page_repository.count() == 1 page = await facebook_page_repository.first() assert page assert page.external_id == api_page.id assert page.is_valid assert page.is_manageable == api_page.is_manageable assert not page.is_agency_provider async def test_connect_facebook_user_deleted_page( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, create_model: CreateModel, facebook_client_mock: mock.MagicMock, facebook_page_repository: FacebookPageRepository, mock_user_pages: MockUserPages, api_user: fb_models.User, fake: FakerTyped, ) -> None: mock_user_pages([]) association = await create_model( FacebookAssociation, orchard_identity_id=handler_request.identity_id, user_id=api_user.id, user_name=api_user.name, ) await create_model(FacebookPage, association=association) connection = await handler.handle(handler_request) assert connection.is_connected assert await facebook_page_repository.count() == 1 page = await facebook_page_repository.first() assert page assert not page.is_valid assert not page.is_agency_provider async def test_connect_facebook_user_manageable_page( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, build_model: BuildModel, facebook_client_mock: mock.MagicMock, facebook_page_repository: FacebookPageRepository, mock_user_pages: MockUserPages, ) -> None: api_page = build_model(fb_models.Page, tasks=["MANAGE"]) mock_user_pages([api_page]) connection = await handler.handle( request=handler_request, ) assert connection.is_connected facebook_client_mock.assign_agency_to_business_manager.assert_called_once() facebook_client_mock.assign_agency_to_app_system_user.assert_called_once() page = await facebook_page_repository.first() assert page assert page.external_id == api_page.id assert page.is_valid assert page.is_manageable assert page.is_agency_provider async def test_connect_facebook_user_manageable_page_app_system_user_error( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, build_model: BuildModel, facebook_client_mock: mock.MagicMock, facebook_page_repository: FacebookPageRepository, mock_user_pages: MockUserPages, ) -> None: api_page = build_model(fb_models.Page, tasks=["MANAGE"]) mock_user_pages([api_page]) facebook_client_mock.assign_agency_to_app_system_user.side_effect = ( FacebookClientError() ) connection = await handler.handle(handler_request) assert connection.is_connected facebook_client_mock.assign_agency_to_business_manager.assert_called_once() facebook_client_mock.assign_agency_to_app_system_user.assert_called_once() page = await facebook_page_repository.first() assert page assert page.external_id == api_page.id assert page.is_valid assert not page.is_manageable assert not page.is_agency_provider async def test_connect_facebook_user_manageable_page_app_system_business_manager( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, build_model: BuildModel, facebook_client_mock: mock.MagicMock, facebook_page_repository: FacebookPageRepository, mock_user_pages: MockUserPages, ) -> None: api_page = build_model(fb_models.Page, tasks=["MANAGE"]) mock_user_pages([api_page]) facebook_client_mock.assign_agency_to_business_manager.side_effect = ( FacebookClientError() ) connection = await handler.handle(handler_request) assert connection.is_connected facebook_client_mock.assign_agency_to_business_manager.assert_called_once() facebook_client_mock.assign_agency_to_app_system_user.assert_not_called() page = await facebook_page_repository.first() assert page assert page.external_id == api_page.id assert page.is_valid assert not page.is_manageable assert not page.is_agency_provider async def test_connect_facebook_user_manageable_page_app_system_business_manager_skip( handler: ConnectFacebookUserHandler, handler_request: ConnectFacebookUserRequest, facebook_client_mock: mock.MagicMock, build_model: BuildModel, facebook_page_repository: FacebookPageRepository, mock_user_pages: MockUserPages, ) -> None: api_page = build_model(fb_models.Page, tasks=["MANAGE"]) mock_user_pages([api_page]) facebook_client_mock.assign_agency_to_business_manager.side_effect = ( FacebookClientError( context={"fb_error": {"code": ErrorCode.DUPLICATE_ASSET_ASSIGNMENT}} ) ) connection = await handler.handle(handler_request) assert connection.is_connected facebook_client_mock.assign_agency_to_business_manager.assert_called_once() facebook_client_mock.assign_agency_to_app_system_user.assert_not_called() page = await facebook_page_repository.first() assert page assert page.external_id == api_page.id assert page.is_valid assert page.is_manageable assert not page.is_agency_provider async def test_connect_facebook_user_has_page_and_has_one_account_access( handler: ConnectFacebookUserHandler, auth_service_mock: mock.MagicMock, build_model: BuildModel, facebook_page_label_repository: FacebookPageLabelRepository, mock_user_pages: MockUserPages, identity_id: str, profile_id: int, api_user: fb_models.User, fake: FakerTyped, ) -> None: api_page = build_model(fb_models.Page) account = Account(vendor_id=1000, subaccount_id=0) mock_user_pages([api_page]) auth_service_mock.authorize_account = mock.AsyncMock( return_value=AccountAccess(accounts=[account]) ) connection = await handler.handle( ConnectFacebookUserRequest( identity_id=identity_id, profile_id=profile_id, user_id=api_user.id, access_token=fake.plain_token(), ), ) assert connection.is_connected page_label = await facebook_page_label_repository.first() assert page_label assert page_label.account == account assert page_label.page.external_id == api_page.id async def test_connect_facebook_user_has_page_and_has_multiple_account_access( handler: ConnectFacebookUserHandler, auth_service_mock: mock.MagicMock, build_model: BuildModel, create_model: CreateModel, facebook_page_label_repository: FacebookPageLabelRepository, mock_user_pages: MockUserPages, identity_id: str, profile_id: int, api_user: fb_models.User, fake: FakerTyped, ) -> None: api_page = build_model(fb_models.Page) mock_user_pages([api_page]) auth_service_mock.authorize_account = mock.AsyncMock( return_value=AccountAccess( accounts=[ Account(vendor_id=1000, subaccount_id=0), Account(vendor_id=9999, subaccount_id=1), ] ) ) connection = await handler.handle( ConnectFacebookUserRequest( identity_id=identity_id, profile_id=profile_id, user_id=api_user.id, access_token=fake.plain_token(), ), ) assert connection.is_connected page_label = await facebook_page_label_repository.first() assert not page_label