Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4711866
feat: IANA review email ingestor API
jennifer-richards Apr 4, 2024
00cc259
refactor: Replace iana email api with generic one
jennifer-richards Apr 5, 2024
88c0027
chore: Add type hint
jennifer-richards Apr 5, 2024
29a4823
feat: Ingest ipr responses
jennifer-richards Apr 5, 2024
d03d7e6
feat: Ingest nomcom feedback
jennifer-richards Apr 5, 2024
d22c940
refactor: message -> msg
jennifer-richards Apr 9, 2024
280b9ac
fix: Typo
jennifer-richards Apr 10, 2024
1c07ad5
feat: Send email on nomcom ingestion failure
jennifer-richards Apr 18, 2024
0d874bd
feat: Send email on IPR mail ingestion error
jennifer-richards Apr 18, 2024
aaf5d5a
feat: Check content type, handle more errs
jennifer-richards Apr 19, 2024
89bb544
fix: drop additionalProperties: false
jennifer-richards Apr 19, 2024
e498022
test: Test ingest_email view
jennifer-richards Apr 19, 2024
527a466
Revert "test: Test ingest_email view"
jennifer-richards Apr 19, 2024
e0e5407
test: Test ingest_email view
jennifer-richards Apr 19, 2024
6f18091
fix: pass new test
jennifer-richards Apr 19, 2024
42444d9
test: Test ingest_review_email
jennifer-richards Apr 19, 2024
ecdd81a
fix: Pass new test
jennifer-richards Apr 19, 2024
8a7e931
test: Test ipr ingest_response_email
jennifer-richards Apr 19, 2024
22b7ab9
fix: pass new test
jennifer-richards Apr 19, 2024
3388a3f
test: test nomcom ingest_feedback_email
jennifer-richards Apr 20, 2024
feef1d3
chore: fix typo found in code reviw
jennifer-richards Apr 22, 2024
13e1f2c
Merge branch 'refs/heads/main' into post-mail
jennifer-richards Apr 22, 2024
9dbfcc3
fix: De-lint
jennifer-richards Apr 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 191 additions & 2 deletions ietf/api/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright The IETF Trust 2015-2020, All Rights Reserved
# -*- coding: utf-8 -*-

import base64
import datetime
import json
import html
Expand Down Expand Up @@ -36,11 +36,12 @@
from ietf.person.models import Email, User
from ietf.person.models import PersonalApiKey
from ietf.stats.models import MeetingRegistration
from ietf.utils.mail import outbox, get_payload_text
from ietf.utils.mail import empty_outbox, outbox, get_payload_text
from ietf.utils.models import DumpInfo
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, reload_db_objects

from .ietf_utils import is_valid_token, requires_api_token
from .views import EmailIngestionError

OMITTED_APPS = (
'ietf.secr.meetings',
Expand Down Expand Up @@ -1013,6 +1014,194 @@ def test_role_holder_addresses(self):
sorted(e.address for e in emails),
)

@override_settings(APP_API_TOKENS={"ietf.api.views.ingest_email": "valid-token"})
@mock.patch("ietf.api.views.iana_ingest_review_email")
@mock.patch("ietf.api.views.ipr_ingest_response_email")
@mock.patch("ietf.api.views.nomcom_ingest_feedback_email")
def test_ingest_email(
self, mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest
):
mocks = {mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest}
empty_outbox()
url = urlreverse("ietf.api.views.ingest_email")

# test various bad calls
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(url)
self.assertEqual(r.status_code, 403)
self.assertFalse(any(m.called for m in mocks))

r = self.client.get(url, headers={"X-Api-Key": "valid-token"})
self.assertEqual(r.status_code, 405)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(url, headers={"X-Api-Key": "valid-token"})
self.assertEqual(r.status_code, 415)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(
url, content_type="application/json", headers={"X-Api-Key": "valid-token"}
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(
url,
"this is not JSON!",
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(
url,
{"json": "yes", "valid_schema": False},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))

# test that valid requests call handlers appropriately
message_b64 = base64.b64encode(b"This is a message").decode()
r = self.client.post(
url,
{"dest": "iana-review", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 200)
self.assertTrue(mock_iana_ingest.called)
self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message"))
self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest})))
mock_iana_ingest.reset_mock()

