Implement email verification flow (#21603)
* feat(): add email_verification property to UserProfile * feat(users): add useful properties to SuppressedEmailVerification model * feat(devhub): add email verification flow view logic * chore(): use date only values for socketlabs recipient search
This commit is contained in:
Родитель
b0a52c5336
Коммит
4ca4686edd
|
@ -0,0 +1,7 @@
|
|||
{% load i18n %}{# L10n: This is an email. Whitespace matters #}{% blocktrans %}Hello,
|
||||
|
||||
Your email was successfully verified.
|
||||
|
||||
Regards,
|
||||
|
||||
The Mozilla Add-ons Team{% endblocktrans %}
|
|
@ -1,9 +1,14 @@
|
|||
{% if waffle.switch("suppressed-email") and request.user.is_authenticated and request.user.suppressed_email %}
|
||||
{% if waffle.switch("suppressed-email") and request.user.is_authenticated and request.user.suppressed_email and request.path != url('devhub.email_verification') %}
|
||||
<div class="notification-box warning" id="suppressed-email">
|
||||
<p>
|
||||
{% trans email=request.user.email %}
|
||||
We have discovered that your email "{{ email }}" is unable to receive emails from us. Please update your email address to one that can receive emails from us.
|
||||
{% endtrans %}
|
||||
<a href="{{ url('devhub.email_verification') }}">
|
||||
{% trans %}
|
||||
Learn more
|
||||
{% endtrans %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
{% extends "devhub/base.html" %}
|
||||
|
||||
{% set title = _('Email Address Verification') %}
|
||||
|
||||
{% block title %}{{ dev_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
<div id="{{ state }}">
|
||||
{% if state == "email_verified" %}
|
||||
{% trans %}
|
||||
Your email address is verified.
|
||||
{% endtrans %}
|
||||
{% elif state == "email_suppressed" %}
|
||||
{% trans %}
|
||||
Please verify your email by clicking "Verify email" above.
|
||||
{% endtrans %}
|
||||
{% elif state == "verification_expired" %}
|
||||
{% trans %}
|
||||
Could not verify email address. The verification link has expired.
|
||||
{% endtrans %}
|
||||
{% elif state == "verification_pending" %}
|
||||
{% trans %}
|
||||
Working... Please be patient.
|
||||
{% endtrans %}
|
||||
<div class="loader"></div>
|
||||
{% elif state == "verification_failed" %}
|
||||
{% trans %}
|
||||
Failed to send confirmation email. Please try again.
|
||||
If you no longer have access to your email address, please update your mozilla account email address.
|
||||
<a
|
||||
href="https://support.mozilla.org/en-US/kb/change-primary-email-address-firefox-accounts"
|
||||
target="_blank"
|
||||
>
|
||||
Change email address
|
||||
</a>
|
||||
{% endtrans %}
|
||||
{% elif state == "verification_timedout" %}
|
||||
{% trans %}
|
||||
This is taking longer than expected. Try again.
|
||||
{% endtrans %}
|
||||
{% elif state == "confirmation_pending" %}
|
||||
{% trans email=request.user.email %}
|
||||
An email with a confirmation link has been sent to your email address: {{ email }}. Please click the link to confirm your email address. If you did not receive the email, please check your spam folder.
|
||||
{% endtrans %}
|
||||
{% elif state == "confirmation_unauthorized" %}
|
||||
{% trans email=request.user.email %}
|
||||
The provided code is associated with another user's email. Please use the link in the email sent to your email address {{ email }}.
|
||||
{% endtrans %}
|
||||
{% endif %}
|
||||
{% if render_button %}
|
||||
{% with submit_text=button_text %}
|
||||
{% include 'devhub/verify_email_form.html' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,6 @@
|
|||
<form action="{{ url('devhub.email_verification') }}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="Button" type="submit">
|
||||
{{ submit_text | default('Verify email address') }}
|
||||
</button>
|
||||
</form>
|
|
@ -12,6 +12,7 @@ from django.urls import reverse
|
|||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import trim_whitespace
|
||||
|
||||
import freezegun
|
||||
import pytest
|
||||
import responses
|
||||
from pyquery import PyQuery as pq
|
||||
|
@ -39,12 +40,17 @@ from olympia.constants.promoted import RECOMMENDED
|
|||
from olympia.devhub.decorators import dev_required
|
||||
from olympia.devhub.models import BlogPost
|
||||
from olympia.devhub.tasks import validate
|
||||
from olympia.devhub.views import get_next_version_number
|
||||
from olympia.devhub.views import VERIFY_EMAIL_STATE, get_next_version_number
|
||||
from olympia.files.models import FileUpload
|
||||
from olympia.files.tests.test_models import UploadMixin
|
||||
from olympia.ratings.models import Rating
|
||||
from olympia.translations.models import Translation, delete_translation
|
||||
from olympia.users.models import IPNetworkUserRestriction, SuppressedEmail, UserProfile
|
||||
from olympia.users.models import (
|
||||
IPNetworkUserRestriction,
|
||||
SuppressedEmail,
|
||||
SuppressedEmailVerification,
|
||||
UserProfile,
|
||||
)
|
||||
from olympia.users.tests.test_views import UserViewBase
|
||||
from olympia.versions.models import Version, VersionPreview
|
||||
from olympia.zadmin.models import set_config
|
||||
|
@ -460,6 +466,10 @@ class TestHome(TestCase):
|
|||
|
||||
assert self.user_profile.email in doc('#suppressed-email').text()
|
||||
assert doc('#suppressed-email').length == 1
|
||||
assert 'Learn more' in doc('#suppressed-email a').text()
|
||||
assert reverse('devhub.email_verification') in doc('#suppressed-email a').attr(
|
||||
'href'
|
||||
)
|
||||
|
||||
@override_switch('suppressed-email', active=False)
|
||||
def test_suppressed_email_hidden_by_flase(self):
|
||||
|
@ -2172,3 +2182,269 @@ class TestStatsLinksInManageMySubmissionsPage(TestCase):
|
|||
assert reverse('stats.overview', args=[self.addon.slug]) in str(
|
||||
response.content
|
||||
)
|
||||
|
||||
|
||||
class TestVerifyEmail(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse('devhub.email_verification')
|
||||
self.user_profile = user_factory()
|
||||
self.client.force_login(self.user_profile)
|
||||
|
||||
def _create_suppressed_email(self, user):
|
||||
return SuppressedEmail.objects.create(email=user.email)
|
||||
|
||||
def _create_suppressed_email_verification(
|
||||
self, user, suppressed_email=None, status=None
|
||||
):
|
||||
if suppressed_email is None:
|
||||
suppressed_email = self._create_suppressed_email(user)
|
||||
|
||||
if status is None:
|
||||
status = SuppressedEmailVerification.STATUS_CHOICES.Pending
|
||||
|
||||
return SuppressedEmailVerification.objects.create(
|
||||
suppressed_email=suppressed_email,
|
||||
status=status,
|
||||
)
|
||||
|
||||
def _get(self, url=None):
|
||||
url = self.url if url is None else url
|
||||
return self.client.get(self.url)
|
||||
|
||||
def _post(self, url=None):
|
||||
url = self.url if url is None else url
|
||||
return self.client.post(self.url)
|
||||
|
||||
def _doc(self, content=None):
|
||||
if content is None:
|
||||
content = self._get().content
|
||||
return pq(content)
|
||||
|
||||
def _set_url_code(self, code):
|
||||
self.url += f'?code={code}'
|
||||
|
||||
def _assert_id_in_doc(self, doc, tag):
|
||||
assert doc(f'#{tag}').length == 1, f'#{tag} not in {doc}'
|
||||
|
||||
def _assert_text_in_doc(self, doc, text):
|
||||
assert text in doc.text(), f'"{text}" not in "{doc.text()}"'
|
||||
|
||||
def _assert_verify_button(self, doc, text):
|
||||
print('text', doc("button[type='submit']").text())
|
||||
assert text in doc("button[type='submit']").text()
|
||||
|
||||
def _assert_redirect_self(self, response, url=None):
|
||||
url = self.url if url is None else url
|
||||
self.assert3xx(response, url)
|
||||
|
||||
def test_hide_suppressed_email_snippet(self):
|
||||
"""
|
||||
on verification page, do not show the suppressed email snippet
|
||||
"""
|
||||
doc = self._doc()
|
||||
assert doc('#suppressed-email').length == 0
|
||||
|
||||
def test_email_verified(self):
|
||||
assert not self.user_profile.suppressed_email
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(doc, 'Your email address is verified.')
|
||||
self._assert_id_in_doc(doc, VERIFY_EMAIL_STATE['email_verified'])
|
||||
|
||||
def test_email_suppressed(self):
|
||||
"""
|
||||
current user has a suppressed email and no verification.
|
||||
"""
|
||||
self._create_suppressed_email(self.user_profile)
|
||||
assert not self.user_profile.email_verification
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(doc, 'Please verify your email')
|
||||
self._assert_verify_button(doc, 'Verify email')
|
||||
self._assert_id_in_doc(doc, VERIFY_EMAIL_STATE['email_suppressed'])
|
||||
|
||||
@mock.patch('olympia.devhub.views.send_suppressed_email_confirmation')
|
||||
def test_create_verification(self, send_suppressed_email_confirmation_mock):
|
||||
"""
|
||||
post request to create verification
|
||||
"""
|
||||
send_suppressed_email_confirmation_mock.delay.return_value = None
|
||||
self._create_suppressed_email(self.user_profile)
|
||||
assert not self.user_profile.email_verification
|
||||
|
||||
response = self._post()
|
||||
self._assert_redirect_self(response)
|
||||
|
||||
assert self.user_profile.reload().email_verification
|
||||
assert send_suppressed_email_confirmation_mock.delay.call_count == 1
|
||||
|
||||
@mock.patch('olympia.devhub.views.send_suppressed_email_confirmation')
|
||||
def test_create_verification_existing(
|
||||
self, send_suppressed_email_confirmation_mock
|
||||
):
|
||||
"""
|
||||
post request to create verification when one already exists
|
||||
will delete the existing one and create a new one
|
||||
"""
|
||||
send_suppressed_email_confirmation_mock.delay.return_value = None
|
||||
verification = self._create_suppressed_email_verification(self.user_profile)
|
||||
|
||||
assert self.user_profile.email_verification
|
||||
|
||||
response = self._post()
|
||||
self._assert_redirect_self(response)
|
||||
|
||||
assert self.user_profile.reload().email_verification
|
||||
|
||||
assert not SuppressedEmailVerification.objects.filter(
|
||||
pk=verification.pk
|
||||
).exists()
|
||||
|
||||
def test_create_verification_not_suppressed(self):
|
||||
"""
|
||||
post request to create verification when email is not suppressed
|
||||
"""
|
||||
assert not self.user_profile.suppressed_email
|
||||
assert not self.user_profile.email_verification
|
||||
|
||||
response = self._post()
|
||||
self._assert_redirect_self(response)
|
||||
|
||||
def test_verification_expired(self):
|
||||
"""
|
||||
user has a verification that is expired, regardless of status.
|
||||
"""
|
||||
verification = self._create_suppressed_email_verification(
|
||||
self.user_profile, None
|
||||
)
|
||||
|
||||
with freezegun.freeze_time(verification.created) as frozen_time:
|
||||
frozen_time.tick(timedelta(days=31))
|
||||
|
||||
assert verification.is_expired
|
||||
|
||||
doc = self._doc()
|
||||
|
||||
self._assert_text_in_doc(
|
||||
doc,
|
||||
(
|
||||
'Could not verify email address. '
|
||||
'The verification link has expired.'
|
||||
),
|
||||
)
|
||||
self._assert_verify_button(doc, 'Try again')
|
||||
self._assert_id_in_doc(doc, VERIFY_EMAIL_STATE['verification_expired'])
|
||||
|
||||
def test_verification_pending(self):
|
||||
"""
|
||||
current user has a verification in `Pending`. waiting for email to be sent
|
||||
"""
|
||||
self._create_suppressed_email_verification(self.user_profile)
|
||||
assert self.user_profile.email_verification
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(doc, 'Working... Please be patient.')
|
||||
assert doc('.loader').length == 1
|
||||
self._assert_id_in_doc(doc, VERIFY_EMAIL_STATE['verification_pending'])
|
||||
|
||||
def test_verification_timedout(self):
|
||||
"""
|
||||
current user has a verification in `Pending`.
|
||||
timeout exceeded so we show static message
|
||||
"""
|
||||
verification = self._create_suppressed_email_verification(self.user_profile)
|
||||
assert self.user_profile.email_verification
|
||||
|
||||
with freezegun.freeze_time(verification.created) as frozen_time:
|
||||
frozen_time.tick(timedelta(seconds=31))
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(doc, 'This is taking longer than expected.')
|
||||
self._assert_id_in_doc(doc, VERIFY_EMAIL_STATE['verification_timedout'])
|
||||
self._assert_verify_button(doc, 'Try again')
|
||||
|
||||
def test_verification_failed(self):
|
||||
"""
|
||||
current user has a verification in `Failed`.
|
||||
"""
|
||||
self._create_suppressed_email_verification(
|
||||
self.user_profile,
|
||||
None,
|
||||
SuppressedEmailVerification.STATUS_CHOICES.Failed,
|
||||
)
|
||||
assert self.user_profile.email_verification
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(doc, 'Failed to send confirmation email. ')
|
||||
self._assert_verify_button(doc, 'Try again')
|
||||
self._assert_id_in_doc(doc, VERIFY_EMAIL_STATE['verification_failed'])
|
||||
|
||||
def test_confirmation_pending(self):
|
||||
"""
|
||||
current user has a verification in `Delivered`.
|
||||
waiting for confirmation link to be clicked
|
||||
"""
|
||||
self._create_suppressed_email_verification(
|
||||
self.user_profile,
|
||||
None,
|
||||
SuppressedEmailVerification.STATUS_CHOICES.Delivered,
|
||||
)
|
||||
assert self.user_profile.email_verification
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(doc, 'An email with a confirmation link has been sent')
|
||||
self._assert_id_in_doc(doc, 'confirmation_pending')
|
||||
|
||||
def test_confirmation_link_invalid_code(self):
|
||||
self._create_suppressed_email_verification(
|
||||
self.user_profile,
|
||||
None,
|
||||
SuppressedEmailVerification.STATUS_CHOICES.Delivered,
|
||||
)
|
||||
self._set_url_code('invalid')
|
||||
|
||||
response = self._get()
|
||||
self._assert_redirect_self(response, reverse('devhub.email_verification'))
|
||||
|
||||
def test_confirmation_link_unauthorized_code(self):
|
||||
"""
|
||||
given code matches a verification that does not belong to the user.
|
||||
"""
|
||||
self._create_suppressed_email_verification(
|
||||
self.user_profile,
|
||||
None,
|
||||
SuppressedEmailVerification.STATUS_CHOICES.Delivered,
|
||||
)
|
||||
verification = self._create_suppressed_email_verification(
|
||||
user_factory(), None, SuppressedEmailVerification.STATUS_CHOICES.Delivered
|
||||
)
|
||||
|
||||
self._set_url_code(verification.confirmation_code)
|
||||
|
||||
doc = self._doc()
|
||||
self._assert_text_in_doc(
|
||||
doc, "The provided code is associated with another user's email"
|
||||
)
|
||||
self._assert_id_in_doc(doc, 'confirmation_unauthorized')
|
||||
self._assert_verify_button(doc, 'Try again')
|
||||
|
||||
def test_confirmation_link_valid_code(self):
|
||||
"""
|
||||
given code is valid and belongs to the user. remove the email suppression
|
||||
"""
|
||||
verification = self._create_suppressed_email_verification(
|
||||
self.user_profile,
|
||||
None,
|
||||
SuppressedEmailVerification.STATUS_CHOICES.Delivered,
|
||||
)
|
||||
self._set_url_code(verification.confirmation_code)
|
||||
|
||||
assert not verification.is_expired
|
||||
|
||||
response = self._get()
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
assert 'Your email was successfully verified.' in mail.outbox[0].body
|
||||
expected_redirect = reverse('devhub.email_verification')
|
||||
self.assert3xx(response, expected_redirect)
|
||||
|
|
|
@ -268,5 +268,8 @@ urlpatterns = decorate(
|
|||
),
|
||||
# logout page
|
||||
re_path(r'^logout', views.logout, name='devhub.logout'),
|
||||
re_path(
|
||||
r'^verify-email', views.email_verification, name='devhub.email_verification'
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -51,6 +51,7 @@ from olympia.amo.utils import (
|
|||
escape_all,
|
||||
is_safe_url,
|
||||
send_mail,
|
||||
send_mail_jinja,
|
||||
)
|
||||
from olympia.api.models import APIKey, APIKeyConfirmation
|
||||
from olympia.devhub.decorators import (
|
||||
|
@ -70,7 +71,11 @@ from olympia.reviewers.forms import PublicWhiteboardForm
|
|||
from olympia.reviewers.models import Whiteboard
|
||||
from olympia.reviewers.templatetags.code_manager import code_manager_url
|
||||
from olympia.reviewers.utils import ReviewHelper
|
||||
from olympia.users.models import DeveloperAgreementRestriction
|
||||
from olympia.users.models import (
|
||||
DeveloperAgreementRestriction,
|
||||
SuppressedEmailVerification,
|
||||
)
|
||||
from olympia.users.tasks import send_suppressed_email_confirmation
|
||||
from olympia.users.utils import (
|
||||
RestrictionChecker,
|
||||
send_addon_author_add_mail,
|
||||
|
@ -2074,3 +2079,103 @@ def logout(request):
|
|||
logout_user(request, response)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
VERIFY_EMAIL_STATE = {
|
||||
'email_verified': 'email_verified',
|
||||
'email_suppressed': 'email_suppressed',
|
||||
'verification_expired': 'verification_expired',
|
||||
'verification_pending': 'verification_pending',
|
||||
'verification_failed': 'verification_failed',
|
||||
'verification_timedout': 'verification_timedout',
|
||||
'confirmation_pending': 'confirmation_pending',
|
||||
'confirmation_unauthorized': 'confirmation_unauthorized',
|
||||
}
|
||||
|
||||
RENDER_BUTTON_STATES = [
|
||||
VERIFY_EMAIL_STATE['email_suppressed'],
|
||||
VERIFY_EMAIL_STATE['verification_expired'],
|
||||
VERIFY_EMAIL_STATE['verification_failed'],
|
||||
VERIFY_EMAIL_STATE['verification_timedout'],
|
||||
VERIFY_EMAIL_STATE['confirmation_unauthorized'],
|
||||
]
|
||||
|
||||
|
||||
def get_button_text(state):
|
||||
if state == VERIFY_EMAIL_STATE['email_suppressed']:
|
||||
return gettext('Verify email')
|
||||
|
||||
return gettext('Try again')
|
||||
|
||||
|
||||
@login_required
|
||||
def email_verification(request):
|
||||
data = {'state': None}
|
||||
email_verification = request.user.email_verification
|
||||
suppressed_email = request.user.suppressed_email
|
||||
|
||||
if request.method == 'POST':
|
||||
if email_verification:
|
||||
email_verification.delete()
|
||||
|
||||
if suppressed_email:
|
||||
email_verification = SuppressedEmailVerification.objects.create(
|
||||
suppressed_email=suppressed_email
|
||||
)
|
||||
send_suppressed_email_confirmation.delay(email_verification.id)
|
||||
|
||||
return redirect('devhub.email_verification')
|
||||
|
||||
if email_verification:
|
||||
if not email_verification.is_expired:
|
||||
if (
|
||||
email_verification.status
|
||||
== SuppressedEmailVerification.STATUS_CHOICES.Pending
|
||||
):
|
||||
if email_verification.is_timedout:
|
||||
data['state'] = VERIFY_EMAIL_STATE['verification_timedout']
|
||||
else:
|
||||
data['state'] = VERIFY_EMAIL_STATE['verification_pending']
|
||||
elif (
|
||||
email_verification.status
|
||||
== SuppressedEmailVerification.STATUS_CHOICES.Failed
|
||||
):
|
||||
data['state'] = VERIFY_EMAIL_STATE['verification_failed']
|
||||
elif (
|
||||
email_verification.status
|
||||
== SuppressedEmailVerification.STATUS_CHOICES.Delivered
|
||||
):
|
||||
if code := request.GET.get('code'):
|
||||
if code == email_verification.confirmation_code:
|
||||
suppressed_email.delete()
|
||||
send_mail_jinja(
|
||||
gettext('Your email has been verified'),
|
||||
'devhub/emails/verify-email-completed.ltxt',
|
||||
{},
|
||||
recipient_list=[request.user.email],
|
||||
)
|
||||
|
||||
if SuppressedEmailVerification.objects.filter(
|
||||
confirmation_code=code
|
||||
).exists():
|
||||
data['state'] = VERIFY_EMAIL_STATE['confirmation_unauthorized']
|
||||
else:
|
||||
return redirect('devhub.email_verification')
|
||||
else:
|
||||
data['state'] = VERIFY_EMAIL_STATE['confirmation_pending']
|
||||
else:
|
||||
data['state'] = VERIFY_EMAIL_STATE['verification_expired']
|
||||
|
||||
elif suppressed_email:
|
||||
data['state'] = VERIFY_EMAIL_STATE['email_suppressed']
|
||||
else:
|
||||
data['state'] = VERIFY_EMAIL_STATE['email_verified']
|
||||
|
||||
if data['state'] is None:
|
||||
raise Exception('Invalid view must result in assigned state')
|
||||
|
||||
if data['state'] in RENDER_BUTTON_STATES:
|
||||
data['render_button'] = True
|
||||
data['button_text'] = get_button_text(data['state'])
|
||||
|
||||
return TemplateResponse(request, 'devhub/verify_email.html', context=data)
|
||||
|
|
|
@ -688,6 +688,12 @@ class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser):
|
|||
def suppressed_email(self):
|
||||
return SuppressedEmail.objects.filter(email=self.email).first()
|
||||
|
||||
@property
|
||||
def email_verification(self):
|
||||
return SuppressedEmailVerification.objects.filter(
|
||||
suppressed_email=self.suppressed_email
|
||||
).first()
|
||||
|
||||
|
||||
class UserNotification(ModelBase):
|
||||
user = models.ForeignKey(
|
||||
|
@ -1373,3 +1379,11 @@ class SuppressedEmailVerification(ModelBase):
|
|||
@property
|
||||
def expiration(self):
|
||||
return self.created + timedelta(days=30)
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.expiration < datetime.now()
|
||||
|
||||
@property
|
||||
def is_timedout(self):
|
||||
return self.created + timedelta(seconds=30) < datetime.now()
|
||||
|
|
|
@ -5,6 +5,7 @@ import tempfile
|
|||
import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext
|
||||
|
||||
import requests
|
||||
|
@ -179,8 +180,9 @@ def send_suppressed_email_confirmation(suppressed_email_verification_id):
|
|||
verification.status = SuppressedEmailVerification.STATUS_CHOICES.Pending
|
||||
|
||||
confirmation_link = (
|
||||
# TODO: replace with email-verification reverse path
|
||||
'' + '?code=' + str(verification.confirmation_code)
|
||||
reverse('devhub.email_verification')
|
||||
+ '?code='
|
||||
+ str(verification.confirmation_code)
|
||||
)
|
||||
|
||||
send_mail_jinja(
|
||||
|
@ -232,7 +234,7 @@ def check_suppressed_email_confirmation(suppressed_email_verification_id, page_s
|
|||
day=before.day,
|
||||
)
|
||||
end_date = datetime.datetime.now() + datetime.timedelta(days=1)
|
||||
date_format = '%Y-%m-%dT%H:%M:%S%z+0100'
|
||||
date_format = '%Y-%m-%d'
|
||||
|
||||
params = {
|
||||
'toEmailAddress': email,
|
||||
|
|
|
@ -920,6 +920,16 @@ class TestUserProfile(TestCase):
|
|||
|
||||
assert user.reload().suppressed_email == suppressed_email
|
||||
|
||||
def test_email_verification(self):
|
||||
user = user_factory()
|
||||
assert not user.email_verification
|
||||
|
||||
verification = SuppressedEmailVerification.objects.create(
|
||||
suppressed_email=SuppressedEmail.objects.create(email=user.email)
|
||||
)
|
||||
|
||||
assert user.reload().email_verification.id == verification.id
|
||||
|
||||
|
||||
class TestDeniedName(TestCase):
|
||||
fixtures = ['users/test_backends']
|
||||
|
@ -1560,3 +1570,21 @@ class TestSuppressedEmailVerification(TestCase):
|
|||
SuppressedEmailVerification.objects.create(
|
||||
suppressed_email=self.suppressed_email, status='invalid'
|
||||
)
|
||||
|
||||
def test_is_expired(self):
|
||||
email_verification = SuppressedEmailVerification.objects.create(
|
||||
suppressed_email=self.suppressed_email
|
||||
)
|
||||
assert not email_verification.is_expired
|
||||
|
||||
with freeze_time(email_verification.created + timedelta(days=31)):
|
||||
assert email_verification.is_expired
|
||||
|
||||
def test_is_timedout(self):
|
||||
email_verification = SuppressedEmailVerification.objects.create(
|
||||
suppressed_email=self.suppressed_email
|
||||
)
|
||||
assert not email_verification.is_timedout
|
||||
|
||||
with freeze_time(email_verification.created + timedelta(seconds=31)):
|
||||
assert email_verification.is_timedout
|
||||
|
|
|
@ -9,6 +9,7 @@ from urllib.parse import parse_qs, urlparse
|
|||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
|
@ -392,8 +393,9 @@ class TestSendSuppressedEmailConfirmation(TestCase):
|
|||
assert len(mail.outbox) == 1
|
||||
|
||||
expected_confirmation_link = (
|
||||
# TODO: replace with reverse devhub.email_verification
|
||||
'' + '?code=' + str(verification.confirmation_code)
|
||||
reverse('devhub.email_verification')
|
||||
+ '?code='
|
||||
+ str(verification.confirmation_code)
|
||||
)
|
||||
assert expected_confirmation_link in mail.outbox[0].body
|
||||
assert str(verification.confirmation_code)[-5:] in mail.outbox[0].subject
|
||||
|
@ -536,8 +538,8 @@ class TestCheckSuppressedEmailConfirmation(TestCase):
|
|||
parsed_url = urlparse(responses.calls[0].request.url)
|
||||
search_params = parse_qs(parsed_url.query)
|
||||
|
||||
assert search_params['startDate'][0] == '2023-06-25T00:00:00+0100'
|
||||
assert search_params['endDate'][0] == '2023-06-27T11:00:00+0100'
|
||||
assert search_params['startDate'][0] == '2023-06-25'
|
||||
assert search_params['endDate'][0] == '2023-06-27'
|
||||
|
||||
def test_pagination(self):
|
||||
verification = SuppressedEmailVerification.objects.create(
|
||||
|
|
|
@ -241,3 +241,17 @@ h2.submission-count {
|
|||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 5px solid #f3f3f3; /* Light grey */
|
||||
border-top: 5px solid #0C99D5; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 5vh;
|
||||
height: 5vh;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
|
@ -5,6 +5,13 @@ $(document).ready(function () {
|
|||
// Edit Add-on
|
||||
$('#edit-addon').exists(initEditAddon);
|
||||
|
||||
// Poll for suppressed email removal updates.
|
||||
$('#verification_pending').exists(function () {
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
//Ownership
|
||||
$('#authors_confirmed').exists(function () {
|
||||
initAuthorFields();
|
||||
|
|
Загрузка…
Ссылка в новой задаче