Simplify socketlabs check for status and messaging when something goes wrong. (#22005)

* Simplify socketlabs check for status and messaging when something goes wrong.

* Fix comments

* Add more details and formatting to suppressed email status table.

* Copy fixes and updates

* Update src/olympia/devhub/templates/devhub/verify_email.html

Co-authored-by: Francesco Lodolo <flodolo@mozilla.com>

---------

Co-authored-by: Mozilla Add-ons Robot <addons-dev-automation+github@mozilla.com>
Co-authored-by: Francesco Lodolo <flodolo@mozilla.com>
This commit is contained in:
Kevin Meinhardt 2024-03-19 17:30:54 +01:00 коммит произвёл GitHub
Родитель b9379559f3
Коммит 059193a3d3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 586 добавлений и 533 удалений

Просмотреть файл

@ -7,6 +7,7 @@
{% block content %}
<h1>{{ title }}</h1>
<div id="{{ state }}">
<div class="verify-email-text">
{% if state == "email_verified" %}
{% trans %}
Your email address is verified.
@ -20,24 +21,13 @@
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 %}
{% trans %}
We are sending an email to you, this might take a minute. The page will automatically refresh.
{% endtrans %}
{% elif state == "verification_timedout" %}
{% trans %}
This is taking longer than expected. Try again.
It is taking longer than expected to confirm delivery of your verification email. Please try again.
{% endtrans %}
{% elif state == "confirmation_pending" %}
{% trans email=request.user.email %}
@ -45,12 +35,54 @@
{% endtrans %}
{% elif state == "confirmation_invalid" %}
{% trans email=request.user.email %}
The provided code is invalid. Please use the link in the email sent to your email address {{ email }}.
- could be unauthorized
- could be expired
- could be a mistake
The provided code is invalid, unauthorized, expired or incomplete. Please use the link in the email sent to your email address {{ email }}. If the code is still not working, please request a new email.
{% endtrans %}
{% endif %}
</div>
{% if found_emails|length > 0 %}
<div class="verify-email-table">
<p>
{% trans %}
We have attempted to send your verification email.
Below are the confirmation records we found and their associated delivery statuses.
{% endtrans %}
</p>
<table border=1 frame=void rules=rows>
<thead>
<tr>
<th>{{ _('Date') }}</th>
<th>{{ _('From') }}</th>
<th>{{ _('To') }}</th>
<th>{{ _('Subject') }}</th>
<th>{{ _('Status') }}</th>
</tr>
</thead>
<tbody>
{% for email in found_emails %}
<tr>
<td>
{{ email.statusDate }}
</td>
<td>
{{ email.from }}
</td>
<td>
{{ email.to }}
</td>
<td>
{{ email.subject }}
</td>
<td>
{{ email.status }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if render_button %}
{% with submit_text=button_text %}
{% include 'devhub/verify_email_form.html' %}

Просмотреть файл

@ -1,6 +1,6 @@
<form action="{{ url('devhub.email_verification') }}" method="post">
{% csrf_token %}
<button class="Button" type="submit">
<button class="Button verify-email-button" type="submit">
{{ submit_text | default('Verify email address') }}
</button>
</form>

Просмотреть файл