r = self.client.post(
url,
{"dest": "ipr-response", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 200)
self.assertTrue(mock_ipr_ingest.called)
self.assertEqual(mock_ipr_ingest.call_args, mock.call(b"This is a message"))
self.assertFalse(any(m.called for m in (mocks - {mock_ipr_ingest})))
mock_ipr_ingest.reset_mock()

r = self.client.post(
url,
{"dest": "nomcom-feedback", "message": message_b64, "year": 2024}, # arbitrary year
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 200)
self.assertTrue(mock_nomcom_ingest.called)
self.assertEqual(mock_nomcom_ingest.call_args, mock.call(b"This is a message", 2024))
self.assertFalse(any(m.called for m in (mocks - {mock_nomcom_ingest})))
mock_nomcom_ingest.reset_mock()

# test that exceptions lead to email being sent - assumes that iana-review handling is representative
mock_iana_ingest.side_effect = EmailIngestionError("Error: don't send email")
r = self.client.post(
url,
{"dest": "iana-review", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 400)
self.assertTrue(mock_iana_ingest.called)
self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message"))
self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest})))
self.assertEqual(len(outbox), 0) # implicitly tests that _none_ of the earlier tests sent email
mock_iana_ingest.reset_mock()

# test default recipients and attached original message
mock_iana_ingest.side_effect = EmailIngestionError(
"Error: do send email",
email_body="This is my email\n",
email_original_message=b"This is the original message"
)
with override_settings(ADMINS=[("Some Admin", "admin@example.com")]):
r = self.client.post(
url,
{"dest": "iana-review", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 400)
self.assertTrue(mock_iana_ingest.called)
self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message"))
self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest})))
self.assertEqual(len(outbox), 1)
self.assertIn("admin@example.com", outbox[0]["To"])
self.assertEqual("Error: do send email", outbox[0]["Subject"])
self.assertEqual("This is my email\n", get_payload_text(outbox[0].get_body()))
attachments = list(a for a in outbox[0].iter_attachments())
self.assertEqual(len(attachments), 1)
self.assertEqual(attachments[0].get_filename(), "original-message")
self.assertEqual(attachments[0].get_content_type(), "application/octet-stream")
self.assertEqual(attachments[0].get_content(), b"This is the original message")
mock_iana_ingest.reset_mock()
empty_outbox()

# test overridden recipients and no attached original message
mock_iana_ingest.side_effect = EmailIngestionError(
"Error: do send email",
email_body="This is my email\n",
email_recipients=("thatguy@example.com")
)
with override_settings(ADMINS=[("Some Admin", "admin@example.com")]):
r = self.client.post(
url,
{"dest": "iana-review", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 400)
self.assertTrue(mock_iana_ingest.called)
self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message"))
self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest})))
self.assertEqual(len(outbox), 1)
self.assertNotIn("admin@example.com", outbox[0]["To"])
self.assertIn("thatguy@example.com", outbox[0]["To"])
self.assertEqual("Error: do send email", outbox[0]["Subject"])
self.assertEqual("This is my email\n", get_payload_text(outbox[0]))
mock_iana_ingest.reset_mock()
empty_outbox()

# test attached traceback
mock_iana_ingest.side_effect = EmailIngestionError(
"Error: do send email",
email_body="This is my email\n",
email_attach_traceback=True,
)
with override_settings(ADMINS=[("Some Admin", "admin@example.com")]):
r = self.client.post(
url,
{"dest": "iana-review", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "valid-token"},
)
self.assertEqual(r.status_code, 400)
self.assertTrue(mock_iana_ingest.called)
self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message"))
self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest})))
self.assertEqual(len(outbox), 1)
self.assertIn("admin@example.com", outbox[0]["To"])
self.assertEqual("Error: do send email", outbox[0]["Subject"])
self.assertEqual("This is my email\n", get_payload_text(outbox[0].get_body()))
attachments = list(a for a in outbox[0].iter_attachments())
self.assertEqual(len(attachments), 1)
self.assertEqual(attachments[0].get_filename(), "traceback.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertIn("ietf.api.views.EmailIngestionError: Error: do send email", attachments[0].get_content())
mock_iana_ingest.reset_mock()
empty_outbox()


class DirectAuthApiTests(TestCase):

Expand Down
4 changes: 3 additions & 1 deletion ietf/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
# --- Custom API endpoints, sorted alphabetically ---
# Email alias information for drafts
url(r'^doc/draft-aliases/$', api_views.draft_aliases),
# GPRD: export of personal information for the logged-in person
# email ingestor
url(r'email/$', api_views.ingest_email),
# GDPR: export of personal information for the logged-in person
url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()),
# Email alias information for groups
url(r'^group/group-aliases/$', api_views.group_aliases),
Expand Down
Loading