import uuid from datetime import UTC, datetime import faker import pytest from dirty_equals import IsPartialDict from fansifter_common.encrypter import Encrypter from fansifter_common.identifiers.types import ( CRMProfileIdentifier, DoubleOptInIdentifier, ) from starlette.testclient import TestClient from preference_center.profile.models import MailingList, Profile, Subscription from tests.unit.equals import IsDateOrNone, IsMaskedEmail, IsMaskedPhoneNumberOrNone from tests.unit.faker import FakerTyped from tests.unit.types import CreateDoubleOptInToken, CreateModel, CreateProfileToken def test_hello(client: TestClient) -> None: response = client.get("/hello/") assert response.status_code == 200 assert response.json() == {"status": "ok"} def test_get_settings(client: TestClient, fake: FakerTyped) -> None: response = client.get( "/settings", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=True), }, ) assert response.status_code == 200 assert response.json() == { "isDateOfBirthAllowed": False, "privacyLinks": { "en": [ { "name": "Privacy & Cookie Policy", "url": "https://www.sonymusic.com/privacy-policy/", }, { "name": "Terms and Condition", "url": "https://www.sonymusic.com/terms-and-conditions/", }, { "name": "How we use your data", "url": "https://www.sonymusic.com/how-we-use-your-data/", }, { "name": "CPRA Do Not Sell or Share Request", "url": "https://www.sonymusic.com/ccpa-contact-form/", }, { "name": "Additional Information for California Residents", "url": "https://www.sonymusic.com/privacy-policy/#_Additional_Information_for_1", }, ] }, "legalEntity": { "name": "Sony Music Entertainment", "address": "25 Madison Avenue, 22nd Floor, New York, NY 10010", }, } @pytest.mark.db def test_get_profile( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.get( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, ) assert response.status_code == 200 assert response.json() == { "email": IsMaskedEmail(profile.email), "firstName": profile.first_name, "lastName": profile.last_name, "phoneNumber": IsMaskedPhoneNumberOrNone(profile.phone_number), "dateOfBirth": IsDateOrNone(profile.date_of_birth), "birthday": profile.birthday, "gender": profile.gender, "countryCode": profile.country_code, "city": profile.city, "address": profile.address, "zipCode": profile.zip_code, } @pytest.mark.db def test_get_profile_not_found( client: TestClient, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: token = create_profile_token(CRMProfileIdentifier(crmId=fake.pystr())) response = client.get( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, ) assert response.status_code == 404 assert response.json() == { "code": "profile_not_found", "message": "Profile not found", } @pytest.mark.db def test_get_profile_invalid_token(client: TestClient, fake: FakerTyped) -> None: token = "invalid" response = client.get( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, ) assert response.status_code == 404 assert response.json() == { "code": "profile_not_found", "message": "Profile not found", } @pytest.mark.db def test_update_profile( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) first_name = fake.first_name() phone_number = fake.phone_number() response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "firstName": first_name, "phoneNumber": phone_number, }, ) assert response.status_code == 200 assert response.json() == { "email": IsMaskedEmail(profile.email), "firstName": first_name, "lastName": profile.last_name, "phoneNumber": IsMaskedPhoneNumberOrNone(phone_number), "dateOfBirth": IsDateOrNone(profile.date_of_birth), "birthday": profile.birthday, "gender": profile.gender, "countryCode": profile.country_code, "city": profile.city, "address": profile.address, "zipCode": profile.zip_code, } @pytest.mark.db def test_update_profile_not_found( client: TestClient, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: token = create_profile_token(CRMProfileIdentifier(crmId=fake.pystr())) first_name = fake.first_name() response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "firstName": first_name, }, ) assert response.status_code == 404 assert response.json() == { "code": "profile_not_found", "message": "Profile not found", } def test_update_profile_invalid_input( client: TestClient, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: token = create_profile_token(CRMProfileIdentifier(crmId=fake.pystr())) phone_number = fake.pystr() response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "phoneNumber": phone_number, }, ) assert response.status_code == 422 assert response.json() == { "code": "invalid_input", "message": "Invalid input", "fieldErrors": { "phoneNumber": { "code": "invalid_phone_number", "message": "value is not a valid phone number", } }, } @pytest.mark.db def test_update_profile_date_of_birth_update_not_allowed( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) date_of_birth = fake.date_of_birth() response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=True), }, json={ "dateOfBirth": date_of_birth.isoformat(), }, ) assert response.status_code == 400 assert response.json() == { "code": "profile_date_of_birth_update_not_allowed", "message": "Profile date of birth update not allowed for your country", } @pytest.mark.db def test_update_profile_birthday_update( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() create_model(Profile, crm_id=crm_id, birthday="01/01/1990") token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) birthday = fake.birthday() response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=True), }, json={ "birthday": birthday, }, ) assert response.status_code == 200 assert response.json() == IsPartialDict( { "dateOfBirth": None, "birthday": birthday, } ) @pytest.mark.db def test_update_profile_birthday_update_not_allowed( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) birthday = fake.birthday() response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "birthday": birthday, }, ) assert response.status_code == 400 assert response.json() == { "code": "profile_birthday_update_not_allowed", "message": ( "Profile birthday update not allowed for your country, " "please use date of birth instead" ), } @pytest.mark.db def test_update_profile_email_must_be_unique( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: other_profile = create_model(Profile) crm_id = fake.pystr() create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "email": other_profile.email, }, ) assert response.status_code == 400 assert response.json() == { "code": "invalid_input", "message": "Invalid input", "fieldErrors": { "email": { "code": "profile_email_already_in_use", "message": "The email address is already in use", } }, } @pytest.mark.db def test_update_profile_empty_phone_number( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, phone_number="+37258000111", crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "phoneNumber": "", }, ) assert response.status_code == 200 assert response.json() == { "email": IsMaskedEmail(profile.email), "firstName": profile.first_name, "lastName": profile.last_name, "phoneNumber": None, "dateOfBirth": IsDateOrNone(profile.date_of_birth), "birthday": profile.birthday, "gender": profile.gender, "countryCode": profile.country_code, "city": profile.city, "address": profile.address, "zipCode": profile.zip_code, } @pytest.mark.db def test_update_profile_empty_date_of_birth( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, date_of_birth=fake.date_of_birth(), crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "dateOfBirth": "", }, ) assert response.status_code == 200 assert response.json() == { "email": IsMaskedEmail(profile.email), "firstName": profile.first_name, "lastName": profile.last_name, "phoneNumber": IsMaskedPhoneNumberOrNone(profile.phone_number), "dateOfBirth": None, "birthday": profile.birthday, "gender": profile.gender, "countryCode": profile.country_code, "city": profile.city, "address": profile.address, "zipCode": profile.zip_code, } @pytest.mark.db def test_update_profile_empty_birthday( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, birthday=fake.birthday(), crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=True), }, json={ "birthday": "", }, ) assert response.status_code == 200 assert response.json() == { "email": IsMaskedEmail(profile.email), "firstName": profile.first_name, "lastName": profile.last_name, "phoneNumber": IsMaskedPhoneNumberOrNone(profile.phone_number), "dateOfBirth": None, "birthday": None, "gender": profile.gender, "countryCode": profile.country_code, "city": profile.city, "address": profile.address, "zipCode": profile.zip_code, } @pytest.mark.db def test_update_profile_nullables( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model( Profile, birthday=fake.birthday(), date_of_birth=fake.date_of_birth(), crm_id=crm_id, ) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.patch( f"/profile/{token}", headers={ "x-ipcountry": fake.country_code(date_of_birth_restricted=False), }, json={ "firstName": "", "lastName": "", "countryCode": "", "city": "", "address": "", "zipCode": "", "dateOfBirth": "", }, ) assert response.status_code == 200 assert response.json() == { "email": IsMaskedEmail(profile.email), "firstName": None, "lastName": None, "phoneNumber": IsMaskedPhoneNumberOrNone(profile.phone_number), "dateOfBirth": None, "birthday": profile.birthday, "gender": profile.gender, "countryCode": None, "city": None, "address": None, "zipCode": None, } @pytest.mark.db def test_delete_profile( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.delete(f"/profile/{token}") assert response.status_code == 204 @pytest.mark.db def test_get_subscriptions( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, crm_id=crm_id) subscription = create_model(Subscription, profile_id=profile.id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) response = client.get(f"/profile/{token}/subscriptions") assert response.status_code == 200 assert response.json() == [ { "id": subscription.id, "name": subscription.name, "isActive": subscription.is_active, } ] @pytest.mark.db def test_update_subscriptions( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, fake: FakerTyped, ) -> None: crm_id = fake.pystr() profile = create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) mailing_list_1 = create_model(MailingList, name="Subscription 1") mailing_list_2 = create_model(MailingList, name="Subscription 2") subscription_1 = create_model( Subscription, mailing_list=mailing_list_1, profile_id=profile.id, is_active=False, ) subscription_2 = create_model( Subscription, mailing_list=mailing_list_2, profile_id=profile.id, is_active=True, ) response = client.put( f"/profile/{token}/subscriptions", json={ "data": [ {"id": subscription_1.id, "isActive": True}, {"id": subscription_2.id, "isActive": False}, ], }, ) assert response.status_code == 200 assert response.json() == [ { "id": subscription_1.id, "name": subscription_1.name, "isActive": True, }, { "id": subscription_2.id, "name": subscription_2.name, "isActive": False, }, ] @pytest.mark.db def test_unsubscribe_all( client: TestClient, create_model: CreateModel, create_profile_token: CreateProfileToken, faker: faker.Faker, ) -> None: crm_id = faker.pystr() profile = create_model(Profile, crm_id=crm_id) token = create_profile_token(CRMProfileIdentifier(crmId=crm_id)) subscription_1 = create_model( Subscription, profile_id=profile.id, is_active=True, ) subscription_2 = create_model( Subscription, profile_id=profile.id, is_active=True, ) response = client.delete(f"/profile/{token}/subscriptions") assert response.status_code == 204 assert not subscription_1.is_active assert not subscription_2.is_active @pytest.mark.db def test_unsubscribe_all_by_email( client: TestClient, create_model: CreateModel ) -> None: profile = create_model(Profile) subscription_1 = create_model( Subscription, profile_id=profile.id, is_active=True, ) subscription_2 = create_model( Subscription, profile_id=profile.id, is_active=True, ) response = client.post( "/subscriptions/unsubscribe-all", json={"email": profile.email} ) assert response.status_code == 204 assert not subscription_1.is_active assert not subscription_2.is_active def test_encrypt_songwhip_fan_consent_id_success( client: TestClient, ) -> None: payload = {"fanConsentId": str(uuid.uuid4())} response = client.post("/songwhip/encrypt-fan-consent-id", json=payload) assert response.status_code == 200 def test_encrypt_songwhip_fan_consent_id_fail( client: TestClient, ) -> None: payload = {"fail_test_fanConsentId": str(uuid.uuid4())} response = client.post("/songwhip/encrypt-fan-consent-id", json=payload) assert response.status_code == 422 def test_songwhip_fan_double_opt_in_consent_confirm_success( client: TestClient, dummy_encrypter: Encrypter, ) -> None: response = client.post( "/songwhip/fan-double-opt-in-consent-confirm", params={ "fanConsentId": dummy_encrypter.encrypt(b"test").decode(), "country": "UK", }, ) assert response.status_code == 200 def test_songwhip_fan_double_opt_in_consent_confirm_wrong_encrypted_fan_consent_id( client: TestClient, ) -> None: response = client.post( "/songwhip/fan-double-opt-in-consent-confirm", params={ "fanConsentId": "test", "country": "UK", }, ) assert response.status_code == 400 def test_songwhip_fan_double_opt_in_consent_confirm_request_validation_fail( client: TestClient, ) -> None: response = client.post("/songwhip/fan-double-opt-in-consent-confirm") assert response.status_code == 422 # Double Opt-In def test_double_opt_in_confirm_success( client: TestClient, create_double_opt_in_token: CreateDoubleOptInToken, ) -> None: identifier = DoubleOptInIdentifier( eventRowId=str(uuid.uuid4()), emailId=str(uuid.uuid4()), automatedEmailTriggerId=str(uuid.uuid4()), iat=datetime.now(UTC).timestamp(), ) token = create_double_opt_in_token(identifier) response = client.post( "/double-opt-in/confirm", params={ "token": token, }, ) assert response.status_code == 200 def test_double_opt_in_confirm_wrong_token( client: TestClient, ) -> None: response = client.post( "/double-opt-in/confirm", params={ "token": "test", }, ) assert response.status_code == 400 def test_double_opt_in_confirm_request_validation_fail( client: TestClient, ) -> None: response = client.post("/double-opt-in/confirm") assert response.status_code == 422