@ -2255,7 +2255,9 @@ class TestVerifyEmail(TestCase):
doc = pq(response.content)
assert doc('#suppressed-email').length == 0
def test_get_confirmation_complete(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_confirmation_complete(self, mock_check_emails):
mock_check_emails.return_value = []
self.with_email_verification()
code = self.email_verification.confirmation_code
url = f'{self.url}?code={code}'
@ -2268,7 +2270,9 @@ class TestVerifyEmail(TestCase):
assert 'Your email was successfully verified.' in mail.outbox[0].body
self.assert3xx(response, reverse('devhub.email_verification'))
def test_get_confirmation_complete_with_timeout(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_confirmation_complete_with_timeout(self, mock_check_emails):
mock_check_emails.return_value = []
self.with_email_verification()
code = self.email_verification.confirmation_code
url = f'{self.url}?code={code}'
@ -2296,8 +2300,11 @@ class TestVerifyEmail(TestCase):
doc = pq(response.content)
assert 'Please verify your email' in doc.text()
assert 'Verify email' in doc.text()
def test_get_verification_expired(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_verification_expired(self, mock_check_emails):
mock_check_emails.return_value = []
self.with_email_verification()
with freezegun.freeze_time(self.email_verification.created) as frozen_time:
@ -2307,26 +2314,37 @@ class TestVerifyEmail(TestCase):
doc = pq(response.content)
assert 'Could not verify email address.' in doc.text()
assert 'Send another email' in doc.text()
def test_get_verification_pending(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_verification_pending_without_emails(self, mock_check_emails):
mock_check_emails.return_value = []
self.with_email_verification()
response = self.client.get(self.url)
doc = pq(response.content)
assert 'Working... Please be patient.' in doc.text()
assert 'We are sending an email to you' in doc.text()
assert 'Send another email' in doc.text()
def test_get_verification_failed(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_verification_pending_with_emails(self, mock_check_emails):
mock_check_emails.return_value = [
{'status': 'Delivered', 'subject': 'subject', 'from': 'from', 'to': 'to'}
]
self.with_email_verification()
self.email_verification.status = (
SuppressedEmailVerification.STATUS_CHOICES.Failed
)
self.email_verification.save()
response = self.client.get(self.url)
doc = pq(response.content)
assert 'Failed to send confirmation email.' in doc.text()
assert 'We have attempted to send your verification' in doc.text()
assert 'Delivered' in doc.text()
assert 'subject' in doc.text()
assert 'from' in doc.text()
assert 'to' in doc.text()
assert 'Send another email' in doc.text()
def test_get_verification_timedout(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_verification_timedout(self, mock_check_emails):
mock_check_emails.return_value = []
self.with_email_verification()
with freezegun.freeze_time(self.email_verification.created) as frozen_time:
@ -2337,9 +2355,12 @@ class TestVerifyEmail(TestCase):
response = self.client.get(self.url)
doc = pq(response.content)
assert 'This is taking longer than expected.' in doc.text()
assert 'It is taking longer than expected' in doc.text()
assert 'Send another email' in doc.text()
def test_get_verification_delivered(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_verification_delivered(self, mock_check_suppressed):
mock_check_suppressed.return_value = []
self.with_email_verification()
self.email_verification.status = (
SuppressedEmailVerification.STATUS_CHOICES.Delivered
@ -2350,11 +2371,17 @@ class TestVerifyEmail(TestCase):
assert 'An email with a confirmation link has been sent' in doc.text()
def test_get_confirmation_invalid(self):
@mock.patch('olympia.devhub.views.check_suppressed_email_confirmation')
def test_get_confirmation_invalid(self, mock_check_emails):
mock_check_emails.return_value = []
self.with_email_verification()
code = 'invalid'
url = f'{self.url}?code={code}'
response = self.client.get(url)
doc = pq(response.content)
assert 'The provided code is invalid.' in doc.text()
assert (
'The provided code is invalid, unauthorized, expired or incomplete.'
in doc.text()
)
assert 'Send another email' in doc.text()

Просмотреть файл

@ -78,6 +78,7 @@ from olympia.users.models import (
from olympia.users.tasks import send_suppressed_email_confirmation
from olympia.users.utils import (
RestrictionChecker,
check_suppressed_email_confirmation,
send_addon_author_add_mail,
send_addon_author_change_mail,
send_addon_author_remove_mail,
@ -2086,7 +2087,6 @@ VERIFY_EMAIL_STATE = {
'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_invalid': 'confirmation_invalid',
@ -2095,7 +2095,7 @@ VERIFY_EMAIL_STATE = {
RENDER_BUTTON_STATES = [
VERIFY_EMAIL_STATE['email_suppressed'],
VERIFY_EMAIL_STATE['verification_expired'],
VERIFY_EMAIL_STATE['verification_failed'],
VERIFY_EMAIL_STATE['verification_pending'],
VERIFY_EMAIL_STATE['verification_timedout'],
VERIFY_EMAIL_STATE['confirmation_invalid'],
]
@ -2105,7 +2105,7 @@ def get_button_text(state):
if state == VERIFY_EMAIL_STATE['email_suppressed']:
return gettext('Verify email')
return gettext('Try again')
return gettext('Send another email')
@login_required
@ -2130,6 +2130,7 @@ def email_verification(request):
return redirect('devhub.email_verification')
if email_verification:
data['found_emails'] = check_suppressed_email_confirmation(email_verification)
if email_verification.is_expired:
data['state'] = VERIFY_EMAIL_STATE['verification_expired']
elif code := request.GET.get('code'):
@ -2144,24 +2145,16 @@ def email_verification(request):
return redirect('devhub.email_verification')
else:
data['state'] = VERIFY_EMAIL_STATE['confirmation_invalid']
elif (
email_verification.status
== SuppressedEmailVerification.STATUS_CHOICES.Pending
):
if email_verification.is_timedout:
data['state'] = VERIFY_EMAIL_STATE['verification_timedout']
elif email_verification.is_timedout:
data['state'] = VERIFY_EMAIL_STATE['verification_timedout']
else:
if (
email_verification.reload().status
== SuppressedEmailVerification.STATUS_CHOICES.Delivered
):
data['state'] = VERIFY_EMAIL_STATE['confirmation_pending']
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
):
data['state'] = VERIFY_EMAIL_STATE['confirmation_pending']
elif suppressed_email:
data['state'] = VERIFY_EMAIL_STATE['email_suppressed']

Просмотреть файл

@ -924,7 +924,6 @@ CELERY_TASK_ROUTES = {
'olympia.reviewers.tasks.recalculate_post_review_weight': {'queue': 'cron'},
'olympia.users.tasks.sync_suppressed_emails_task': {'queue': 'cron'},
'olympia.users.tasks.send_suppressed_email_confirmation': {'queue': 'devhub'},
'olympia.users.tasks.check_suppressed_email_confirmation': {'queue': 'devhub'},
# Reviewers.
'olympia.lib.crypto.tasks.sign_addons': {'queue': 'reviewers'},
# Admin.

Просмотреть файл

@ -1386,3 +1386,7 @@ class SuppressedEmailVerification(ModelBase):
@property
def is_timedout(self):
return self.created + timedelta(seconds=30) < datetime.now()
def mark_as_delivered(self):
self.update(status=SuppressedEmailVerification.STATUS_CHOICES.Delivered)
self.save()

Просмотреть файл

@ -1,5 +1,4 @@
import csv
import datetime
import itertools
import tempfile
import urllib.parse
@ -9,7 +8,6 @@ from django.urls import reverse
from django.utils.translation import gettext
import requests
from celery.exceptions import Retry
from requests.exceptions import HTTPError, Timeout
import olympia.core.logger
@ -30,6 +28,7 @@ from .models import (
SuppressedEmailVerification,
UserProfile,
)
from .utils import assert_socket_labs_settings_defined
task_log = olympia.core.logger.getLogger('z.task')
@ -91,17 +90,6 @@ def update_user_ratings_task(data, **kw):
BATCH_SIZE = 100
def assert_socket_labs_settings_defined():
if not settings.SOCKET_LABS_TOKEN:
raise Exception('SOCKET_LABS_TOKEN is not defined')
if not settings.SOCKET_LABS_HOST:
raise Exception('SOCKET_LABS_HOST is not defined')
if not settings.SOCKET_LABS_SERVER_ID:
raise Exception('SOCKET_LABS_SERVER_ID is not defined')
@task(autoretry_for=(HTTPError, Timeout), max_retries=5, retry_backoff=True)
def sync_suppressed_emails_task(batch_size=BATCH_SIZE, **kw):
assert_socket_labs_settings_defined()
@ -186,6 +174,8 @@ def send_suppressed_email_confirmation(suppressed_email_verification_id):
else:
response.raise_for_status()
task_log.info('email removed from suppression')
code_snippet = str(verification.confirmation_code)[-5:]
verification.status = SuppressedEmailVerification.STATUS_CHOICES.Pending
@ -206,113 +196,3 @@ def send_suppressed_email_confirmation(suppressed_email_verification_id):
)
verification.save()
check_suppressed_email_confirmation.delay(verification.id)
@task(
autoretry_for=(
HTTPError,
Timeout,
),
max_retries=5,
retry_backoff=True,
)
def check_suppressed_email_confirmation(suppressed_email_verification_id, page_size=5):
assert_socket_labs_settings_defined()
verification = SuppressedEmailVerification.objects.filter(
id=suppressed_email_verification_id
).first()
if not verification:
raise Exception(f'invalid id: {suppressed_email_verification_id}')
email = verification.suppressed_email.email
current_count = 0
total = 0
code_snippet = str(verification.confirmation_code)[-5:]
path = f'servers/{settings.SOCKET_LABS_SERVER_ID}/reports/recipient-search/'
# socketlabs might set the queued time any time of day
# so we need to check to midnight, one day before the verification was created
# and to midnight of tomorrow
before = verification.created - datetime.timedelta(days=1)
start_date = datetime.datetime(
year=before.year,
month=before.month,
day=before.day,
)
end_date = datetime.datetime.now() + datetime.timedelta(days=1)
date_format = '%Y-%m-%d'
params = {
'toEmailAddress': email,
'startDate': start_date.strftime(date_format),
'endDate': end_date.strftime(date_format),
'pageNumber': 0,
'pageSize': page_size,
'sortField': 'queuedTime',
'sortDirection': 'dsc',
}
is_first_page = True
while current_count < total or is_first_page:
if not is_first_page:
params['pageNumber'] = params['pageNumber'] + 1
url = (
urllib.parse.urljoin(settings.SOCKET_LABS_HOST, path)
+ '?'
+ urllib.parse.urlencode(params)
)
headers = {
'authorization': f'Bearer {settings.SOCKET_LABS_TOKEN}',
}
task_log.info(f'checking for {code_snippet} with params {params}')
response = requests.get(url, headers=headers)
response.raise_for_status()
json = response.json()
if is_first_page:
total = json['total']
if total == 0:
raise Retry(
f'No emails found for email {email}.'
'retrying as email could not be queued yet'
)
is_first_page = False
data = json['data']
current_count += len(data)
## TODO: check if we can set `customMessageId` to replace code snippet
for item in data:
if code_snippet in item['subject']:
options = dict(SuppressedEmailVerification.STATUS_CHOICES).values()
new_status = item['status']
if new_status not in options:
raise Exception(
f'invalid status: {new_status} '
f'for {suppressed_email_verification_id}. '
f'expected {", ".join(options)}'
)
task_log.info(f'Found matching email {item}')
verification.update(
status=SuppressedEmailVerification.STATUS_CHOICES[item['status']]
)
return
raise Retry(
f'failed to find email for code: {code_snippet} in {total} emails.'
'retrying as email could not be queued yet'
)

Просмотреть файл

@ -1,11 +1,9 @@
import csv
import io
import json
import shutil
import tempfile
import uuid
from unittest import mock
from urllib.parse import parse_qs, urlparse
from django.conf import settings
from django.core import mail
@ -14,7 +12,6 @@ from django.urls import reverse
import pytest
import responses
from celery.exceptions import Retry
from freezegun import freeze_time
from PIL import Image
from requests.exceptions import Timeout
@ -28,7 +25,6 @@ from olympia.users.models import (
SuppressedEmailVerification,
)
from olympia.users.tasks import (
check_suppressed_email_confirmation,
delete_photo,
resize_photo,
send_suppressed_email_confirmation,
@ -326,8 +322,7 @@ class TestSendSuppressedEmailConfirmation(TestCase):
with pytest.raises(Exception, match=f'invalid id: {invalid_id}'):
send_suppressed_email_confirmation.apply([invalid_id])
@mock.patch('olympia.users.tasks.check_suppressed_email_confirmation')
def test_socket_labs_returns_404(self, mock_check_suppressed_email_confirmation):
def test_socket_labs_returns_404(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
@ -343,15 +338,11 @@ class TestSendSuppressedEmailConfirmation(TestCase):
status=404,
)
mock_check_suppressed_email_confirmation.delay.return_value = None
try:
send_suppressed_email_confirmation.apply([verification.id])
except Exception as err:
pytest.fail('Unexpected exception: {0}'.format(err))
assert mock_check_suppressed_email_confirmation.delay.call_count == 1
def test_socket_labs_returns_5xx(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
@ -371,8 +362,7 @@ class TestSendSuppressedEmailConfirmation(TestCase):
with pytest.raises(Retry):
send_suppressed_email_confirmation.apply([verification.id])
@mock.patch('olympia.users.tasks.check_suppressed_email_confirmation')
def test_email_sent(self, mock_check_suppressed_email_confirmation):
def test_email_sent(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
@ -387,8 +377,6 @@ class TestSendSuppressedEmailConfirmation(TestCase):
status=201,
)
mock_check_suppressed_email_confirmation.delay.return_value = None
send_suppressed_email_confirmation.apply([verification.id])
assert len(mail.outbox) == 1
@ -400,12 +388,9 @@ class TestSendSuppressedEmailConfirmation(TestCase):
)
assert expected_confirmation_link in mail.outbox[0].body
assert str(verification.confirmation_code)[-5:] in mail.outbox[0].subject
assert mock_check_suppressed_email_confirmation.delay.call_count == 1
@mock.patch('olympia.users.tasks.check_suppressed_email_confirmation')
def test_retry_existing_verification(
self,
mock_check_suppressed_email_confirmation,
):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
@ -423,345 +408,9 @@ class TestSendSuppressedEmailConfirmation(TestCase):
status=201,
)
mock_check_suppressed_email_confirmation.delay.return_value = None
assert verification.status == SuppressedEmailVerification.STATUS_CHOICES.Failed
send_suppressed_email_confirmation.apply([verification.id])
assert (
verification.reload().status
== SuppressedEmailVerification.STATUS_CHOICES.Pending
)
class TestCheckSuppressedEmailConfirmation(TestCase):
def setUp(self):
self.user_profile = user_factory()
def test_fails_missing_settings(self):
for setting in (
'SOCKET_LABS_TOKEN',
'SOCKET_LABS_HOST',
'SOCKET_LABS_SERVER_ID',
):
with pytest.raises(Exception) as exc:
setattr(settings, setting, None)
check_suppressed_email_confirmation.apply(1)
assert exc.match('SOCKET_LABS_TOKEN is not defined')
def test_no_verification_for_id(self):
invalid_id = 1
assert SuppressedEmailVerification.objects.all().count() == 0
with pytest.raises(
Exception,
match=f'invalid id: {invalid_id}',
):
check_suppressed_email_confirmation.apply([invalid_id])
def test_socket_labs_returns_5xx(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=500,
)
with pytest.raises(Retry):
check_suppressed_email_confirmation.apply([verification.id])
def test_socket_labs_returns_empty(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=200,
body=json.dumps(
{
'data': [],
'total': 0,
}
),
content_type='application/json',
)
with pytest.raises(Retry) as error_info:
check_suppressed_email_confirmation.apply([verification.id])
assert f'No emails found for email {self.user_profile.email}' in str(
error_info.value
)
def test_auth_header_present(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=200,
body=json.dumps(
{
'data': [],
'total': 0,
}
),
content_type='application/json',
)
with pytest.raises(Retry):
check_suppressed_email_confirmation.apply([verification.id])
assert (
settings.SOCKET_LABS_TOKEN
in responses.calls[0].request.headers['authorization']
)
@freeze_time('2023-06-26 11:00')
def test_format_date_params(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=200,
body=json.dumps(
{
'data': [],
'total': 0,
}
),
content_type='application/json',
)
with pytest.raises(Retry):
check_suppressed_email_confirmation.apply([verification.id])
parsed_url = urlparse(responses.calls[0].request.url)
search_params = parse_qs(parsed_url.query)
assert search_params['startDate'][0] == '2023-06-25'
assert search_params['endDate'][0] == '2023-06-27'
def test_pagination(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
response_size = 5
body = [{'subject': 'test'} for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size + 1,
}
),
content_type='application/json',
)
code_snippet = str(verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [
{
'subject': f'test {code_snippet}',
'status': 'Delivered',
}
],
'total': response_size + 1,
}
),
content_type='application/json',
)
check_suppressed_email_confirmation.apply([verification.id, response_size])
assert len(responses.calls) == 2
def test_found_email(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
response_size = 5
body = [{'subject': 'test'} for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size + 1,
}
),
content_type='application/json',
)
code_snippet = str(verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [
{
'subject': f'test {code_snippet}',
'status': 'Delivered',
}
],
'total': response_size + 1,
}
),
content_type='application/json',
)
check_suppressed_email_confirmation.apply([verification.id, response_size])
assert (
verification.reload().status
== SuppressedEmailVerification.STATUS_CHOICES.Delivered
)
def test_invalid_status(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
response_size = 5
body = [{'subject': 'test'} for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size + 1,
}
),
content_type='application/json',
)
code_snippet = str(verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [
{
'subject': f'test {code_snippet}',
'status': 'InvalidStsatus',
}
],
'total': response_size + 1,
}
),
content_type='application/json',
)
with pytest.raises(Exception) as exc:
check_suppressed_email_confirmation.apply([verification.id, response_size])
exc_msg = str(exc.value)
assert f'invalid status: InvalidStsatus for {verification.id}' in exc_msg
for status in dict(SuppressedEmailVerification.STATUS_CHOICES).values():
assert status in exc_msg
def test_rsponse_does_not_contain_suppressed_email(self):
verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
),
)
response_size = 5
body = [{'subject': 'test'} for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size,
}
),
content_type='application/json',
)
with pytest.raises(Retry) as error_info:
check_suppressed_email_confirmation.apply([verification.id, response_size])
assert 'failed to find email for code: ' in str(error_info.value)

