import base64 import decimal from unittest import mock from unittest.mock import MagicMock import faker import pytest from anydi import Container from dirty_equals import IsDatetime, IsStr from fansifter_common.adapters.graphql_router import GlobalParticipant from fansifter_common.adapters.ows_account import Vendor from fansifter_common.auth.account import Account from fansifter_common.utils import timezone from pytest_mock import MockerFixture from starlette.testclient import TestClient from ows_text_campaigns.adapters.aws.sts import AssumedRoleCredentials from ows_text_campaigns.adapters.scanner import ContentRecommendations, ContentScanner from ows_text_campaigns.api.schemas import AgeRange, Gender from ows_text_campaigns.artist.models import ArtistPhoneNumber, ArtistSettings from ows_text_campaigns.assets.models import Asset from ows_text_campaigns.assets.services import AssetService from ows_text_campaigns.assets.types import BinaryAsset from ows_text_campaigns.audiences.enums import AudienceTarget, FanDataListType from ows_text_campaigns.audiences.models import ( Audience, AudienceFanDataList, AudienceTextFan, ) from ows_text_campaigns.campaigns.enums import ( CampaignStatus, LinkType, MessageChannel, MessageSendQueueStatus, ShorteningMethod, ) from ows_text_campaigns.campaigns.handlers import ( GetCampaignSendTimezonesHandler, GetCampaignSendTimezonesResponse, ScanCampaignContentHandler, ) from ows_text_campaigns.campaigns.models import ( Campaign, CampaignSendsCountryAggregated, CampaignUrlAttributes, MessageSendQueue, ShortenedUrl, UrlDomain, UrlShortenerClicksCountryAggregated, UrlShortenerClicksGenderAgeAggregated, ) from ows_text_campaigns.campaigns.types import CampaignSendCountry, SendTimezone from ows_text_campaigns.campaigns.utils import create_short_link_tag from ows_text_campaigns.config import Settings from ows_text_campaigns.twilio.models import ( TwilioAccount, TwilioMessagePricingByCountry, ) from tests.unit.equals import IsISODatetime, IsISODatetimeOrNone from tests.unit.types import BuildModel, CreateModel, CreatePgModel @pytest.mark.db def test_get_campaigns( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: audience = create_pg_model(Audience) campaign = create_model( Campaign, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, audience_id=audience.id, content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) response = client.get("/campaigns") assert response.status_code == 200 assert response.json() == { "total": 1, "items": [ { "id": campaign.id, "name": campaign.name, "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "globalParticipantId": campaign.global_participant_id, "audienceId": audience.id, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "status": campaign.status, "channel": campaign.channel, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "updatedBy": campaign.updated_by, "analytics": { "clicks": 0, "clickThroughRate": 0.0, }, "contentScanResult": {"includedArtistName": True, "sensitiveWords": []}, } ], "limit": 50, "offset": 0, } @pytest.mark.db def test_get_campaigns_analytics( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: audience = create_pg_model(Audience) campaign = create_model( Campaign, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, audience_id=audience.id, recipients_count=100, content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) redirect_url = "https://example.com/redirect" create_model( UrlShortenerClicksGenderAgeAggregated, campaign_id=campaign.id, redirect_url=redirect_url, gender=Gender.MALE, age_range=AgeRange.FROM_18_TO_22, clicks=50, ) create_model( CampaignSendsCountryAggregated, campaign_id=campaign.id, country_code="US", sends=100, ) response = client.get("/campaigns") assert response.status_code == 200 assert response.json() == { "total": 1, "items": [ { "id": campaign.id, "name": campaign.name, "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "globalParticipantId": campaign.global_participant_id, "audienceId": audience.id, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "status": campaign.status, "channel": campaign.channel, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "updatedBy": campaign.updated_by, "analytics": { "clicks": 50, "clickThroughRate": 0.5, }, "contentScanResult": {"includedArtistName": True, "sensitiveWords": []}, } ], "limit": 50, "offset": 0, } @pytest.mark.db def test_create_campaign_draft( client: TestClient, identity_id: str, graphql_router_client_mock: mock.MagicMock, account: Account, create_model: CreateModel, faker: faker.Faker, ) -> None: global_participant = GlobalParticipant(id=faker.uuid4(), name="Artist Name") create_model( ArtistSettings, global_participant_id=global_participant.id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=global_participant.id, phone_number=faker.phone_number(), ) ], ) graphql_router_client_mock.get_global_participant_by_gp_id.return_value = ( global_participant ) response = client.post( "/campaigns", json={ "name": "Test Campaign", "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, "globalParticipantId": global_participant.id, }, ) assert response.status_code == 201 assert response.json() == { "id": IsStr(), "name": "Test Campaign", "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, "globalParticipantId": global_participant.id, "audienceId": None, "audience": None, "status": "DRAFT", "channel": "SMS", "sendAt": None, "sendAtTimezone": None, "recipientsCount": None, "cancelReason": None, "cancelledAt": None, "createdAt": IsStr(), "createdBy": identity_id, "updatedAt": IsStr(), "updatedBy": identity_id, "analytics": None, "contentScanResult": None, } @pytest.mark.db def test_create_campaign_draft_not_unique_name( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(Campaign, name="Test Campaign") response = client.post( "/campaigns", json={ "name": "test campaign", "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "globalParticipantId": campaign.global_participant_id, }, ) assert response.status_code == 400 assert response.json() == { "code": "campaign_unique_name", "message": "Campaign with this name already exists", } @pytest.mark.db def test_get_campaign( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel ) -> None: audience = create_pg_model(Audience) campaign = create_model( Campaign, audience_id=audience.id, content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) response = client.get(f"/campaigns/{campaign.id}") assert response.status_code == 200 assert response.json() == { "id": campaign.id, "name": campaign.name, "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "globalParticipantId": campaign.global_participant_id, "audienceId": audience.id, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "status": campaign.status, "channel": campaign.channel, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "updatedBy": campaign.updated_by, "analytics": None, "contentScanResult": {"includedArtistName": True, "sensitiveWords": []}, } @pytest.mark.db def test_delete_campaign(client: TestClient, create_model: CreateModel) -> None: campaign = create_model(Campaign, status=CampaignStatus.DRAFT) response = client.delete(f"/campaigns/{campaign.id}") assert response.status_code == 204 @pytest.mark.db def test_update_campaign( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, account: Account, ) -> None: global_participant_id = faker.uuid4() create_model( ArtistSettings, global_participant_id=global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=global_participant_id, phone_number=faker.phone_number(), ) ], ) audience = create_pg_model( Audience, target=AudienceTarget.TEXT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fandata_lists=[ AudienceFanDataList( list_id=global_participant_id, type=FanDataListType.ARTIST, ) ], fan_count=100, ) campaign = create_model( Campaign, audience_id=audience.id, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=audience.global_participant_id, content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) response = client.put( f"/campaigns/{campaign.id}", json={ "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, "globalParticipantId": audience.global_participant_id, "name": campaign.name, "channel": "SMS", "audienceId": audience.id, }, ) assert response.status_code == 200 assert response.json() == { "id": campaign.id, "name": campaign.name, "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "globalParticipantId": campaign.global_participant_id, "audienceId": campaign.audience_id, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "status": campaign.status, "channel": campaign.channel, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "updatedBy": campaign.updated_by, "analytics": None, "contentScanResult": {"includedArtistName": True, "sensitiveWords": []}, } @pytest.mark.db def test_duplicate_campaign( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, account: Account, identity_id: str, ) -> None: global_participant_id = faker.uuid4() audience = create_pg_model( Audience, target=AudienceTarget.TEXT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fan_count=100, fandata_lists=[ AudienceFanDataList( list_id=global_participant_id, type=FanDataListType.ARTIST, ) ], ) campaign = create_model( Campaign, audience_id=audience.id, status=CampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, content_scan_result=ContentRecommendations( includedArtistName=True, sensitiveWords=[] ), ) new_name = f"{campaign.name} (Copy)" response = client.post( f"/campaigns/{campaign.id}/duplicate", json={"name": new_name}, ) assert response.status_code == 201 assert response.json() == { "id": IsStr(), "name": new_name, "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "globalParticipantId": campaign.global_participant_id, "audienceId": campaign.audience_id, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "status": campaign.status, "channel": campaign.channel, "sendAt": None, "sendAtTimezone": None, "recipientsCount": audience.fan_count, "cancelReason": None, "cancelledAt": None, "createdAt": IsDatetime(iso_string=True), "createdBy": identity_id, "updatedAt": IsDatetime(iso_string=True), "updatedBy": identity_id, "analytics": None, "contentScanResult": None, } @pytest.mark.db def test_get_campaign_content(client: TestClient, create_model: CreateModel) -> None: campaign = create_model( Campaign, status=CampaignStatus.DRAFT, ) response = client.get(f"/campaigns/{campaign.id}/content") assert response.status_code == 200 assert response.json() == { "content": campaign.content, } @pytest.mark.db def test_update_text_campaign_content( client: TestClient, create_model: CreateModel, faker: faker.Faker, container: Container, ) -> None: content = faker.text() campaign = create_model( Campaign, status=CampaignStatus.DRAFT, ) 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 = MagicMock(spec=ContentScanner) content_scanner_mock.scan.return_value = ContentRecommendations( sensitiveWords=[], includedArtistName=True ) with container.override( ContentScanner, content_scanner_mock, ): response = client.patch( f"/campaigns/{campaign.id}/content", json={ "content": base64.b64encode(content.encode()).decode(), }, ) assert response.status_code == 200 assert response.json() == { "content": content, } @pytest.mark.db def test_get_campaign_attachments( client: TestClient, create_model: CreateModel, ) -> None: asset = create_model(Asset) campaign = create_model(Campaign, assets=[asset]) response = client.get(f"/campaigns/{campaign.id}/attachments") assert response.status_code == 200 assert response.json() == [ { "id": asset.id, "key": asset.key, "url": asset.url, "contentType": asset.content_type, "extension": asset.extension, "originalFilename": asset.original_filename, "fileSize": asset.file_size, "createdBy": asset.created_by, "createdAt": IsDatetime(approx=asset.created_at, iso_string=True), } ] @pytest.mark.db def test_create_campaign_attachment( container: Container, client: TestClient, create_model: CreateModel, faker: faker.Faker, ) -> None: attachment_key = "test.jpg" campaign = create_model(Campaign) asset = create_model( Asset, object_type="text_campaign", object_id=campaign.id, key=attachment_key ) binary = BinaryAsset.from_data( faker.image(size=(300, 300), image_format="jpeg"), ) asset_service_mock = mock.MagicMock(spec=AssetService) asset_service_mock.get_asset_from_s3.return_value = binary asset_service_mock.create_and_upload_image_to_s3.return_value = asset with container.override(AssetService, asset_service_mock): response = client.post( f"/campaigns/{campaign.id}/attachments", json={ "attachmentKey": attachment_key, }, ) assert response.status_code == 201 assert response.json() == { "id": asset.id, "key": asset.key, "url": asset.url, "contentType": asset.content_type, "extension": asset.extension, "originalFilename": asset.original_filename, "fileSize": asset.file_size, "createdBy": asset.created_by, "createdAt": IsDatetime(approx=asset.created_at, iso_string=True), } @pytest.mark.db def test_create_campaign_attachment_upload_credentials( client: TestClient, build_model: BuildModel, create_model: CreateModel, sts_client_mock: mock.MagicMock, settings: Settings, mocker: MockerFixture, ) -> None: campaign = create_model(Campaign) credentials = build_model(AssumedRoleCredentials, expiration=timezone.now()) sts_client_mock.assume_role.return_value = credentials mocker.patch( "ows_text_campaigns.assets.utils.unique_hash", return_value="123", ) response = client.post( f"/campaigns/{campaign.id}/attachments/upload/credentials", json={ "filename": "test.jpg", }, ) assert response.status_code == 201 assert response.json() == { "accessKeyId": credentials.access_key_id, "secretAccessKey": credentials.secret_access_key, "sessionToken": credentials.session_token, "expiresAt": IsISODatetime(credentials.expiration), "key": f"assets/campaigns/{campaign.id}/test__123.jpg", "bucket": settings.assets_bucket_name, "region": settings.aws_region_name, } @pytest.mark.db def test_get_campaign_shortened_urls( client: TestClient, create_model: CreateModel, build_model: BuildModel ) -> None: url_domain = create_model(UrlDomain) shortened_url = build_model(ShortenedUrl, domain=url_domain.domain) campaign = create_model(Campaign, content=create_short_link_tag(shortened_url)) response = client.get(f"/campaigns/{campaign.id}/shortened-urls") assert response.status_code == 200 assert response.json() == [ { "id": shortened_url.id, "url": shortened_url.url, "method": shortened_url.method, "domain": url_domain.domain, "path": shortened_url.path, "previewUrl": shortened_url.preview_url, } ] @pytest.mark.db def test_validate_campaign_shortened_url( client: TestClient, build_model: BuildModel, create_model: CreateModel, faker: faker.Faker, ows_account_client_mock: mock.Mock, ) -> None: vendor = build_model(Vendor, company_brand="sme") campaign = create_model(Campaign) url_domain = create_model( UrlDomain, domain="sme.com", brand=vendor.brand, vendor_id=None, subaccount_id=None, ) shortened_url_id = faker.uuid4() url = faker.url() shortening_method = ShorteningMethod.STANDARD path = "test1" ows_account_client_mock.get_vendor.return_value = vendor response = client.post( f"/campaigns/{campaign.id}/shortened-urls/validate", json={ "id": shortened_url_id, "url": url, "method": shortening_method, "domain": url_domain.domain, "path": path, }, ) assert response.status_code == 200 assert response.json() == { "id": shortened_url_id, "url": url, "method": shortening_method, "domain": url_domain.domain, "path": path, "previewUrl": "https://sme.com/test1", } @pytest.mark.db def test_get_campaign_link_analytics( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: audience = create_pg_model(Audience) redirect_url = "https://example.com/redirect" campaign = create_model( Campaign, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, audience_id=audience.id, content=f""" short_link """, ) create_model( CampaignSendsCountryAggregated, campaign_id=campaign.id, sends=60, country_code="US", ) create_model( CampaignUrlAttributes, campaign_id=campaign.id, redirect_url=redirect_url, is_personalized=True, ) gender_age_aggregation = create_model( UrlShortenerClicksGenderAgeAggregated, campaign_id=campaign.id, redirect_url=redirect_url, gender=Gender.MALE, age_range=AgeRange.FROM_18_TO_22, clicks=30, ) country_aggregation = create_model( UrlShortenerClicksCountryAggregated, campaign_id=campaign.id, redirect_url=redirect_url, country_code="US", clicks=30, ) response = client.get(f"/campaigns/{campaign.id}/analytics/links") assert response.status_code == 200 assert response.json() == [ { "demographics": { "availableFansShare": 1, "data": [ { "id": "MALE", "items": [ {"label": "FROM_13_TO_17", "value": 0}, { "label": "FROM_18_TO_22", "value": gender_age_aggregation.clicks, }, {"label": "FROM_23_TO_30", "value": 0}, {"label": "FROM_31_TO_40", "value": 0}, {"label": "FROM_41_TO_51", "value": 0}, {"label": "FROM_52_TO_60", "value": 0}, {"label": "FROM_61", "value": 0}, ], "total": gender_age_aggregation.clicks, }, { "id": "FEMALE", "items": [ {"label": "FROM_13_TO_17", "value": 0}, {"label": "FROM_18_TO_22", "value": 0}, {"label": "FROM_23_TO_30", "value": 0}, {"label": "FROM_31_TO_40", "value": 0}, {"label": "FROM_41_TO_51", "value": 0}, {"label": "FROM_52_TO_60", "value": 0}, {"label": "FROM_61", "value": 0}, ], "total": 0, }, { "id": "PREFER_NOT_TO_ANSWER", "items": [ {"label": "FROM_13_TO_17", "value": 0}, {"label": "FROM_18_TO_22", "value": 0}, {"label": "FROM_23_TO_30", "value": 0}, {"label": "FROM_31_TO_40", "value": 0}, {"label": "FROM_41_TO_51", "value": 0}, {"label": "FROM_52_TO_60", "value": 0}, {"label": "FROM_61", "value": 0}, ], "total": 0, }, { "id": "NON_BINARY_OTHER", "items": [ {"label": "FROM_13_TO_17", "value": 0}, {"label": "FROM_18_TO_22", "value": 0}, {"label": "FROM_23_TO_30", "value": 0}, {"label": "FROM_31_TO_40", "value": 0}, {"label": "FROM_41_TO_51", "value": 0}, {"label": "FROM_52_TO_60", "value": 0}, {"label": "FROM_61", "value": 0}, ], "total": 0, }, ], }, "locations": [ {"clicks": country_aggregation.clicks, "countryCode": "US", "sends": 60} ], "totalClicks": gender_age_aggregation.clicks, "totalSends": 60, "url": redirect_url, "linkType": LinkType.PERSONALIZED, "clickthroughRate": 0.5, "clickthroughRateChange": None, } ] @pytest.mark.db def test_get_campaign_link_analytics_non_personalized_link( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: audience = create_pg_model(Audience) redirect_url = "https://example.com/redirect" campaign = create_model( Campaign, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, audience_id=audience.id, content=f""" short_link """, ) create_model( CampaignSendsCountryAggregated, campaign_id=campaign.id, sends=60, country_code="US", ) create_model( CampaignUrlAttributes, campaign_id=campaign.id, redirect_url=redirect_url, is_personalized=False, ) create_model( UrlShortenerClicksGenderAgeAggregated, campaign_id=campaign.id, redirect_url=redirect_url, gender=None, age_range=None, clicks=30, ) country_aggregation = create_model( UrlShortenerClicksCountryAggregated, campaign_id=campaign.id, redirect_url=redirect_url, country_code="US", clicks=30, ) response = client.get(f"/campaigns/{campaign.id}/analytics/links") assert response.status_code == 200 assert response.json() == [ { "demographics": None, "locations": [ {"clicks": country_aggregation.clicks, "countryCode": "US", "sends": 60} ], "totalClicks": 30, "totalSends": 60, "url": redirect_url, "linkType": LinkType.STANDARD, "clickthroughRate": 0.5, "clickthroughRateChange": None, } ] @pytest.mark.db def test_get_campaign_link_analytics_long_link( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: audience = create_pg_model(Audience) long_url = "https://example.com/redirect" campaign = create_model( Campaign, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, audience_id=audience.id, content=f'short_link', ) create_model( CampaignSendsCountryAggregated, campaign_id=campaign.id, sends=10, country_code="US", ) response = client.get(f"/campaigns/{campaign.id}/analytics/links") assert response.status_code == 200 assert response.json() == [ { "demographics": None, "locations": [{"clicks": None, "countryCode": "US", "sends": 10}], "totalClicks": None, "totalSends": 10, "url": long_url, "linkType": LinkType.LONG, "clickthroughRate": None, "clickthroughRateChange": None, } ] @pytest.mark.db def test_get_campaign_message_preview( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(Campaign, content="Hello") response = client.get( f"/campaigns/{campaign.id}/messages/preview", params={ "channel": "SMS", "includeUnsubscribeText": False, }, ) assert response.status_code == 200 assert response.json() == { "message": "Hello", "info": { "encoding": "GSM-7", "numberOfCharacters": 5, "numberOfUnicodeScalars": 5, "segmentsCount": 1, }, } @pytest.mark.db def test_send_campaign_test_message( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() twilio_account = create_pg_model(TwilioAccount) create_model( ArtistSettings, global_participant_id=global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=global_participant_id, phone_number="+12345678900", ) ], twilio_account_sid=twilio_account.account_sid, ) campaign = create_model( Campaign, global_participant_id=global_participant_id, content="Hello World!" ) response = client.post( f"/campaigns/{campaign.id}/messages/send-test", json={ "phoneNumber": "+12345678901", }, ) assert response.status_code == 204 @pytest.mark.db def test_schedule_campaign( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() twilio_account = create_pg_model(TwilioAccount) create_model( ArtistSettings, global_participant_id=global_participant_id, phone_numbers=[ ArtistPhoneNumber( phone_number="+12345678900", global_participant_id=global_participant_id, ) ], twilio_account_sid=twilio_account.account_sid, ) audience = create_pg_model( Audience, target=AudienceTarget.TEXT, fan_count=100, fandata_lists=[ AudienceFanDataList( list_id=global_participant_id, type=FanDataListType.ARTIST, ) ], ) campaign = create_model( Campaign, status=CampaignStatus.DRAFT, vendor_id=audience.vendor_id, subaccount_id=audience.subaccount_id, global_participant_id=global_participant_id, audience_id=audience.id, ) response = client.post( f"/campaigns/{campaign.id}/schedule", json={ "sendAt": faker.future_datetime(tzinfo=timezone.UTC).isoformat(), "timezone": "UTC", }, ) assert response.status_code == 204 @pytest.mark.db def test_edit_schedule_campaign( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() twilio_account = create_pg_model(TwilioAccount) create_model( ArtistSettings, global_participant_id=global_participant_id, phone_numbers=[ ArtistPhoneNumber( global_participant_id=global_participant_id, phone_number="+12345678900", ) ], twilio_account_sid=twilio_account.account_sid, ) audience = create_pg_model( Audience, target=AudienceTarget.TEXT, fan_count=100, fandata_lists=[ AudienceFanDataList( list_id=global_participant_id, type=FanDataListType.ARTIST, ) ], ) campaign = create_model( Campaign, status=CampaignStatus.SCHEDULED, vendor_id=audience.vendor_id, subaccount_id=audience.subaccount_id, global_participant_id=global_participant_id, audience_id=audience.id, ) response = client.put( f"/campaigns/{campaign.id}/schedule", json={ "sendAt": faker.future_datetime(tzinfo=timezone.UTC).isoformat(), "timezone": "UTC", }, ) assert response.status_code == 204 @pytest.mark.db def test_unschedule_campaign( client: TestClient, create_model: CreateModel, faker: faker.Faker ) -> None: campaign = create_model( Campaign, status=CampaignStatus.SCHEDULED, send_at=faker.future_datetime(tzinfo=timezone.UTC), ) response = client.post(f"/campaigns/{campaign.id}/unschedule") assert response.status_code == 204 @pytest.mark.db def test_get_campaign_live_status( client: TestClient, create_model: CreateModel, ) -> None: campaign = create_model(Campaign, status=CampaignStatus.IN_PROGRESS) message_send_queue = create_model( MessageSendQueue, campaign_id=campaign.id, status=MessageSendQueueStatus.PROCESSING, ) response = client.get( f"/campaigns/{campaign.id}/status", ) assert response.status_code == 200 assert response.json() == { "status": campaign.status, "messagesToSend": message_send_queue.total, "sentMessages": message_send_queue.offset, } @pytest.mark.db def test_get_campaign_analytics(client: TestClient, create_model: CreateModel) -> None: campaign = create_model(Campaign) response = client.get(f"/campaigns/{campaign.id}/analytics") assert response.status_code == 200 assert response.json() == { "sends": 0, "clicks": None, "clickThroughRate": None, "unsubscribeRate": 0, "unsubscribeRateChange": None, "deliveryRate": 0, "deliveryRateChange": None, "deliveryFailedRate": 0, "deliveryFailedRateChange": None, "locations": None, } @pytest.mark.db def test_get_campaign_price_estimation( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ) -> None: country = "AA" message_channel = MessageChannel.SMS min_price = faker.pyint(min_value=1, max_value=10000) / 100 max_price = min_price * 2 reply_share = faker.pyfloat(min_value=0.0, max_value=1) audience = create_pg_model(Audience, name="Test Audience") campaign = create_model( Campaign, name="Test Campaign", status=CampaignStatus.IN_PROGRESS, audience_id=audience.id, ) create_model( AudienceTextFan, audience_id=audience.id, channel=message_channel, fan_country=country, ) create_model( TwilioMessagePricingByCountry, country_iso2=country, message_type=message_channel, min_current_price=min_price, max_current_price=max_price, ) response = client.get( f"/campaigns/{campaign.id}/pricing-estimation", params={"replyShare": reply_share}, ) assert response.status_code == 200 assert response.json() == { "countries": [ { "channels": [ { "name": "SMS", "inboundCost": { "max": round(max_price * reply_share, 2), "min": round(min_price * reply_share, 2), }, "numberOfFans": 1, "outboundCost": {"max": max_price, "min": min_price}, } ], "name": "AA", } ], "totalCost": { "max": round(max_price + max_price * reply_share, 2), "min": round(min_price + min_price * reply_share, 2), }, } @pytest.mark.db def test_get_campaign_price_estimation_with_segments_count( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ) -> None: country = "AA" message_channel = MessageChannel.SMS min_price = faker.pyint(min_value=1, max_value=10000) / 100 max_price = min_price * 2 reply_share = faker.pyfloat(min_value=0.0, max_value=1) segments_count = 5 audience = create_pg_model(Audience, name="Test Audience") campaign = create_model( Campaign, name="Test Campaign", status=CampaignStatus.IN_PROGRESS, audience_id=audience.id, ) create_model( AudienceTextFan, audience_id=audience.id, channel=message_channel, fan_country=country, ) create_model( TwilioMessagePricingByCountry, country_iso2=country, message_type=message_channel, min_current_price=min_price, max_current_price=max_price, ) response = client.get( f"/campaigns/{campaign.id}/pricing-estimation", params={"replyShare": reply_share, "segmentsCount": segments_count}, ) assert response.status_code == 200 assert response.json() == { "countries": [ { "channels": [ { "name": "SMS", "inboundCost": { "max": round(max_price * segments_count * reply_share, 2), "min": round(min_price * segments_count * reply_share, 2), }, "numberOfFans": 1, "outboundCost": { "max": round(max_price * segments_count, 2), "min": round(min_price * segments_count, 2), }, } ], "name": "AA", } ], "totalCost": { "max": round( max_price * segments_count + max_price * segments_count * reply_share, 2 ), "min": round( min_price * segments_count + min_price * segments_count * reply_share, 2 ), }, } @pytest.mark.db def test_scan_campaign_content( client: TestClient, container: Container, faker: faker.Faker, create_model: CreateModel, ) -> None: artist_name = faker.name() handler_mock = mock.MagicMock(spec=ScanCampaignContentHandler) handler_mock.handle.return_value = { "sensitiveWords": [], "includedArtistName": True, } artist_settings = create_model(ArtistSettings, artist_name=artist_name) campaign = create_model( Campaign, global_participant_id=artist_settings.global_participant_id, content="Hello World!", ) with container.override( ScanCampaignContentHandler, handler_mock, ): response = client.post( f"/campaigns/{campaign.id}/content/scan", json={ "content": f"Hi my fans. Come and join my tour. {artist_name}", }, ) assert response.status_code == 200 assert response.json() == { "sensitiveWords": [], "includedArtistName": True, } @pytest.mark.db def test_scan_empty_campaign_content( client: TestClient, create_model: CreateModel, faker: faker.Faker, ) -> None: artist_name = faker.name() artist_settings = create_model(ArtistSettings, artist_name=artist_name) campaign = create_model( Campaign, global_participant_id=artist_settings.global_participant_id, content="Hello World!", ) response = client.post( f"/campaigns/{campaign.id}/content/scan", params={ "content": "", }, ) assert response.status_code == 422 def test_get_campaign_send_timezones( client: TestClient, container: Container, faker: faker.Faker ) -> None: send_at = timezone.now() handler_mock = mock.MagicMock(spec=GetCampaignSendTimezonesHandler) handler_mock.handle.return_value = GetCampaignSendTimezonesResponse( send_at=send_at, countries=[ CampaignSendCountry( country_code="US", timezones=[ SendTimezone( country_code="US", state_provinces=[], utc_offset_std=decimal.Decimal(0), iana_tz_id="Europe/London", quiet_hours_min="09:00", quiet_hours_max="09:30", next_safe_time=None, is_in_safe_hours=True, send_at_local=send_at, ), SendTimezone( country_code="US", state_provinces=[], utc_offset_std=decimal.Decimal(0), iana_tz_id="America/Los_Angeles", quiet_hours_min="09:00", quiet_hours_max="09:30", next_safe_time=None, is_in_safe_hours=True, send_at_local=send_at, is_adjusted=True, ), ], ) ], ) campaign_id = faker.uuid4() with container.override( GetCampaignSendTimezonesHandler, handler_mock, ): response = client.get( f"/campaigns/{campaign_id}/send-timezones", params={ "sendAt": send_at.isoformat(), }, ) assert response.status_code == 200 assert response.json() == { "sendAt": IsISODatetime(send_at), "countries": [ { "countryCode": "US", "timezones": [ { "ianaTzId": "Europe/London", "utcOffsetStd": "0", "isAdjusted": False, "isInSafeHours": True, "nextSafeTime": None, "quietHoursMax": "09:30", "quietHoursMin": "09:00", "sendAtLocal": IsISODatetime(send_at), "stateProvinces": [], }, { "ianaTzId": "America/Los_Angeles", "utcOffsetStd": "0", "isAdjusted": True, "isInSafeHours": True, "nextSafeTime": None, "quietHoursMax": "09:30", "quietHoursMin": "09:00", "sendAtLocal": IsISODatetime(send_at), "stateProvinces": [], }, ], } ], }