import base64 import json import urllib.parse from collections.abc import Iterator from datetime import timedelta from unittest import mock from unittest.mock import Mock import faker import pytest from anydi import Container from dirty_equals import IsNumeric, 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.core.enums import Brand from fansifter_common.utils import timezone from starlette.testclient import TestClient from email_campaigns.audiences.enums import AudienceTarget from email_campaigns.audiences.models import Audience from email_campaigns.campaigns.enums import EmailCampaignStatus from email_campaigns.campaigns.handlers import ( AutosaveCampaignHandler, AutosaveCampaignV2Handler, ) from email_campaigns.campaigns.models import CampaignBatch, DeliveryQuota, EmailCampaign from email_campaigns.emails.models import ( EmailAnalyticsBenchmarkByAccountArtistV2Dbt, EmailAnalyticsBenchmarkByLinktV3Dbt, EmailAnalyticsV3Dbt, EmailDomain, ShopifyEmailOrdersDbt, ShopifyEmailProductsDbt, ShopifyProductTypesDbt, ) from tests.unit.equals import IsISODatetime, IsISODatetimeOrNone from tests.unit.types import BuildModel, CreateModel, CreatePgModel @pytest.mark.db def test_create_email_campaign_draft( client: TestClient, identity_id: str, faker: faker.Faker, create_model: CreateModel, build_model: BuildModel, ows_account_client_mock: mock.MagicMock, graphql_router_client_mock: mock.MagicMock, account: Account, ) -> None: vendor = build_model(Vendor, vendor_id=account.vendor_id, company_brand=Brand.SME) global_participant = GlobalParticipant(id=faker.uuid4(), name="Artist Name") email_domain = create_model( EmailDomain, brand=vendor.brand, domain="example.com", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) ows_account_client_mock.get_vendor.return_value = vendor 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, "fanDataListId": { "id": global_participant.id, "type": "ARTIST", }, }, ) assert response.status_code == 201 assert response.json() == { "id": IsStr(), "name": "Test Campaign", "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, "fanDataListId": { "id": global_participant.id, "type": "ARTIST", }, "audienceId": None, "status": "DRAFT", "emailDomain": { "id": email_domain.id, "domain": "example.com", "brand": Brand.SME, }, "emailUsername": "artistname", "previewText": None, "senderName": "Artist Name", "sendAt": None, "sendAtTimezone": None, "subject": None, "recipientsCount": 0, "cancelReason": None, "cancelledAt": None, "createdAt": IsStr(), "createdBy": identity_id, "updatedAt": IsStr(), "hasUnappliedChanges": False, "updatedBy": identity_id, "analytics": None, "audience": None, } @pytest.mark.db def test_create_email_campaign_draft_not_unique_name( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") response = client.post( "/campaigns", json={ "name": "test campaign", "vendorId": campaign.vendor_id, "subaccountId": campaign.subaccount_id, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, }, ) assert response.status_code == 400 assert response.json() == { "code": "email_campaign_unique_name", "message": "Email campaign with this name already exists", } @pytest.mark.db def test_get_email_campaigns( client: TestClient, create_model: CreateModel, account: Account ) -> None: email_domain = create_model(EmailDomain, vendor_id=None, subaccount_id=None) campaign = create_model( EmailCampaign, email_domain=email_domain, custom_list_id=None, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = client.post("/campaigns/list", json={}) 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, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, "audienceId": campaign.audience_id, "status": campaign.status, "emailDomain": { "id": email_domain.id, "domain": email_domain.domain, "brand": email_domain.brand, }, "emailUsername": campaign.email_username, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "subject": campaign.subject, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "hasUnappliedChanges": campaign.has_unapplied_changes, "updatedBy": campaign.updated_by, "analytics": None, "audience": None, } ], "limit": 50, "offset": 0, } @pytest.mark.db def test_get_email_campaigns_with_analytics( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: email_domain = create_model(EmailDomain, vendor_id=None, subaccount_id=None) global_participant_id = "123" audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fan_count=100, ) campaign = create_model( EmailCampaign, email_domain=email_domain, global_participant_id=global_participant_id, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, audience_id=audience.id, custom_list_id=None, ) create_model( EmailAnalyticsV3Dbt, email_id=campaign.id, email_type=campaign.email_type, ) create_model( EmailAnalyticsBenchmarkByAccountArtistV2Dbt, global_participant_id=campaign.global_participant_id, custom_list_id=campaign.custom_list_id, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, email_type=campaign.email_type, ) response = client.post("/campaigns/list", json={}) 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, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, "audienceId": campaign.audience_id, "status": campaign.status, "emailDomain": { "id": email_domain.id, "domain": email_domain.domain, "brand": email_domain.brand, }, "emailUsername": campaign.email_username, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "subject": campaign.subject, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "hasUnappliedChanges": campaign.has_unapplied_changes, "updatedBy": campaign.updated_by, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "analytics": { "bounceRate": IsNumeric(), "bounceRateChange": IsNumeric(), "bounces": IsNumeric(), "clickThroughRate": IsNumeric(), "clickThroughRateChange": IsNumeric(), "clickToOpenRate": IsNumeric(), "clickToOpenRateChange": IsNumeric(), "clicks": IsNumeric(), "delivered": IsNumeric(), "deliveredRate": IsNumeric(), "deliveredRateChange": IsNumeric(), "openRate": IsNumeric(), "openRateChange": IsNumeric(), "opens": IsNumeric(), "sends": IsNumeric(), "spamComplaints": IsNumeric(), "spamComplaintsRate": IsNumeric(), "spamComplaintsRateChange": IsNumeric(), "uniqueClicks": IsNumeric(), "uniqueClicksChange": IsNumeric(), "uniqueClicksDropoff": IsNumeric(), "uniqueOpens": IsNumeric(), "uniqueOpensDropoff": IsNumeric(), "unsubscribeRate": IsNumeric(), "unsubscribeRateChange": IsNumeric(), "unsubscribes": IsNumeric(), }, } ], "limit": 50, "offset": 0, } @pytest.mark.db def test_search_campaigns( client: TestClient, create_model: CreateModel, account: Account, faker: faker.Faker ) -> None: global_participant_id = faker.uuid4() email_domain = create_model(EmailDomain, vendor_id=None, subaccount_id=None) campaign = create_model( EmailCampaign, email_domain=email_domain, custom_list_id=None, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, ) response = client.post( "/campaigns/search", json={ "campaignIds": [campaign.id], "fanDataListIds": [ { "type": "ARTIST", "id": global_participant_id, } ], }, ) assert response.status_code == 200 assert response.json() == [ { "id": campaign.id, "name": campaign.name, } ] @pytest.mark.db def test_get_email_campaign( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, faker: faker.Faker, ) -> None: email_domain = create_model(EmailDomain) audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fan_count=100, ) campaign = create_model( EmailCampaign, sender_name=faker.name(), subject=faker.sentence(), audience_id=audience.id, preview_text=faker.sentence(), email_domain=email_domain, email_username=faker.user_name(), send_at=faker.past_datetime(tzinfo=timezone.UTC), custom_list_id=None, ) 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, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, "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, "emailDomain": { "id": email_domain.id, "domain": email_domain.domain, "brand": email_domain.brand, }, "emailUsername": campaign.email_username, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "subject": campaign.subject, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "hasUnappliedChanges": campaign.has_unapplied_changes, "updatedBy": campaign.updated_by, "analytics": None, } @pytest.mark.db def test_update_email_campaign( client: TestClient, build_model: BuildModel, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ows_account_client_mock: mock.MagicMock, account: Account, ) -> None: vendor = build_model(Vendor, vendor_id=account.vendor_id, company_brand=Brand.SME) subject = faker.sentence() sender_name = faker.first_name() email_username = faker.user_name() audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fan_count=100, ) email_domain = create_model( EmailDomain, brand=vendor.brand, vendor_id=None, subaccount_id=None ) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, status=EmailCampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=audience.global_participant_id, subject=subject, sender_name=sender_name, email_username=email_username, custom_list_id=None, ) ows_account_client_mock.get_vendor.return_value = vendor response = client.put( f"/campaigns/{campaign.id}", json={ "name": campaign.name, "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, "senderName": sender_name, "emailDomainId": email_domain.id, "emailUsername": email_username, "subject": subject, "previewText": campaign.preview_text, "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, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, "audienceId": campaign.audience_id, "status": campaign.status, "emailDomain": { "id": email_domain.id, "domain": email_domain.domain, "brand": email_domain.brand, }, "emailUsername": campaign.email_username, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "subject": campaign.subject, "cancelReason": campaign.cancel_reason, "cancelledAt": IsISODatetimeOrNone(campaign.cancelled_at), "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "hasUnappliedChanges": campaign.has_unapplied_changes, "updatedBy": campaign.updated_by, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "analytics": None, } @pytest.mark.db def test_send_test_email_email_campaign_validate_emails( client: TestClient, create_model: CreateModel, preference_center_encrypter_mock: mock.MagicMock, ) -> None: preference_center_encrypter_mock.encrypt.return_value = b"token" campaign = create_model(EmailCampaign) response = client.post( f"/campaigns/{campaign.id}/send-test-email", json={ "emails": ["@gmail.com"], "content": base64.b64encode(b"test").decode(), }, ) assert response.status_code == 422 assert response.json() == { "code": "invalid_input", "message": "Invalid input", "fieldErrors": { "emails.0": { "code": "value_error", "message": IsStr(), } }, } @pytest.mark.db def test_schedule_email_campaign( client: TestClient, build_model: BuildModel, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, stripo_client_mock: mock.MagicMock, ) -> None: vendor = build_model(Vendor, company_brand=Brand.SME) subject = faker.sentence() sender_name = faker.first_name() email_username = faker.user_name() audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=vendor.vendor_id, fan_count=100, ) email_domain = create_model(EmailDomain, brand=vendor.brand) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, status=EmailCampaignStatus.DRAFT, vendor_id=audience.vendor_id, subaccount_id=audience.subaccount_id, global_participant_id=audience.global_participant_id, subject=subject, sender_name=sender_name, email_username=email_username, ) stripo_client_mock.compress.return_value = "compressed" 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_unschedule_email_campaign( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model( EmailCampaign, status=EmailCampaignStatus.SCHEDULED, send_at=timezone.now() + timedelta(days=1), ) response = client.post(f"/campaigns/{campaign.id}/unschedule") assert response.status_code == 204 @pytest.mark.db def test_send_email_campaign( client: TestClient, build_model: BuildModel, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, stripo_client_mock: mock.MagicMock, ) -> None: vendor = build_model(Vendor, company_brand=Brand.SME) subject = faker.sentence() sender_name = faker.first_name() email_username = faker.user_name() audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=vendor.vendor_id, fan_count=100, ) email_domain = create_model(EmailDomain, brand=vendor.brand) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, status=EmailCampaignStatus.DRAFT, vendor_id=audience.vendor_id, subaccount_id=audience.subaccount_id, global_participant_id=audience.global_participant_id, subject=subject, sender_name=sender_name, email_username=email_username, ) stripo_client_mock.compress.return_value = "compressed" response = client.post(f"/campaigns/{campaign.id}/send") assert response.status_code == 204 @pytest.mark.db def test_duplicate_email_campaign( client: TestClient, identity_id: str, create_model: CreateModel, create_pg_model: CreatePgModel, ) -> None: email_domain = create_model(EmailDomain) audience = create_pg_model(Audience, target=AudienceTarget.EMAIL) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, custom_list_id=None, ) 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, "fanDataListId": { "id": campaign.global_participant_id, "type": "ARTIST", }, "audienceId": campaign.audience_id, "audience": { "id": audience.id, "name": audience.name, "fanCount": audience.fan_count, "isArchived": audience.is_archived, "target": audience.target, }, "status": "DRAFT", "emailDomain": { "id": email_domain.id, "domain": email_domain.domain, "brand": email_domain.brand, }, "emailUsername": campaign.email_username, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "sendAt": None, "sendAtTimezone": None, "recipientsCount": audience.fan_count, "subject": campaign.subject, "cancelReason": None, "cancelledAt": None, "createdAt": IsStr(), "createdBy": identity_id, "updatedAt": IsStr(), "hasUnappliedChanges": False, "updatedBy": identity_id, "analytics": None, } @pytest.fixture def _real_google_fonts_service(container: Container, db) -> Iterator[None]: from cachelib import BaseCache from email_campaigns.fonts.services.google_fonts import GoogleFontsService cache_mock = mock.MagicMock(spec=BaseCache) cache_mock.get.return_value = None real_service = GoogleFontsService( google_fonts_client=mock.MagicMock(), cache=cache_mock, db=db, ) with container.override(GoogleFontsService, real_service): yield @pytest.mark.db def test_get_email_campaign_preview( client: TestClient, create_model: CreateModel, _real_google_fonts_service: None ) -> None: campaign = create_model(EmailCampaign, custom_fonts=None) response = client.get( f"/campaigns/{campaign.id}/preview", ) assert response.status_code == 200 assert response.json() == { "id": campaign.id, "name": campaign.name, "subject": campaign.subject, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "html": campaign.preview.html, "css": campaign.preview.css, "customFonts": None, "favoriteFonts": [], } @pytest.mark.db def test_get_email_campaign_preview_includes_custom_fonts( client: TestClient, create_model: CreateModel ) -> None: from email_campaigns.api.schemas import CustomFont font = CustomFont( cssFontFamily="'Roboto', sans-serif", name="Roboto", url="https://fonts.example.com/roboto", ) campaign = create_model(EmailCampaign, custom_fonts=[font]) response = client.get(f"/campaigns/{campaign.id}/preview") assert response.status_code == 200 assert response.json()["customFonts"] == [ { "cssFontFamily": "'Roboto', sans-serif", "name": "Roboto", "url": "https://fonts.example.com/roboto", } ] @pytest.mark.db def test_get_email_campaign_preview_favorite_fonts_ordered_by_usage( client: TestClient, create_model: CreateModel, _real_google_fonts_service: None ) -> None: from email_campaigns.api.schemas import CustomFont from email_campaigns.automated.models import AutomatedEmail roboto = CustomFont( cssFontFamily="'Roboto', sans-serif", name="Roboto", url="https://fonts.example.com/roboto", ) lato = CustomFont( cssFontFamily="'Lato', sans-serif", name="Lato", url="https://fonts.example.com/lato", ) base = create_model(EmailCampaign, custom_fonts=None) # Roboto used in 2 SENT campaigns + 1 automated email = 3 uses # Lato used in 1 SENT campaign = 1 use create_model( EmailCampaign, global_participant_id=base.global_participant_id, vendor_id=base.vendor_id, subaccount_id=base.subaccount_id, status=EmailCampaignStatus.SENT, custom_fonts=[roboto, lato], ) create_model( EmailCampaign, global_participant_id=base.global_participant_id, vendor_id=base.vendor_id, subaccount_id=base.subaccount_id, status=EmailCampaignStatus.SENT, custom_fonts=[roboto], ) create_model( AutomatedEmail, global_participant_id=base.global_participant_id, vendor_id=base.vendor_id, subaccount_id=base.subaccount_id, custom_fonts=[roboto], ) response = client.get(f"/campaigns/{base.id}/preview") assert response.status_code == 200 favorite_fonts = response.json()["favoriteFonts"] assert len(favorite_fonts) == 2 assert favorite_fonts[0]["name"] == "Roboto" assert favorite_fonts[1]["name"] == "Lato" @pytest.mark.db def test_get_email_campaign_preview_favorite_fonts_excludes_draft_campaigns( client: TestClient, create_model: CreateModel, _real_google_fonts_service: None ) -> None: from email_campaigns.api.schemas import CustomFont lato = CustomFont( cssFontFamily="'Lato', sans-serif", name="Lato", url="https://fonts.example.com/lato", ) base = create_model(EmailCampaign, custom_fonts=None) create_model( EmailCampaign, global_participant_id=base.global_participant_id, vendor_id=base.vendor_id, subaccount_id=base.subaccount_id, status=EmailCampaignStatus.DRAFT, custom_fonts=[lato], ) response = client.get(f"/campaigns/{base.id}/preview") assert response.status_code == 200 assert response.json()["favoriteFonts"] == [] @pytest.mark.db def test_get_email_campaign_preview_favorite_fonts_count_not_inflated_by_non_sent( client: TestClient, create_model: CreateModel, _real_google_fonts_service: None ) -> None: from email_campaigns.api.schemas import CustomFont roboto = CustomFont( cssFontFamily="'Roboto', sans-serif", name="Roboto", url="https://fonts.example.com/roboto", ) lato = CustomFont( cssFontFamily="'Lato', sans-serif", name="Lato", url="https://fonts.example.com/lato", ) base = create_model(EmailCampaign, custom_fonts=None) # Roboto appears in 1 SENT campaign — its count must be 1, not 2 create_model( EmailCampaign, global_participant_id=base.global_participant_id, vendor_id=base.vendor_id, subaccount_id=base.subaccount_id, status=EmailCampaignStatus.SENT, custom_fonts=[roboto], ) # Roboto also in a DRAFT campaign — must not inflate the count # Lato only in a DRAFT campaign — must not appear at all create_model( EmailCampaign, global_participant_id=base.global_participant_id, vendor_id=base.vendor_id, subaccount_id=base.subaccount_id, status=EmailCampaignStatus.DRAFT, custom_fonts=[roboto, lato], ) response = client.get(f"/campaigns/{base.id}/preview") assert response.status_code == 200 favorite_fonts = response.json()["favoriteFonts"] assert len(favorite_fonts) == 1 assert favorite_fonts[0]["name"] == "Roboto" @pytest.mark.db def test_get_email_campaign_preview_with_substitutions( client: TestClient, create_model: CreateModel, sendgrid_client_mock: mock.MagicMock, ) -> None: sendgrid_client_mock.get_user_privacy_footer_block.return_value = "" campaign = create_model( EmailCampaign, html_content="
Hi -firstName-!
-privacyFooterBlock-", ) response = client.get( f"/campaigns/{campaign.id}/preview", params={"applySubstitutions": True}, ) assert response.status_code == 200 payload = response.json() assert "-firstName-" not in payload["html"] assert "Hi Fan!" in payload["html"] assert "" in payload["html"] @pytest.mark.db def test_update_email_campaign_content( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model( EmailCampaign, status=EmailCampaignStatus.DRAFT, custom_fonts=None ) html = "Hello, world!
" css = "p { color: red; }" response = client.patch( f"/campaigns/{campaign.id}/content", json={ "html": base64.b64encode(html.encode()).decode(), "css": base64.b64encode(css.encode()).decode(), }, ) assert response.status_code == 200 assert response.json() == { "id": campaign.id, "name": campaign.name, "subject": campaign.subject, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "html": html, "css": css, "customFonts": None, "favoriteFonts": None, } @pytest.mark.db def test_delete_email_campaign(client: TestClient, create_model: CreateModel) -> None: campaign = create_model(EmailCampaign, status=EmailCampaignStatus.DRAFT) response = client.delete( f"/campaigns/{campaign.id}", ) assert response.status_code == 204 @pytest.mark.db def test_get_email_campaign_stripo_auth_token( client: TestClient, campaign_stripo_client_mock: mock.MagicMock, create_model: CreateModel, faker: faker.Faker, ) -> None: campaign = create_model(EmailCampaign) auth_token = faker.pystr() campaign_stripo_client_mock.generate_auth_token.return_value = auth_token response = client.get( f"/campaigns/{campaign.id}/stripo/token", ) assert response.status_code == 200 assert response.json() == { "token": auth_token, } @pytest.mark.db def test_get_email_campaign_internal( client: TestClient, create_model: CreateModel, faker: faker.Faker, ) -> None: email_domain = create_model(EmailDomain) campaign = create_model( EmailCampaign, audience_id=faker.uuid4(cast_to=str), sender_name=faker.name(), subject=faker.sentence(), preview_text=faker.sentence(), email_domain=email_domain, email_username=faker.user_name(), send_at=faker.past_datetime(tzinfo=timezone.UTC), ) response = client.get(f"/campaigns/{campaign.id}/internal") 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, "status": campaign.status, "emailDomain": { "id": email_domain.id, "domain": email_domain.domain, "brand": email_domain.brand, }, "emailUsername": campaign.email_username, "previewText": campaign.preview_text, "senderName": campaign.sender_name, "sendAt": IsISODatetimeOrNone(campaign.send_at), "sendAtTimezone": campaign.send_at_timezone, "recipientsCount": campaign.recipients_count, "html": campaign.html_content, "css": campaign.css_content, "subject": campaign.subject, "createdAt": IsISODatetime(campaign.created_at), "createdBy": campaign.created_by, "updatedAt": IsISODatetime(campaign.updated_at), "updatedBy": campaign.updated_by, } @pytest.mark.db def test_get_email_campaign_analytics( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") create_model( EmailAnalyticsV3Dbt, email_id=campaign.id, email_type=campaign.email_type, sends=10, drops=0, delivered=9, opens=6, clicks=5, unique_opens=6, unique_clicks=5, unique_bounces=0, unique_spam_complaints=1, unsubscribes=0, open_rate=0.6, delivered_rate=0.9, click_through_rate=0.3, click_to_open_rate=0.24, bounce_rate=0.0, spam_complaints_rate=0.1, unsubscribe_rate=0.0, unique_opens_dropoff=0.67, unique_clicks_dropoff=0.83, ) create_model( EmailAnalyticsBenchmarkByAccountArtistV2Dbt, global_participant_id=campaign.global_participant_id, custom_list_id=campaign.custom_list_id, vendor_id=campaign.vendor_id, subaccount_id=campaign.subaccount_id, email_type=campaign.email_type, avg_open_rate=0.5, avg_click_through_rate=0.4, avg_bounce_rate=0.1, avg_delivered_rate=0.9, avg_unique_clicks=7, avg_click_to_open_rate=0.6, avg_spam_complaints_rate=0.55, avg_unsubscribe_rate=0.1, ) response = client.get( f"/campaigns/{campaign.id}/analytics", ) assert response.status_code == 200 @pytest.mark.db def test_edit_schedule_email_campaign( client: TestClient, build_model: BuildModel, create_model: CreateModel, create_pg_model: CreatePgModel, faker: faker.Faker, ) -> None: vendor = build_model(Vendor, company_brand=Brand.SME) subject = faker.sentence() sender_name = faker.first_name() email_username = faker.user_name() audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=vendor.vendor_id, fan_count=100, ) email_domain = create_model(EmailDomain, brand=vendor.brand) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, status=EmailCampaignStatus.SCHEDULED, vendor_id=audience.vendor_id, subaccount_id=audience.subaccount_id, global_participant_id=audience.global_participant_id, subject=subject, sender_name=sender_name, email_username=email_username, ) response = client.put( f"/campaigns/{campaign.id}/schedule", json={ "sendAt": faker.future_datetime(tzinfo=timezone.UTC).isoformat(), "timezone": "UTC", }, ) assert response.status_code == 204 def test_autosave_campaign( client: TestClient, container: Container, faker: faker.Faker ) -> None: handler_mock = mock.MagicMock(spec=AutosaveCampaignHandler) handler_mock.handle.return_value = None with container.override(AutosaveCampaignHandler, handler_mock): response = client.post( "/campaigns/autosave", auth=("username", "password"), json={ "key": faker.pystr(), "emailId": faker.pystr(), "html": faker.pystr(), "css": faker.pystr(), }, ) assert response.status_code == 200 def test_autosave_campaign_with_user_id( client: TestClient, container: Container, faker: faker.Faker ) -> None: handler_mock = mock.MagicMock(spec=AutosaveCampaignHandler) handler_mock.handle.return_value = None user_id = faker.uuid4() email_id = faker.uuid4() plugin_data = {"emailId": email_id, "userId": user_id} plugin_ui_data = urllib.parse.quote(json.dumps(plugin_data)) with container.override(AutosaveCampaignHandler, handler_mock): response = client.post( "/campaigns/autosave", auth=("username", "password"), headers={"plugin-ui-data": plugin_ui_data}, json={ "key": faker.pystr(), "emailId": email_id, "html": faker.pystr(), "css": faker.pystr(), }, ) assert response.status_code == 200 # Verify that handler was called with user_id handler_mock.handle.assert_called_once() call_args = handler_mock.handle.call_args[0][0] assert call_args.user_id == user_id assert call_args.email_id == email_id def test_autosave_campaign_without_plugin_data( client: TestClient, container: Container, faker: faker.Faker ) -> None: handler_mock = mock.MagicMock(spec=AutosaveCampaignHandler) handler_mock.handle.return_value = None with container.override(AutosaveCampaignHandler, handler_mock): response = client.post( "/campaigns/autosave", auth=("username", "password"), json={ "key": faker.pystr(), "emailId": faker.pystr(), "html": faker.pystr(), "css": faker.pystr(), }, ) assert response.status_code == 200 # Verify that handler was called with user_id=None handler_mock.handle.assert_called_once() call_args = handler_mock.handle.call_args[0][0] assert call_args.user_id is None def test_autosave_campaign_v2( client: TestClient, container: Container, faker: faker.Faker ) -> None: handler_mock = mock.MagicMock(spec=AutosaveCampaignV2Handler) handler_mock.handle.return_value = None with container.override(AutosaveCampaignV2Handler, handler_mock): response = client.post( "/campaigns/autosave/v2", auth=("username", "password"), json={ "emailId": faker.uuid4(), "userId": faker.uuid4(), "dateTime": 1713360000000, }, ) assert response.status_code == 200 handler_mock.handle.assert_called_once() call_args = handler_mock.handle.call_args[0][0] assert call_args.username == "username" assert call_args.password == "password" @pytest.mark.db def test_get_campaign_live_status( client: TestClient, create_model: CreateModel, ) -> None: campaign = create_model(EmailCampaign, status=EmailCampaignStatus.IN_PROGRESS) batch = create_model( CampaignBatch, campaign_id=campaign.id, domain_id="domain-1", provider="google", batch_size=10, batch_offset=5, ) response = client.get( f"/campaigns/{campaign.id}/status", ) assert response.status_code == 200 assert response.json() == { "status": campaign.status, "emailsToSend": batch.batch_size, "sentEmails": batch.batch_offset, } @pytest.mark.db def test_get_email_campaign_link_analytics( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign", custom_list_id=None) create_model( EmailAnalyticsV3Dbt, email_id=campaign.id, email_type=campaign.email_type, sends=10, drops=0, delivered=9, opens=6, clicks=5, unique_opens=6, unique_clicks=5, unique_bounces=0, unique_spam_complaints=1, unsubscribes=0, open_rate=0.6, delivered_rate=0.9, click_through_rate=0.3, click_to_open_rate=0.24, bounce_rate=0.0, spam_complaints_rate=0.1, unsubscribe_rate=0.0, unique_opens_dropoff=0.67, unique_clicks_dropoff=0.83, ) create_model( EmailAnalyticsBenchmarkByAccountArtistV2Dbt, global_participant_id=campaign.global_participant_id, custom_list_id=campaign.custom_list_id, vendor_id=campaign.vendor_id, subaccount_id=campaign.subaccount_id, email_type=campaign.email_type, avg_open_rate=0.5, avg_click_through_rate=0.4, avg_bounce_rate=0.1, avg_delivered_rate=0.9, avg_unique_clicks=7, avg_click_to_open_rate=0.6, avg_spam_complaints_rate=0.55, avg_unsubscribe_rate=0.1, ) create_model( EmailAnalyticsBenchmarkByLinktV3Dbt, email_id=campaign.id, email_type=campaign.email_type, url="http://www.awesome-artist.com", total_clicks=10, unique_clicks=10, percentage_of_total_clicks=1.0, ) response = client.get( f"/campaigns/{campaign.id}/analytics/links", ) assert response.status_code == 200 @pytest.mark.db def test_get_email_campaign_privacy_footer( client: TestClient, create_model: CreateModel, sendgrid_client_mock: Mock ) -> None: campaign = create_model(EmailCampaign) sendgrid_client_mock.get_user_privacy_footer_block.return_value = ( "privacy block" ) response = client.get( f"/campaigns/{campaign.id}/privacy-footer", ) assert response.status_code == 200 assert response.json() == { "content": campaign.id, } @pytest.mark.db def test_estimate_email_campaign_send_time( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: email_domain = create_model( EmailDomain, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fan_count=1000, recipients_count_by_email_provider={"google": 1000}, ) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, status=EmailCampaignStatus.SCHEDULED, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, send_at=timezone.now() + timedelta(hours=1), recipients_count=1000, ) create_model( DeliveryQuota, domain_id=email_domain.id, provider="google", last_hourly_quota=1000.0, ) response = client.get(f"/campaigns/{campaign.id}/send-estimation") assert response.status_code == 200 assert response.json() == { "estimatedSendDuration": IsNumeric(), "emailsToSend": 1000, "emailsCount": 1000, } @pytest.mark.db def test_estimate_email_campaign_send_time_with_send_at( client: TestClient, create_model: CreateModel, create_pg_model: CreatePgModel, account: Account, ) -> None: email_domain = create_model( EmailDomain, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) audience = create_pg_model( Audience, target=AudienceTarget.EMAIL, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, fan_count=1000, recipients_count_by_email_provider={"google": 1000}, ) campaign = create_model( EmailCampaign, email_domain=email_domain, audience_id=audience.id, status=EmailCampaignStatus.DRAFT, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, recipients_count=1000, ) create_model( DeliveryQuota, domain_id=email_domain.id, provider="google", last_hourly_quota=1000.0, ) send_at = timezone.now() + timedelta(hours=2) response = client.get( f"/campaigns/{campaign.id}/send-estimation", params={"sendAt": send_at.isoformat()}, ) assert response.status_code == 200 assert response.json() == { "estimatedSendDuration": IsNumeric(), "emailsToSend": 1000, "emailsCount": 1000, } @pytest.mark.db def test_get_email_campaign_sales_analytics( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") create_model( ShopifyEmailOrdersDbt, email_id=campaign.id, shop_id="shop1", currency="USD", country_code="US", shop_name="Shop One", total_orders=5, items_sold=10, total_sales_usd=120.0, total_discounts_usd=10.0, total_taxes_usd=8.0, total_delivery_usd=5.0, total_net_sales_usd=100.0, total_refunds_usd=10.0, first_time_buyer_rate=0.4, ) response = client.get(f"/campaigns/{campaign.id}/analytics/sales") assert response.status_code == 200 assert response.json() == { "data": { "ordersCount": 5, "itemsSold": 10, "netSalesUsd": 100.0, "grossSalesUsd": 120.0, "discountsUsd": 10.0, "taxesUsd": 8.0, "shippingUsd": 5.0, "refundsUsd": 10.0, "averageUnitsPerOrder": 2.0, "averageOrderAmount": 20.0, "firstTimeBuyerRate": 0.4, } } @pytest.mark.db def test_get_email_campaign_sales_products_analytics( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") create_model( ShopifyEmailProductsDbt, email_id=campaign.id, product_title="Vinyl Record", product_type="Music", currency="USD", total_quantity_sold=5, total_net_sales_usd=200.0, ) create_model( ShopifyEmailProductsDbt, email_id=campaign.id, product_title="T-Shirt", product_type="Merch", currency="USD", total_quantity_sold=10, total_net_sales_usd=100.0, ) create_model(ShopifyProductTypesDbt, product_type="Music", chart_eligible=True) create_model(ShopifyProductTypesDbt, product_type="Merch", chart_eligible=False) response = client.get(f"/campaigns/{campaign.id}/analytics/sales/products") assert response.status_code == 200 assert response.json() == { "data": [ { "productTitle": "Vinyl Record", "productType": "Music", "itemsSold": 5, "netSalesUsd": 200.0, "chartEligible": True, }, { "productTitle": "T-Shirt", "productType": "Merch", "itemsSold": 10, "netSalesUsd": 100.0, "chartEligible": False, }, ] } @pytest.mark.db def test_get_email_campaign_sales_locations_analytics( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") create_model( ShopifyEmailOrdersDbt, email_id=campaign.id, shop_id="shop1", currency="USD", country_code="US", shop_name="Shop One", total_orders=5, items_sold=10, total_sales_usd=220.0, total_discounts_usd=10.0, total_taxes_usd=8.0, total_delivery_usd=5.0, total_net_sales_usd=200.0, ) create_model( ShopifyEmailOrdersDbt, email_id=campaign.id, shop_id="shop2", currency="USD", country_code="CA", shop_name="Shop Two", total_orders=3, items_sold=6, total_sales_usd=115.0, total_discounts_usd=5.0, total_taxes_usd=4.0, total_delivery_usd=3.0, total_net_sales_usd=100.0, ) response = client.get(f"/campaigns/{campaign.id}/analytics/sales/locations") assert response.status_code == 200 assert response.json() == { "data": [ { "countryCode": "US", "itemsSold": 10, "netSalesUsd": 200.0, "salesRate": pytest.approx(200 / 300), }, { "countryCode": "CA", "itemsSold": 6, "netSalesUsd": 100.0, "salesRate": pytest.approx(100 / 300), }, ] } @pytest.mark.db def test_get_email_campaign_sales_shops_analytics( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") create_model( ShopifyEmailOrdersDbt, email_id=campaign.id, shop_id="shop1", currency="USD", country_code="US", shop_name="Big Shop", total_orders=5, items_sold=10, total_sales_usd=320.0, total_discounts_usd=10.0, total_taxes_usd=8.0, total_delivery_usd=5.0, total_net_sales_usd=300.0, ) create_model( ShopifyEmailOrdersDbt, email_id=campaign.id, shop_id="shop2", currency="USD", country_code="CA", shop_name="Small Shop", total_orders=2, items_sold=4, total_sales_usd=115.0, total_discounts_usd=5.0, total_taxes_usd=4.0, total_delivery_usd=3.0, total_net_sales_usd=100.0, ) response = client.get(f"/campaigns/{campaign.id}/analytics/sales/shops") assert response.status_code == 200 assert response.json() == { "data": [ {"shopName": "Big Shop", "itemsSold": 10, "netSalesUsd": 300.0}, {"shopName": "Small Shop", "itemsSold": 4, "netSalesUsd": 100.0}, ] } @pytest.mark.db def test_get_email_campaign_sales_products_analytics_no_data( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") response = client.get(f"/campaigns/{campaign.id}/analytics/sales/products") assert response.status_code == 200 assert response.json() == {"data": []} @pytest.mark.db def test_get_email_campaign_sales_locations_analytics_no_data( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") response = client.get(f"/campaigns/{campaign.id}/analytics/sales/locations") assert response.status_code == 200 assert response.json() == {"data": []} @pytest.mark.db def test_get_email_campaign_sales_shops_analytics_no_data( client: TestClient, create_model: CreateModel ) -> None: campaign = create_model(EmailCampaign, name="Test Campaign") response = client.get(f"/campaigns/{campaign.id}/analytics/sales/shops") assert response.status_code == 200 assert response.json() == {"data": []}