from unittest import mock import faker import pytest from anydi import Container from fansifter_common.auth.account import Account from ows_text_campaigns.adapters.scanner import ContentRecommendations, ContentScanner from ows_text_campaigns.artist.models import ArtistPhoneNumber, ArtistSettings from ows_text_campaigns.assets.models import Asset from ows_text_campaigns.campaigns.enums import CampaignStatus, ShorteningMethod from ows_text_campaigns.campaigns.exceptions import ( CampaignAttachmentLimitExceededError, CampaignMustBeDraftError, ) from ows_text_campaigns.campaigns.handlers import ( UpdateCampaignContentHandler, UpdateCampaignContentRequest, ) from ows_text_campaigns.campaigns.models import Campaign, UrlDomain from ows_text_campaigns.campaigns.types import ShortenedUrl from ows_text_campaigns.campaigns.utils import create_short_link_tag from ows_text_campaigns.campaigns.validators import ShortenedUrlValidator from tests.unit.types import BuildModel, CreateModel class TestUpdateCampaignContentHandler: @pytest.mark.db def test_update_campaign_content( self, container: Container, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, faker: faker.Faker, ) -> None: content = faker.text() campaign = create_model( Campaign, status=CampaignStatus.DRAFT, content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) content_scanner_mock = mock.MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with container.override(ContentScanner, content_scanner_mock): campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=content, asset_ids=[], ) ) assert campaign.content == content assert campaign.updated_by == identity_id @pytest.mark.db def test_update_campaign_content_must_be_draft( self, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, ) -> None: campaign = create_model( Campaign, status=CampaignStatus.SENT, ) with pytest.raises(CampaignMustBeDraftError): handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=None, asset_ids=None, ) ) @pytest.mark.db def test_update_campaign_content_delete_existing( self, container: Container, handler: UpdateCampaignContentHandler, build_model: BuildModel, create_model: CreateModel, identity_id: str, ows_url_shortener_client_mock: mock.MagicMock, ) -> None: url_domain = create_model(UrlDomain, domain="sme.com") # Create existing shortened URLs shortened_url_1 = build_model( ShortenedUrl, domain=url_domain.domain, ) shortened_url_2 = build_model( ShortenedUrl, domain=url_domain.domain, ) shortened_url_3 = build_model( ShortenedUrl, domain=url_domain.domain, method=ShorteningMethod.STANDARD, ) campaign = create_model( Campaign, status=CampaignStatus.DRAFT, content="".join( [ create_short_link_tag(shortened_url_1), create_short_link_tag(shortened_url_2), create_short_link_tag(shortened_url_3), ] ), content_scan_result=ContentRecommendations( includedArtistName=False, sensitiveWords=[], ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) content_scanner_mock = mock.MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with container.override(ContentScanner, content_scanner_mock): handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content="".join( [ create_short_link_tag(shortened_url_1), create_short_link_tag(shortened_url_2), ] ), asset_ids=None, ) ) ows_url_shortener_client_mock.delete_paths.assert_called_once_with( [shortened_url_3.path], ) assert campaign.shortened_urls == [ shortened_url_1, shortened_url_2, ] @pytest.mark.db def test_update_campaign_content_attach_shortened_urls_with_content( self, container: Container, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, account: Account, faker: faker.Faker, ows_url_shortener_client_mock: mock.MagicMock, ) -> None: global_participant_id = faker.uuid4() url_domain = create_model(UrlDomain, domain="sme.com") shortened_url = ShortenedUrl( id=faker.uuid4(), url="https://sme.com/test1", domain=url_domain.domain, method=ShorteningMethod.STANDARD, path="test1", ) campaign = create_model( Campaign, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, content_scan_result=ContentRecommendations( includedArtistName=False, sensitiveWords=[], ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) content = create_short_link_tag(shortened_url) shortened_url_validator_mock = mock.MagicMock(spec=ShortenedUrlValidator) shortened_url_validator_mock.validate_allowed_domain.return_value = url_domain content_scanner_mock = mock.MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with ( container.override(ShortenedUrlValidator, shortened_url_validator_mock), container.override(ContentScanner, content_scanner_mock), ): campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=content, asset_ids=None, ) ) ows_url_shortener_client_mock.shorten_url.assert_called_once_with( url=shortened_url.url, domain=url_domain.domain, path=shortened_url.path, additional_attributes={ "campaign_id": campaign.id, "shortened_url_id": shortened_url.id, }, ) assert campaign.shortened_urls == [shortened_url] @pytest.mark.db def test_update_campaign_content_existing( self, container: Container, handler: UpdateCampaignContentHandler, create_model: CreateModel, build_model: BuildModel, identity_id: str, account: Account, faker: faker.Faker, ows_url_shortener_client_mock: mock.MagicMock, ) -> None: global_participant_id = faker.uuid4() url_domain = create_model(UrlDomain, domain="sme.com") shortened_url = build_model( ShortenedUrl, url="https://sme.com/test1", method=ShorteningMethod.STANDARD, path="test1", domain=url_domain.domain, ) campaign = create_model( Campaign, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, content_scan_result=ContentRecommendations( includedArtistName=False, sensitiveWords=["free loan"], ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) content = create_short_link_tag(shortened_url) shortened_url_validator_mock = mock.MagicMock(spec=ShortenedUrlValidator) shortened_url_validator_mock.validate_allowed_domain.return_value = url_domain content_scanner_mock = mock.MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with ( container.override(ShortenedUrlValidator, shortened_url_validator_mock), container.override(ContentScanner, content_scanner_mock), ): campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=content, asset_ids=None, ) ) ows_url_shortener_client_mock.shorten_url.assert_called_once_with( url=shortened_url.url, domain=url_domain.domain, path=shortened_url.path, additional_attributes={ "campaign_id": campaign.id, "shortened_url_id": shortened_url.id, }, ) assert campaign.shortened_urls == [shortened_url] @pytest.mark.db def test_update_campaign_content_attach_asset( self, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, account: Account, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() campaign_id = faker.uuid4() asset = create_model(Asset, object_type="text_campaign", object_id=campaign_id) campaign = create_model( Campaign, id=campaign_id, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, ) campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=None, asset_ids=[asset.id], ) ) assert campaign.assets == [asset] @pytest.mark.db def test_update_campaign_content_attach_asset_max_allowed( self, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, account: Account, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() campaign_id = faker.uuid4() asset_1 = create_model( Asset, object_type="text_campaign", object_id=campaign_id ) asset_2 = create_model( Asset, object_type="text_campaign", object_id=campaign_id ) campaign = create_model( Campaign, id=campaign_id, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, ) with pytest.raises(CampaignAttachmentLimitExceededError): handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=None, asset_ids=[asset_1.id, asset_2.id], ) ) @pytest.mark.db def test_update_campaign_content_delete_attachment( self, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, account: Account, faker: faker.Faker, s3_client_mock: mock.MagicMock, ) -> None: global_participant_id = faker.uuid4() campaign_id = faker.uuid4() asset = create_model(Asset, object_type="text_campaign", object_id=campaign_id) campaign = create_model( Campaign, id=campaign_id, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, assets=[asset], ) campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=None, asset_ids=[], ) ) assert campaign.assets == [] # Is deleted from S3 s3_client_mock.delete_object.assert_called_once() @pytest.mark.db def test_update_campaign_content_replace_on_method_change( self, container: Container, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, account: Account, faker: faker.Faker, ows_url_shortener_client_mock: mock.MagicMock, ) -> None: global_participant_id = faker.uuid4() url_domain = create_model(UrlDomain, domain="sme.com") shortened_url = ShortenedUrl( id=faker.uuid4(), url="https://sme.com/old-path", domain=url_domain.domain, path="old-path", method=ShorteningMethod.STANDARD, ) updated_shortened_url = ShortenedUrl( id=shortened_url.id, url=shortened_url.url, domain=shortened_url.domain, path=shortened_url.path, method=ShorteningMethod.PERSONALIZED, ) campaign = create_model( Campaign, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, content=create_short_link_tag(shortened_url), content_scan_result=ContentRecommendations( includedArtistName=False, sensitiveWords=["free loan"], ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) new_content = create_short_link_tag(updated_shortened_url) shortened_url_validator_mock = mock.MagicMock(spec=ShortenedUrlValidator) shortened_url_validator_mock.validate_allowed_domain.return_value = url_domain content_scanner_mock = mock.MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with ( container.override(ShortenedUrlValidator, shortened_url_validator_mock), container.override(ContentScanner, content_scanner_mock), ): updated_campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=new_content, asset_ids=None, ) ) ows_url_shortener_client_mock.delete_paths.assert_called_once_with( [shortened_url.path] ) assert updated_campaign.content == new_content @pytest.mark.db def test_update_campaign_content_replace_standard_on_path_change( self, container: Container, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, account: Account, faker: faker.Faker, ows_url_shortener_client_mock: mock.MagicMock, ) -> None: global_participant_id = faker.uuid4() url_domain = create_model(UrlDomain, domain="sme.com") shortened_url = ShortenedUrl( id=faker.uuid4(), url="https://sme.com/old-path", domain=url_domain.domain, path="old-path", method=ShorteningMethod.STANDARD, ) updated_shortened_url = ShortenedUrl( id=shortened_url.id, url=shortened_url.url, domain=shortened_url.domain, path="new-path", method=shortened_url.method, ) campaign = create_model( Campaign, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, content=create_short_link_tag(shortened_url), content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) new_content = create_short_link_tag(updated_shortened_url) shortened_url_validator_mock = mock.MagicMock(spec=ShortenedUrlValidator) shortened_url_validator_mock.validate_allowed_domain.return_value = url_domain content_scanner_mock = mock.MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with ( container.override(ShortenedUrlValidator, shortened_url_validator_mock), container.override(ContentScanner, content_scanner_mock), ): updated_campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=new_content, asset_ids=None, ) ) ows_url_shortener_client_mock.delete_paths.assert_called_once_with( [shortened_url.path] ) ows_url_shortener_client_mock.shorten_url.assert_called_once_with( url=updated_shortened_url.url, domain=updated_shortened_url.domain, path=updated_shortened_url.path, additional_attributes={ "campaign_id": campaign.id, "shortened_url_id": updated_shortened_url.id, }, ) assert updated_campaign.content == new_content @pytest.mark.db def test_update_campaign_content_nok_scan_result( self, container: Container, handler: UpdateCampaignContentHandler, create_model: CreateModel, identity_id: str, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() campaign = create_model( Campaign, status=CampaignStatus.DRAFT, global_participant_id=global_participant_id, content="My fans, come and enjoy music", content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) create_model( ArtistSettings, global_participant_id=campaign.global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=campaign.global_participant_id, phone_number="+1234567890", ) ], ) new_content = "Come and enjoy free loans." scanner_mock = mock.MagicMock(spec=ContentScanner) scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=["free loan"], includedArtistName=False ) with container.override(ContentScanner, scanner_mock): updated_campaign = handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign.id, content=new_content, asset_ids=None, ) ) assert updated_campaign.content == new_content assert updated_campaign.content_scan_result == ContentRecommendations( includedArtistName=False, sensitiveWords=["free loan"], )