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: handle enrollment errors with retry view #1936

Closed
wants to merge 2 commits into from
Closed
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
23 changes: 23 additions & 0 deletions benefits/enrollment/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging

from django.template.response import TemplateResponse
from django.utils.deprecation import MiddlewareMixin


logger = logging.getLogger(__name__)


TEMPLATE_RETRY = "enrollment/retry.html"


class HandleEnrollmentError(MiddlewareMixin):
"""Middleware handles an error in the enrollment process and sends the user to the retry page."""

def process_request(self, request):
try:
response = self.get_response(request)
except Exception as e:
logger.error(f"Error caught during enrollment: {e}")
return TemplateResponse(request, TEMPLATE_RETRY)

return response
6 changes: 4 additions & 2 deletions benefits/enrollment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
pageview_decorator,
)
from benefits.core.views import ROUTE_LOGGED_OUT
from benefits.enrollment.middleware import TEMPLATE_RETRY, HandleEnrollmentError
from . import analytics, forms


Expand All @@ -27,14 +28,14 @@
ROUTE_TOKEN = "enrollment:token"

TEMPLATE_INDEX = "enrollment/index.html"
TEMPLATE_RETRY = "enrollment/retry.html"
TEMPLATE_SUCCESS = "enrollment/success.html"


logger = logging.getLogger(__name__)


@decorator_from_middleware(EligibleSessionRequired)
@decorator_from_middleware(HandleEnrollmentError)
def token(request):
"""View handler for the enrollment auth token."""
if not session.enrollment_token_valid(request):
Expand All @@ -56,6 +57,7 @@ def token(request):


@decorator_from_middleware(EligibleSessionRequired)
@decorator_from_middleware(HandleEnrollmentError)
def index(request):
"""View handler for the enrollment landing page."""
session.update(request, origin=reverse(ROUTE_INDEX))
Expand Down Expand Up @@ -126,6 +128,7 @@ def index(request):


@decorator_from_middleware(EligibleSessionRequired)
@decorator_from_middleware(HandleEnrollmentError)
def retry(request):
"""View handler for a recoverable failure condition."""
if request.method == "POST":
Expand All @@ -137,7 +140,6 @@ def retry(request):
analytics.returned_error(request, "Invalid retry submission.")
raise Exception("Invalid retry submission.")
else:
analytics.returned_error(request, "This view method only supports POST.")
raise Exception("This view method only supports POST.")


Expand Down
32 changes: 32 additions & 0 deletions tests/pytest/enrollment/test_middleware_payment_error_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest

from benefits.enrollment.middleware import HandleEnrollmentError, TEMPLATE_RETRY


@pytest.fixture
def mock_decorator_status_ok(mocker):
mock_response = mocker.Mock(status_code=200, template_name="success.html")
get_response = mocker.Mock(return_value=mock_response)
return HandleEnrollmentError(get_response)


@pytest.fixture
def mock_decorator_status_error(mocker):
get_response = mocker.Mock(side_effect=Exception("Enrollment error"))
return HandleEnrollmentError(get_response)


@pytest.mark.django_db
def test_handle_enrollment_error_no_error(app_request, mock_decorator_status_ok):
response = mock_decorator_status_ok(app_request)

assert response.status_code == 200
assert response.template_name == "success.html"


@pytest.mark.django_db
def test_handle_enrollment_error_error(app_request, mock_decorator_status_error):
response = mock_decorator_status_error(app_request)

assert response.status_code == 200
assert response.template_name == TEMPLATE_RETRY
36 changes: 23 additions & 13 deletions tests/pytest/enrollment/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

from benefits.core.middleware import TEMPLATE_USER_ERROR
from benefits.core.views import ROUTE_LOGGED_OUT
from benefits.enrollment.middleware import TEMPLATE_RETRY
from benefits.enrollment.views import (
ROUTE_INDEX,
ROUTE_TOKEN,
ROUTE_SUCCESS,
ROUTE_RETRY,
TEMPLATE_INDEX,
TEMPLATE_SUCCESS,
TEMPLATE_RETRY,
)

import benefits.enrollment.views
Expand Down Expand Up @@ -122,8 +122,10 @@ def test_index_eligible_get(client):
def test_index_eligible_post_invalid_form(client, invalid_form_data):
path = reverse(ROUTE_INDEX)

with pytest.raises(Exception, match=r"form"):
client.post(path, invalid_form_data)
response = client.post(path, invalid_form_data)

assert response.status_code == 200
assert response.template_name == TEMPLATE_RETRY


@pytest.mark.django_db
Expand All @@ -141,8 +143,10 @@ def test_index_eligible_post_valid_form_http_error(mocker, client, card_tokenize
)

path = reverse(ROUTE_INDEX)
with pytest.raises(Exception, match=mock_error["message"]):
client.post(path, card_tokenize_form_data)
response = client.post(path, card_tokenize_form_data)

assert response.status_code == 200
assert response.template_name == TEMPLATE_RETRY


@pytest.mark.django_db
Expand All @@ -154,8 +158,10 @@ def test_index_eligible_post_valid_form_failure(mocker, client, card_tokenize_fo
mock_client.link_concession_group_funding_source.side_effect = Exception("some other exception")

path = reverse(ROUTE_INDEX)
with pytest.raises(Exception, match=r"some other exception"):
client.post(path, card_tokenize_form_data)
response = client.post(path, card_tokenize_form_data)

assert response.status_code == 200
assert response.template_name == TEMPLATE_RETRY


@pytest.mark.django_db
Expand Down Expand Up @@ -223,21 +229,25 @@ def test_retry_ineligible(client):


@pytest.mark.django_db
@pytest.mark.usefixtures("mocked_session_eligibility")
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility")
def test_retry_get(client):
path = reverse(ROUTE_RETRY)
with pytest.raises(Exception, match=r"POST"):
client.get(path)
response = client.get(path)

assert response.status_code == 200
assert response.template_name == TEMPLATE_RETRY


@pytest.mark.django_db
@pytest.mark.usefixtures("mocked_session_eligibility")
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility")
def test_retry_invalid_form(mocker, client):
mocker.patch("benefits.enrollment.views.forms.CardTokenizeFailForm.is_valid", return_value=False)

path = reverse(ROUTE_RETRY)
with pytest.raises(Exception, match=r"Invalid"):
client.post(path)
response = client.get(path)

assert response.status_code == 200
assert response.template_name == TEMPLATE_RETRY


@pytest.mark.django_db
Expand Down
Loading