Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: email ingestor api test endpoint #7915

Merged
merged 3 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions dev/deploy-to-container/settings_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,11 @@

DE_GFM_BINARY = '/usr/local/bin/de-gfm'

# No real secrets here, these are public testing values _only_
APP_API_TOKENS = {
"ietf.api.views.ingest_email_test": ["ingestion-test-token"]
}


# OIDC configuration
SITE_URL = 'https://__HOSTNAME__'
97 changes: 96 additions & 1 deletion ietf/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,9 @@ 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"})
@override_settings(
APP_API_TOKENS={"ietf.api.views.ingest_email": "valid-token", "ietf.api.views.ingest_email_test": "test-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")
Expand All @@ -1032,29 +1034,47 @@ def test_ingest_email(
mocks = {mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest}
empty_outbox()
url = urlreverse("ietf.api.views.ingest_email")
test_mode_url = urlreverse("ietf.api.views.ingest_email_test")

# 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.get(test_mode_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.post(test_mode_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.get(test_mode_url, headers={"X-Api-Key": "test-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(test_mode_url, headers={"X-Api-Key": "test-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(
test_mode_url, content_type="application/json", headers={"X-Api-Key": "test-token"}
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(
url,
Expand All @@ -1064,6 +1084,14 @@ def test_ingest_email(
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))
r = self.client.post(
test_mode_url,
"this is not JSON!",
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))

r = self.client.post(
url,
Expand All @@ -1073,6 +1101,14 @@ def test_ingest_email(
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))
r = self.client.post(
test_mode_url,
{"json": "yes", "valid_schema": False},
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 400)
self.assertFalse(any(m.called for m in mocks))

# bad destination
message_b64 = base64.b64encode(b"This is a message").decode()
Expand All @@ -1086,6 +1122,16 @@ def test_ingest_email(
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "bad_dest"})
self.assertFalse(any(m.called for m in mocks))
r = self.client.post(
test_mode_url,
{"dest": "not-a-destination", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "bad_dest"})
self.assertFalse(any(m.called for m in mocks))

# test that valid requests call handlers appropriately
r = self.client.post(
Expand All @@ -1102,6 +1148,19 @@ def test_ingest_email(
self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest})))
mock_iana_ingest.reset_mock()

# the test mode endpoint should _not_ call the handler
r = self.client.post(
test_mode_url,
{"dest": "iana-review", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "ok"})
self.assertFalse(any(m.called for m in mocks))
mock_iana_ingest.reset_mock()

r = self.client.post(
url,
{"dest": "ipr-response", "message": message_b64},
Expand All @@ -1116,6 +1175,19 @@ def test_ingest_email(
self.assertFalse(any(m.called for m in (mocks - {mock_ipr_ingest})))
mock_ipr_ingest.reset_mock()

# the test mode endpoint should _not_ call the handler
r = self.client.post(
test_mode_url,
{"dest": "ipr-response", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "ok"})
self.assertFalse(any(m.called for m in mocks))
mock_ipr_ingest.reset_mock()

# bad nomcom-feedback dest
for bad_nomcom_dest in [
"nomcom-feedback", # no suffix
Expand All @@ -1133,6 +1205,16 @@ def test_ingest_email(
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "bad_dest"})
self.assertFalse(any(m.called for m in mocks))
r = self.client.post(
test_mode_url,
{"dest": bad_nomcom_dest, "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "bad_dest"})
self.assertFalse(any(m.called for m in mocks))

# good nomcom-feedback dest
random_year = randrange(100000)
Expand All @@ -1150,6 +1232,19 @@ def test_ingest_email(
self.assertFalse(any(m.called for m in (mocks - {mock_nomcom_ingest})))
mock_nomcom_ingest.reset_mock()

# the test mode endpoint should _not_ call the handler
r = self.client.post(
test_mode_url,
{"dest": f"nomcom-feedback-{random_year}", "message": message_b64},
content_type="application/json",
headers={"X-Api-Key": "test-token"},
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.headers["Content-Type"], "application/json")
self.assertEqual(json.loads(r.content), {"result": "ok"})
self.assertFalse(any(m.called for m in mocks))
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(
Expand Down
2 changes: 2 additions & 0 deletions ietf/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
url(r'^doc/draft-aliases/$', api_views.draft_aliases),
# email ingestor
url(r'email/$', api_views.ingest_email),
# email ingestor
url(r'email/test/$', api_views.ingest_email_test),
# 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
Expand Down
41 changes: 34 additions & 7 deletions ietf/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,14 +614,16 @@ def as_emailmessage(self) -> Optional[EmailMessage]:
return msg


@requires_api_token
@csrf_exempt
def ingest_email(request):
"""Ingest incoming email
def ingest_email_handler(request, test_mode=False):
"""Ingest incoming email - handler

Returns a 4xx or 5xx status code if the HTTP request was invalid or something went
wrong while processing it. If the request was valid, returns a 200. This may or may
not indicate that the message was accepted.

If test_mode is true, actual processing of a valid message will be skipped. In this
mode, a valid request with a valid destination will be treated as accepted. The
"bad_dest" error may still be returned.
"""

def _http_err(code, text):
Expand Down Expand Up @@ -657,15 +659,18 @@ def _api_response(result):
try:
if dest == "iana-review":
valid_dest = True
iana_ingest_review_email(message)
if not test_mode:
iana_ingest_review_email(message)
elif dest == "ipr-response":
valid_dest = True
ipr_ingest_response_email(message)
if not test_mode:
ipr_ingest_response_email(message)
elif dest.startswith("nomcom-feedback-"):
maybe_year = dest[len("nomcom-feedback-"):]
if maybe_year.isdecimal():
valid_dest = True
nomcom_ingest_feedback_email(message, int(maybe_year))
if not test_mode:
nomcom_ingest_feedback_email(message, int(maybe_year))
except EmailIngestionError as err:
error_email = err.as_emailmessage()
if error_email is not None:
Expand All @@ -677,3 +682,25 @@ def _api_response(result):
return _api_response("bad_dest")

return _api_response("ok")


@requires_api_token
@csrf_exempt
def ingest_email(request):
"""Ingest incoming email

Hands off to ingest_email_handler() with test_mode=False. This allows @requires_api_token to
give the test endpoint a distinct token from the real one.
"""
return ingest_email_handler(request, test_mode=False)


@requires_api_token
@csrf_exempt
def ingest_email_test(request):
"""Ingest incoming email test endpoint

Hands off to ingest_email_handler() with test_mode=True. This allows @requires_api_token to
give the test endpoint a distinct token from the real one.
"""
return ingest_email_handler(request, test_mode=True)
Loading