Просмотреть файл

@ -1,11 +1,16 @@
import json
from contextlib import ExitStack
from ipaddress import IPv4Address
from unittest import mock
from urllib.parse import parse_qs, urlparse
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test.client import RequestFactory
import pytest
import responses
from freezegun import freeze_time
from olympia import amo, core
from olympia.activity.models import ActivityLog
@ -16,9 +21,15 @@ from ..models import (
RESTRICTION_TYPES,
EmailUserRestriction,
IPNetworkUserRestriction,
SuppressedEmail,
SuppressedEmailVerification,
UserRestrictionHistory,
)
from ..utils import RestrictionChecker, UnsubscribeCode
from ..utils import (
RestrictionChecker,
UnsubscribeCode,
check_suppressed_email_confirmation,
)
def test_email_unsubscribe_code_parse():
@ -298,3 +309,326 @@ class TestRestrictionChecker(TestCase):
assert restriction_mock.call_count == 0
for restriction_mock in allow_auto_approval_mocks:
assert restriction_mock.call_count == 1
class TestCheckSuppressedEmailConfirmation(TestCase):
def setUp(self):
self.verification = None
self.user_profile = user_factory()
def with_verification(self):
self.verification = SuppressedEmailVerification.objects.create(
suppressed_email=SuppressedEmail.objects.create(
email=self.user_profile.email
)
)
def fake_email_response(self, code='', status='Suppressed'):
return {
'subject': f'test {code}',
'status': status,
'from': 'from',
'to': 'to',
'statusDate': '2023-06-26T11:00:00Z',
}
def test_fails_missing_settings(self):
self.with_verification()
for setting in (
'SOCKET_LABS_TOKEN',
'SOCKET_LABS_HOST',
'SOCKET_LABS_SERVER_ID',
):
with pytest.raises(Exception) as exc:
setattr(settings, setting, None)
check_suppressed_email_confirmation(self.verification)
assert exc.match(f'{setting} is not defined')
def test_no_verification(self):
assert not self.user_profile.suppressed_email
with pytest.raises(AssertionError):
check_suppressed_email_confirmation(self.verification)
def test_socket_labs_returns_5xx(self):
self.with_verification()
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=500,
)
with pytest.raises(Exception): # noqa: B017
check_suppressed_email_confirmation(self.verification)
def test_socket_labs_returns_empty(self):
self.with_verification()
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=200,
body=json.dumps(
{
'data': [],
'total': 0,
}
),
content_type='application/json',
)
assert len(check_suppressed_email_confirmation(self.verification)) == 0
def test_auth_header_present(self):
self.with_verification()
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=200,
body=json.dumps(
{
'data': [],
'total': 0,
}
),
content_type='application/json',
)
check_suppressed_email_confirmation(self.verification)
assert (
settings.SOCKET_LABS_TOKEN
in responses.calls[0].request.headers['authorization']
)
@freeze_time('2023-06-26 11:00')
def test_format_date_params(self):
self.with_verification()
responses.add(
responses.GET,
(
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
),
status=200,
body=json.dumps(
{
'data': [],
'total': 0,
}
),
content_type='application/json',
)
check_suppressed_email_confirmation(self.verification)
parsed_url = urlparse(responses.calls[0].request.url)
search_params = parse_qs(parsed_url.query)
assert search_params['startDate'][0] == '2023-06-25'
assert search_params['endDate'][0] == '2023-06-27'
def test_pagination(self):
self.with_verification()
response_size = 5
body = [self.fake_email_response() for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size + 1,
}
),
content_type='application/json',
)
code_snippet = str(self.verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [self.fake_email_response(code_snippet)],
'total': response_size + 1,
}
),
content_type='application/json',
)
check_suppressed_email_confirmation(self.verification, response_size)
assert len(responses.calls) == 2
def test_found_email(self):
self.with_verification()
response_size = 5
body = [self.fake_email_response() for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size + 1,
}
),
content_type='application/json',
)
code_snippet = str(self.verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [self.fake_email_response(code_snippet, 'Delivered')],
'total': response_size + 1,
}
),
content_type='application/json',
)
check_suppressed_email_confirmation(self.verification, response_size)
assert (
self.verification.reload().status
== SuppressedEmailVerification.STATUS_CHOICES.Delivered
)
def test_verify_response_data(self):
self.with_verification()
response_size = 1
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
code_snippet = str(self.verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [self.fake_email_response(code_snippet, 'Delivered')],
'total': response_size,
}
),
content_type='application/json',
)
result = check_suppressed_email_confirmation(self.verification, response_size)
assert len(result) == 1
assert result[0]['subject'] == f'test {code_snippet}'
assert result[0]['status'] == 'Delivered'
assert result[0]['from'] == 'from'
assert result[0]['to'] == 'to'
assert result[0]['statusDate'] == '2023-06-26T11:00:00Z'
def test_not_delivered_status(self):
self.with_verification()
response_size = 5
body = [self.fake_email_response() for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size + 1,
}
),
content_type='application/json',
)
code_snippet = str(self.verification.confirmation_code)[-5:]
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': [self.fake_email_response(code_snippet, 'InvalidStatus')],
'total': response_size + 1,
}
),
content_type='application/json',
)
result = check_suppressed_email_confirmation(self.verification, response_size)
assert len(result) == 1
assert result[0]['status'] == 'InvalidStatus'
def test_rsponse_does_not_contain_suppressed_email(self):
self.with_verification()
response_size = 5
body = [self.fake_email_response() for _ in range(response_size)]
url = (
f'{settings.SOCKET_LABS_HOST}servers/{settings.SOCKET_LABS_SERVER_ID}/'
f'reports/recipient-search/'
)
responses.add(
responses.GET,
url,
status=200,
body=json.dumps(
{
'data': body,
'total': response_size,
}
),
content_type='application/json',
)
result = check_suppressed_email_confirmation(self.verification, response_size)
assert len(result) == 0

