Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c969a34

Browse files
authoredMay 27, 2025··
Refactor: add enrollment_littlepay token view (#2934)
2 parents be3caaf + 0ab6925 commit c969a34

File tree

9 files changed

+245
-212
lines changed

9 files changed

+245
-212
lines changed
 

‎benefits/enrollment/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
urlpatterns = [
1313
# /enrollment
1414
path("", views.index, name=routes.name(routes.ENROLLMENT_INDEX)),
15-
path("token", views.token, name=routes.name(routes.ENROLLMENT_TOKEN)),
1615
path("error/reenrollment", views.reenrollment_error, name=routes.name(routes.ENROLLMENT_REENROLLMENT_ERROR)),
1716
path("retry", views.retry, name=routes.name(routes.ENROLLMENT_RETRY)),
1817
path("success", views.success, name=routes.name(routes.ENROLLMENT_SUCCESS)),

‎benefits/enrollment/views.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import logging
66

77

8-
from django.http import JsonResponse
98
from django.template.response import TemplateResponse
109
from django.urls import reverse
1110
from django.utils.decorators import decorator_from_middleware
@@ -15,8 +14,6 @@
1514
from benefits.core import models, session
1615
from benefits.core.middleware import EligibleSessionRequired, FlowSessionRequired, pageview_decorator
1716

18-
from benefits.enrollment_littlepay.enrollment import request_card_tokenization_access
19-
from benefits.enrollment_littlepay.session import Session as LittlepaySession
2017
from benefits.enrollment_littlepay.views import IndexView as LittlepayIndexView
2118
from . import analytics
2219
from .enrollment import Status
@@ -28,35 +25,6 @@
2825
logger = logging.getLogger(__name__)
2926

3027

31-
@decorator_from_middleware(EligibleSessionRequired)
32-
def token(request):
33-
"""View handler for the enrollment auth token."""
34-
session = LittlepaySession(request)
35-
36-
if not session.access_token_valid():
37-
response = request_card_tokenization_access(request)
38-
39-
if response.status is Status.SUCCESS:
40-
session.access_token = response.access_token
41-
session.access_token_expiry = response.expires_at
42-
elif response.status is Status.SYSTEM_ERROR or response.status is Status.EXCEPTION:
43-
logger.debug("Error occurred while requesting access token", exc_info=response.exception)
44-
sentry_sdk.capture_exception(response.exception)
45-
analytics.failed_access_token_request(request, response.status_code)
46-
47-
if response.status is Status.SYSTEM_ERROR:
48-
redirect = reverse(routes.ENROLLMENT_SYSTEM_ERROR)
49-
else:
50-
redirect = reverse(routes.SERVER_ERROR)
51-
52-
data = {"redirect": redirect}
53-
return JsonResponse(data)
54-
55-
data = {"token": session.access_token}
56-
57-
return JsonResponse(data)
58-
59-
6028
@decorator_from_middleware(EligibleSessionRequired)
6129
def index(request):
6230
"""View handler for the enrollment landing page."""

‎benefits/enrollment_littlepay/templates/enrollment_littlepay/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
$.ajax({ dataType: "script", attrs: { nonce: "{{ request.csp_nonce }}"}, url: "{{ transit_processor.card_tokenize_url }}" })
1717
.done(function() {
18-
$.get("{% url routes.ENROLLMENT_TOKEN %}", function(data) {
18+
$.get("{% url routes.ENROLLMENT_LITTLEPAY_TOKEN %}", function(data) {
1919
if (data.redirect) {
2020
// https://stackoverflow.com/a/42469170
2121
// use 'assign' because 'replace' was giving strange Back button behavior

‎benefits/enrollment_littlepay/urls.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.urls import path
2+
3+
from benefits.routes import routes
4+
from benefits.enrollment_littlepay.views import TokenView
5+
6+
7+
app_name = "littlepay"
8+
urlpatterns = [
9+
# /littlepay
10+
path("token", TokenView.as_view(), name=routes.name(routes.ENROLLMENT_LITTLEPAY_TOKEN)),
11+
]

‎benefits/enrollment_littlepay/views.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,49 @@
1-
from django.views.generic import FormView
1+
import logging
22

3-
from benefits.routes import routes
4-
from benefits.core import session
3+
from django.http import JsonResponse
4+
from django.urls import reverse
5+
from django.views.generic import FormView, View
6+
import sentry_sdk
57

6-
from benefits.core import models
8+
from benefits.routes import routes
9+
from benefits.core import models, session
710
from benefits.core.mixins import EligibleSessionRequiredMixin
8-
from benefits.enrollment import forms
9-
from benefits.enrollment_littlepay.enrollment import enroll
11+
12+
from benefits.enrollment import analytics, forms
13+
from benefits.enrollment.enrollment import Status
14+
from benefits.enrollment_littlepay.enrollment import enroll, request_card_tokenization_access
15+
from benefits.enrollment_littlepay.session import Session
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class TokenView(EligibleSessionRequiredMixin, View):
21+
"""View handler for the card tokenization access token."""
22+
23+
def get(self, request, *args, **kwargs):
24+
session = Session(request)
25+
26+
if not session.access_token_valid():
27+
response = request_card_tokenization_access(request)
28+
29+
if response.status is Status.SUCCESS:
30+
session.access_token = response.access_token
31+
session.access_token_expiry = response.expires_at
32+
elif response.status is Status.SYSTEM_ERROR or response.status is Status.EXCEPTION:
33+
logger.debug("Error occurred while requesting access token", exc_info=response.exception)
34+
sentry_sdk.capture_exception(response.exception)
35+
analytics.failed_access_token_request(request, response.status_code)
36+
37+
if response.status is Status.SYSTEM_ERROR:
38+
redirect = reverse(routes.ENROLLMENT_SYSTEM_ERROR)
39+
else:
40+
redirect = reverse(routes.SERVER_ERROR)
41+
42+
data = {"redirect": redirect}
43+
return JsonResponse(data)
44+
45+
data = {"token": session.access_token}
46+
return JsonResponse(data)
1047

1148

1249
class IndexView(EligibleSessionRequiredMixin, FormView):

‎benefits/routes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def ENROLLMENT_INDEX(self):
8585
return "enrollment:index"
8686

8787
@property
88-
def ENROLLMENT_TOKEN(self):
89-
"""Acquire a TransitProcessor API token for enrollment."""
90-
return "enrollment:token"
88+
def ENROLLMENT_LITTLEPAY_TOKEN(self):
89+
"""Acquire a Littlepay card tokenization access token for enrollment."""
90+
return "littlepay:token"
9191

9292
@property
9393
def ENROLLMENT_SUCCESS(self):

‎benefits/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
path("i18n/", include("django.conf.urls.i18n")),
2929
path("oauth/", include("benefits.oauth.urls")),
3030
path("in_person/", include("benefits.in_person.urls")),
31+
path("littlepay/", include("benefits.enrollment_littlepay.urls")),
3132
]
3233

3334
if settings.RUNTIME_ENVIRONMENT() == settings.RUNTIME_ENVS.LOCAL:

‎tests/pytest/enrollment/test_views.py

Lines changed: 0 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import time
2-
31
import pytest
4-
from authlib.integrations.base_client.errors import UnsupportedTokenTypeError
52
from django.urls import reverse
63
from requests import HTTPError
7-
from unittest.mock import patch, PropertyMock
84

95
from benefits.core import models
106
from benefits.routes import routes
117
import benefits.enrollment.views
128
from benefits.enrollment.enrollment import Status
13-
from benefits.enrollment_littlepay.enrollment import CardTokenizationAccessResponse
149
from benefits.core.middleware import TEMPLATE_USER_ERROR
1510
from benefits.enrollment.views import TEMPLATE_SYSTEM_ERROR, TEMPLATE_RETRY
1611

@@ -35,170 +30,6 @@ def mocked_sentry_sdk_module(mocker):
3530
return mocker.patch.object(benefits.enrollment.views, "sentry_sdk")
3631

3732

38-
@pytest.mark.django_db
39-
def test_token_ineligible(client):
40-
path = reverse(routes.ENROLLMENT_TOKEN)
41-
42-
response = client.get(path)
43-
44-
assert response.status_code == 200
45-
assert response.template_name == TEMPLATE_USER_ERROR
46-
47-
48-
@pytest.mark.django_db
49-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible")
50-
def test_token_refresh(mocker, client):
51-
mocker.patch("benefits.enrollment_littlepay.session.Session.access_token_valid", return_value=False)
52-
53-
mock_token = {}
54-
mock_token["access_token"] = "access_token"
55-
mock_token["expires_at"] = time.time() + 10000
56-
57-
mocker.patch(
58-
"benefits.enrollment.views.request_card_tokenization_access",
59-
return_value=CardTokenizationAccessResponse(
60-
Status.SUCCESS,
61-
access_token=mock_token["access_token"],
62-
expires_at=mock_token["expires_at"],
63-
),
64-
)
65-
66-
path = reverse(routes.ENROLLMENT_TOKEN)
67-
response = client.get(path)
68-
69-
assert response.status_code == 200
70-
data = response.json()
71-
assert "token" in data
72-
assert data["token"] == mock_token["access_token"]
73-
74-
75-
@pytest.mark.django_db
76-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible")
77-
@patch("benefits.enrollment_littlepay.session.Session.access_token", new=PropertyMock(return_value="enrollment_token"))
78-
def test_token_valid(mocker, client):
79-
mocker.patch("benefits.enrollment_littlepay.session.Session.access_token_valid", return_value=True)
80-
81-
path = reverse(routes.ENROLLMENT_TOKEN)
82-
response = client.get(path)
83-
84-
assert response.status_code == 200
85-
data = response.json()
86-
assert "token" in data
87-
assert data["token"] == "enrollment_token"
88-
89-
90-
@pytest.mark.django_db
91-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible")
92-
def test_token_system_error(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module):
93-
mocker.patch("benefits.enrollment_littlepay.session.Session.access_token_valid", return_value=False)
94-
95-
mock_error = {"message": "Mock error message"}
96-
mock_error_response = mocker.Mock(status_code=500, **mock_error)
97-
mock_error_response.json.return_value = mock_error
98-
http_error = HTTPError(response=mock_error_response)
99-
100-
mocker.patch(
101-
"benefits.enrollment.views.request_card_tokenization_access",
102-
return_value=CardTokenizationAccessResponse(
103-
Status.SYSTEM_ERROR, access_token=None, expires_at=None, exception=http_error, status_code=500
104-
),
105-
)
106-
107-
path = reverse(routes.ENROLLMENT_TOKEN)
108-
response = client.get(path)
109-
110-
assert response.status_code == 200
111-
data = response.json()
112-
assert "token" not in data
113-
assert "redirect" in data
114-
assert data["redirect"] == reverse(routes.ENROLLMENT_SYSTEM_ERROR)
115-
mocked_analytics_module.failed_access_token_request.assert_called_once()
116-
assert 500 in mocked_analytics_module.failed_access_token_request.call_args.args
117-
mocked_sentry_sdk_module.capture_exception.assert_called_once()
118-
119-
120-
@pytest.mark.django_db
121-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible")
122-
def test_token_http_error_400(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module):
123-
mocker.patch("benefits.enrollment_littlepay.session.Session.access_token_valid", return_value=False)
124-
125-
mock_error = {"message": "Mock error message"}
126-
mock_error_response = mocker.Mock(status_code=400, **mock_error)
127-
mock_error_response.json.return_value = mock_error
128-
http_error = HTTPError(response=mock_error_response)
129-
130-
mocker.patch(
131-
"benefits.enrollment.views.request_card_tokenization_access",
132-
return_value=CardTokenizationAccessResponse(
133-
Status.EXCEPTION, access_token=None, expires_at=None, exception=http_error, status_code=400
134-
),
135-
)
136-
137-
path = reverse(routes.ENROLLMENT_TOKEN)
138-
response = client.get(path)
139-
140-
assert response.status_code == 200
141-
data = response.json()
142-
assert "token" not in data
143-
assert "redirect" in data
144-
assert data["redirect"] == reverse(routes.SERVER_ERROR)
145-
mocked_analytics_module.failed_access_token_request.assert_called_once()
146-
assert 400 in mocked_analytics_module.failed_access_token_request.call_args.args
147-
mocked_sentry_sdk_module.capture_exception.assert_called_once()
148-
149-
150-
@pytest.mark.django_db
151-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible")
152-
def test_token_misconfigured_client_id(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module):
153-
mocker.patch("benefits.enrollment_littlepay.session.Session.access_token_valid", return_value=False)
154-
155-
exception = UnsupportedTokenTypeError()
156-
157-
mocker.patch(
158-
"benefits.enrollment.views.request_card_tokenization_access",
159-
return_value=CardTokenizationAccessResponse(
160-
Status.EXCEPTION, access_token=None, expires_at=None, exception=exception, status_code=None
161-
),
162-
)
163-
164-
path = reverse(routes.ENROLLMENT_TOKEN)
165-
response = client.get(path)
166-
167-
assert response.status_code == 200
168-
data = response.json()
169-
assert "token" not in data
170-
assert "redirect" in data
171-
assert data["redirect"] == reverse(routes.SERVER_ERROR)
172-
mocked_analytics_module.failed_access_token_request.assert_called_once()
173-
mocked_sentry_sdk_module.capture_exception.assert_called_once()
174-
175-
176-
@pytest.mark.django_db
177-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible")
178-
def test_token_connection_error(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module):
179-
mocker.patch("benefits.enrollment_littlepay.session.Session.access_token_valid", return_value=False)
180-
181-
exception = ConnectionError()
182-
183-
mocker.patch(
184-
"benefits.enrollment.views.request_card_tokenization_access",
185-
return_value=CardTokenizationAccessResponse(
186-
Status.EXCEPTION, access_token=None, expires_at=None, exception=exception, status_code=None
187-
),
188-
)
189-
190-
path = reverse(routes.ENROLLMENT_TOKEN)
191-
response = client.get(path)
192-
193-
assert response.status_code == 200
194-
data = response.json()
195-
assert "token" not in data
196-
assert "redirect" in data
197-
assert data["redirect"] == reverse(routes.SERVER_ERROR)
198-
mocked_analytics_module.failed_access_token_request.assert_called_once()
199-
mocked_sentry_sdk_module.capture_exception.assert_called_once()
200-
201-
20233
@pytest.mark.django_db
20334
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible")
20435
def test_index_eligible_get(client):

0 commit comments

Comments
 (0)
Please sign in to comment.