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": [],
},
],
}
],
}