Просмотреть файл

@ -1,6 +1,8 @@
import base64
import datetime
import hashlib
import hmac
import urllib
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@ -9,6 +11,7 @@ from django.urls import reverse
from django.utils.encoding import force_bytes, force_str
from django.utils.translation import gettext
import requests
from django_statsd.clients import statsd
import olympia.core.logger
@ -285,3 +288,112 @@ def upload_picture(user, picture):
user.picture_path,
set_modified_on=user.serializable_reference(),
)
def assert_socket_labs_settings_defined():
if not settings.SOCKET_LABS_TOKEN:
raise Exception('SOCKET_LABS_TOKEN is not defined')
if not settings.SOCKET_LABS_HOST:
raise Exception('SOCKET_LABS_HOST is not defined')
if not settings.SOCKET_LABS_SERVER_ID:
raise Exception('SOCKET_LABS_SERVER_ID is not defined')
utils_log = olympia.core.logger.getLogger('z.users')
def check_suppressed_email_confirmation(verification, page_size=5):
from olympia.users.models import SuppressedEmailVerification
assert_socket_labs_settings_defined()
assert isinstance(verification, SuppressedEmailVerification)
email = verification.suppressed_email.email
current_count = 0
total = 0
code_snippet = str(verification.confirmation_code)[-5:]
path = f'servers/{settings.SOCKET_LABS_SERVER_ID}/reports/recipient-search/'
# socketlabs might set the queued time any time of day
# so we need to check to midnight, one day before the verification was created
# and to midnight of tomorrow
before = verification.created - datetime.timedelta(days=1)
start_date = datetime.datetime(
year=before.year,
month=before.month,
day=before.day,
)
end_date = datetime.datetime.now() + datetime.timedelta(days=1)
date_format = '%Y-%m-%d'
params = {
'toEmailAddress': email,
'startDate': start_date.strftime(date_format),
'endDate': end_date.strftime(date_format),
'pageNumber': 0,
'pageSize': page_size,
'sortField': 'queuedTime',
'sortDirection': 'dsc',
}
is_first_page = True
found_emails = []
while current_count < total or is_first_page:
if not is_first_page:
params['pageNumber'] = params['pageNumber'] + 1
url = (
urllib.parse.urljoin(settings.SOCKET_LABS_HOST, path)
+ '?'
+ urllib.parse.urlencode(params)
)
headers = {
'authorization': f'Bearer {settings.SOCKET_LABS_TOKEN}',
}
utils_log.info(f'checking for {code_snippet} with params {params}')
response = requests.get(url, headers=headers)
response.raise_for_status()
json_data = response.json()
utils_log.info(f'recieved data {json_data} for {code_snippet}')
if is_first_page:
total = json_data['total']
if total == 0:
return found_emails
is_first_page = False
data = json_data['data']
current_count += len(data)
utils_log.info(f'found emails {data} for {code_snippet}')
## TODO: check if we can set `customMessageId` to replace code snippet
for item in data:
if code_snippet in item['subject']:
found_emails.append(
{
'from': item['from'],
'to': item['to'],
'subject': item['subject'],
'status': item['status'],
'statusDate': item['statusDate'],
}
)
if item['status'] == 'Delivered':
verification.mark_as_delivered()
return found_emails

Просмотреть файл

@ -1507,6 +1507,29 @@ form.select-review .errorlist {
margin-bottom: 10px;
}
.verify-email-button {
margin-top: 10px;
}
.verify-email-table {
margin: 20px 0;
width: 100%;
table {
border-collapse: collapse;
border: 1px solid;
}
tr {
border: solid;
border-width: 1px 0;
}
td {
padding: 5px;
border: solid;
border-width: 0 1px;
}
}
button.search-button {
background: #84c63c url("../../img/icons/go-arrow.png") center no-repeat;
background-image: url("../../img/icons/go-arrow.png"), linear-gradient(#84c63c, #489615);