diff --git a/emails/models.py b/emails/models.py index 25ddfcc96a..c944a5694b 100644 --- a/emails/models.py +++ b/emails/models.py @@ -19,6 +19,7 @@ get_supported_language_variant, ) +from allauth.socialaccount.models import SocialAccount from rest_framework.authtoken.models import Token from api.exceptions import ErrorContextType, RelayAPIException @@ -276,10 +277,11 @@ def at_max_free_aliases(self) -> bool: return relay_addresses_count >= settings.MAX_NUM_FREE_ALIASES @property - def fxa(self): + def fxa(self) -> SocialAccount | None: # Note: we are NOT using .filter() here because it invalidates # any profile instances that were queried with prefetch_related, which # we use in at least the profile view to minimize queries + assert hasattr(self.user, "socialaccount_set") for sa in self.user.socialaccount_set.all(): if sa.provider == "fxa": return sa diff --git a/emails/tests/models_tests.py b/emails/tests/models_tests.py index 3d1ec24ad3..f8095eac81 100644 --- a/emails/tests/models_tests.py +++ b/emails/tests/models_tests.py @@ -47,7 +47,11 @@ def make_free_test_user(email: str = "") -> User: user_profile.server_storage = True user_profile.save() baker.make( - SocialAccount, user=user, provider="fxa", extra_data={"avatar": "avatar.png"} + SocialAccount, + user=user, + uid=str(uuid4()), + provider="fxa", + extra_data={"avatar": "avatar.png"}, ) return user @@ -83,6 +87,7 @@ def upgrade_test_user_to_premium(user): baker.make( SocialAccount, user=user, + uid=str(uuid4()), provider="fxa", extra_data={"avatar": "avatar.png", "subscriptions": [random_sub]}, ) @@ -1116,6 +1121,7 @@ def test_flags_profile_when_emails_forwarded_abuse_threshold_met(self) -> None: self.profile.update_abuse_metric(email_forwarded=True) self.abuse_metric.refresh_from_db() + assert self.profile.fxa self.mocked_abuse_info.assert_called_once_with( "Abuse flagged", extra={ @@ -1139,6 +1145,7 @@ def test_flags_profile_when_forwarded_email_size_abuse_threshold_met(self) -> No self.profile.update_abuse_metric(forwarded_email_size=50) self.abuse_metric.refresh_from_db() + assert self.profile.fxa self.mocked_abuse_info.assert_called_once_with( "Abuse flagged", extra={ diff --git a/emails/tests/signals_tests.py b/emails/tests/signals_tests.py index 65bb5ccd58..9bfd79b2c4 100644 --- a/emails/tests/signals_tests.py +++ b/emails/tests/signals_tests.py @@ -26,6 +26,7 @@ def test_remove_level_one_email_trackers_enabled(self) -> None: self.profile.remove_level_one_email_trackers = True self.profile.save() + assert self.profile.fxa expected_hashed_uid = sha256(self.profile.fxa.uid.encode("utf-8")).hexdigest() self.mocked_incr.assert_called_once_with("tracker_removal_enabled") self.mocked_events_info.assert_called_once_with( @@ -45,6 +46,7 @@ def test_remove_level_one_email_trackers_disabled(self) -> None: self.profile.remove_level_one_email_trackers = False self.profile.save() + assert self.profile.fxa expected_hashed_uid = sha256(self.profile.fxa.uid.encode("utf-8")).hexdigest() self.mocked_incr.assert_called_once_with("tracker_removal_disabled") self.mocked_events_info.assert_called_once_with( diff --git a/emails/tests/views_tests.py b/emails/tests/views_tests.py index b275839011..5755d7db92 100644 --- a/emails/tests/views_tests.py +++ b/emails/tests/views_tests.py @@ -462,7 +462,7 @@ def test_block_list_email_former_premium_user(self) -> None: self.ra.save() # Remove premium from the user - fxa_account = self.premium_user.profile.fxa + assert (fxa_account := self.premium_user.profile.fxa) fxa_account.extra_data["subscriptions"] = [] fxa_account.save() assert not self.premium_user.profile.has_premium diff --git a/phones/tests/mgmt_delete_phone_data_tests.py b/phones/tests/mgmt_delete_phone_data_tests.py index 43ee471f89..0f75e3410e 100644 --- a/phones/tests/mgmt_delete_phone_data_tests.py +++ b/phones/tests/mgmt_delete_phone_data_tests.py @@ -76,6 +76,7 @@ def test_active_user(phone_user: User) -> None: assert RealPhone.objects.filter(user=phone_user).exists() relay_number = RelayNumber.objects.get(user=phone_user) assert InboundContact.objects.filter(relay_number=relay_number).count() == 2 + assert phone_user.profile.fxa stdout = StringIO() call_command(THE_COMMAND, phone_user.profile.fxa.uid, "--force", stdout=stdout) @@ -105,6 +106,7 @@ def test_no_contacts(phone_user: User) -> None: """A user's real phone and relay phone are deleted, even without contacts.""" relay_number = RelayNumber.objects.get(user=phone_user) InboundContact.objects.filter(relay_number=relay_number).delete() + assert phone_user.profile.fxa stdout = StringIO() call_command(THE_COMMAND, phone_user.profile.fxa.uid, "--force", stdout=stdout) @@ -133,6 +135,7 @@ def test_no_contacts(phone_user: User) -> None: def test_no_relay_phone(phone_user: User) -> None: """A user's real phone is deleted, even without a relay phone setup.""" RelayNumber.objects.filter(user=phone_user).delete() + assert phone_user.profile.fxa stdout = StringIO() call_command(THE_COMMAND, phone_user.profile.fxa.uid, "--force", stdout=stdout) @@ -160,6 +163,7 @@ def test_no_real_phone(phone_user: User) -> None: """Nothing is done if a user doesn't have a real phone setup.""" RelayNumber.objects.filter(user=phone_user).delete() RealPhone.objects.filter(user=phone_user).delete() + assert phone_user.profile.fxa stdout = StringIO() call_command(THE_COMMAND, phone_user.profile.fxa.uid, "--force", stdout=stdout) @@ -195,6 +199,7 @@ def test_user_not_found() -> None: def test_confirm_yes_active_user(phone_user: User) -> None: """When the user confirms yes, the data is deleted.""" + assert phone_user.profile.fxa stdout = StringIO() with patch("builtins.input", return_value="Y"): call_command(THE_COMMAND, phone_user.profile.fxa.uid, stdout=stdout) @@ -221,6 +226,7 @@ def test_confirm_yes_active_user(phone_user: User) -> None: def test_confirm_no_active_user(phone_user: User) -> None: """When the user confirms no, the data is not deleted.""" + assert phone_user.profile.fxa stdout = StringIO() with patch("builtins.input", return_value="n"): call_command(THE_COMMAND, phone_user.profile.fxa.uid, stdout=stdout) @@ -248,6 +254,7 @@ def test_confirm_no_active_user(phone_user: User) -> None: def test_confirm_retry_active_user(phone_user: User) -> None: """The user keeps trying until they answer Y or N.""" + assert phone_user.profile.fxa stdout = StringIO() with patch("builtins.input", side_effect=("maybe", "ok no", "no", "n")): call_command(THE_COMMAND, phone_user.profile.fxa.uid, stdout=stdout) @@ -278,6 +285,7 @@ def test_confirm_retry_active_user(phone_user: User) -> None: def test_confirmation_skipped_when_no_data(phone_user: User) -> None: """When the user does not have data, confirmation is skipped.""" + assert phone_user.profile.fxa RelayNumber.objects.filter(user=phone_user).delete() RealPhone.objects.filter(user=phone_user).delete() diff --git a/privaterelay/management/commands/update_phone_remaining_stats.py b/privaterelay/management/commands/update_phone_remaining_stats.py index 2575a5243b..954695865f 100644 --- a/privaterelay/management/commands/update_phone_remaining_stats.py +++ b/privaterelay/management/commands/update_phone_remaining_stats.py @@ -37,10 +37,14 @@ def get_next_reset_date(profile: Profile) -> datetime: if profile.date_phone_subscription_reset is None: # there is a problem with the sync_phone_related_dates_on_profile # or a new foxfooder whose date_phone_subscription_reset did not get set in + if profile.fxa: + fxa_uid = profile.fxa.uid + else: + fxa_uid = "None" logger.error( "phone_user_profile_dates_not_set", extra={ - "fxa_uid": profile.fxa.uid, + "fxa_uid": fxa_uid, "date_subscribed_phone": profile.date_phone_subscription_end, "date_phone_subscription_start": profile.date_phone_subscription_start, "date_phone_subscription_reset": profile.date_phone_subscription_reset,