From 20a7016c23224ac880e17b417730dec86d4611dd Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 13 Jun 2024 19:56:41 +0000 Subject: [PATCH 1/2] chore: Revert "Revert "Feat: enrollments can expire (#1989)" (#2052)" This reverts commit ea0dbeac2e7beff6edfee2a34b829dc54f75309a, reversing changes made to dd3bd7a2bf65662985149be7ad566cd31d5c2f98. --- benefits/enrollment/views.py | 107 ++++++- benefits/settings.py | 6 +- tests/pytest/conftest.py | 10 + tests/pytest/enrollment/test_views.py | 445 ++++++++++++++++++++++++-- 4 files changed, 536 insertions(+), 32 deletions(-) diff --git a/benefits/enrollment/views.py b/benefits/enrollment/views.py index 916d942fb..f95308646 100644 --- a/benefits/enrollment/views.py +++ b/benefits/enrollment/views.py @@ -3,10 +3,12 @@ """ import logging +from datetime import timedelta from django.http import JsonResponse from django.template.response import TemplateResponse from django.urls import reverse +from django.utils import timezone from django.utils.decorators import decorator_from_middleware from littlepay.api.client import Client from requests.exceptions import HTTPError @@ -92,7 +94,6 @@ def index(request): if not form.is_valid(): raise Exception("Invalid card token form") - logger.debug("Read tokenized card") card_token = form.cleaned_data.get("card_token") client = Client( @@ -104,17 +105,69 @@ def index(request): client.oauth.ensure_active_token(client.token) funding_source = client.get_funding_source_by_token(card_token) + group_id = eligibility.group_id try: - client.link_concession_group_funding_source(funding_source_id=funding_source.id, group_id=eligibility.group_id) + group_funding_source = _get_group_funding_source( + client=client, group_id=group_id, funding_source_id=funding_source.id + ) + + already_enrolled = group_funding_source is not None + + if eligibility.supports_expiration: + # set expiry on session + if already_enrolled and group_funding_source.expiry_date is not None: + session.update(request, enrollment_expiry=group_funding_source.expiry_date) + else: + session.update(request, enrollment_expiry=_calculate_expiry(eligibility.expiration_days)) + + if not already_enrolled: + # enroll user with an expiration date, return success + client.link_concession_group_funding_source( + group_id=group_id, funding_source_id=funding_source.id, expiry=session.enrollment_expiry(request) + ) + return success(request) + else: # already_enrolled + if group_funding_source.expiry_date is None: + # update expiration of existing enrollment, return success + client.update_concession_group_funding_source_expiry( + group_id=group_id, + funding_source_id=funding_source.id, + expiry=session.enrollment_expiry(request), + ) + return success(request) + else: + is_expired = _is_expired(group_funding_source.expiry_date) + is_within_reenrollment_window = _is_within_reenrollment_window( + group_funding_source.expiry_date, session.enrollment_reenrollment(request) + ) + + if is_expired or is_within_reenrollment_window: + # update expiration of existing enrollment, return success + client.update_concession_group_funding_source_expiry( + group_id=group_id, + funding_source_id=funding_source.id, + expiry=session.enrollment_expiry(request), + ) + return success(request) + else: + # re-enrollment error, return enrollment error with expiration and reenrollment_date + return reenrollment_error(request) + else: # eligibility does not support expiration + if not already_enrolled: + # enroll user with no expiration date, return success + client.link_concession_group_funding_source(group_id=group_id, funding_source_id=funding_source.id) + return success(request) + else: # already_enrolled + if group_funding_source.expiry_date is None: + # no action, return success + return success(request) + else: + # remove expiration date, return success + raise NotImplementedError("Removing expiration date is currently not supported") + except HTTPError as e: - # 409 means that customer already belongs to a concession group. - # the response JSON will look like: - # {"errors":[{"detail":"Conflict (409) - Customer already belongs to a concession group."}]} - if e.response.status_code == 409: - analytics.returned_success(request, eligibility.group_id) - return success(request) - elif e.response.status_code >= 500: + if e.response.status_code >= 500: analytics.returned_error(request, str(e)) sentry_sdk.capture_exception(e) @@ -125,9 +178,6 @@ def index(request): except Exception as e: analytics.returned_error(request, str(e)) raise e - else: - analytics.returned_success(request, eligibility.group_id) - return success(request) # GET enrollment index else: @@ -154,6 +204,37 @@ def index(request): return TemplateResponse(request, eligibility.enrollment_index_template, context) +def _get_group_funding_source(client: Client, group_id, funding_source_id): + group_funding_sources = client.get_concession_group_linked_funding_sources(group_id) + matching_group_funding_source = None + for group_funding_source in group_funding_sources: + if group_funding_source.id == funding_source_id: + matching_group_funding_source = group_funding_source + break + + return matching_group_funding_source + + +def _is_expired(expiry_date): + """Returns whether the passed in datetime is expired or not.""" + return expiry_date <= timezone.now() + + +def _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date): + """Returns if we are currently within the reenrollment window.""" + return enrollment_reenrollment_date <= timezone.now() < expiry_date + + +def _calculate_expiry(expiration_days): + """Returns the expiry datetime, which should be midnight in our configured timezone of the (N + 1)th day from now, + where N is expiration_days.""" + default_time_zone = timezone.get_default_timezone() + expiry_date = timezone.localtime(timezone=default_time_zone) + timedelta(days=expiration_days + 1) + expiry_datetime = expiry_date.replace(hour=0, minute=0, second=0, microsecond=0) + + return expiry_datetime + + @decorator_from_middleware(EligibleSessionRequired) def reenrollment_error(request): """View handler for a re-enrollment attempt that is not yet within the re-enrollment window.""" @@ -192,6 +273,7 @@ def system_error(request): @pageview_decorator +@decorator_from_middleware(EligibleSessionRequired) @decorator_from_middleware(VerifierSessionRequired) def success(request): """View handler for the final success page.""" @@ -206,4 +288,5 @@ def success(request): # if they click the logout button, they are taken to the new route session.update(request, origin=reverse(ROUTE_LOGGED_OUT)) + analytics.returned_success(request, eligibility.group_id) return TemplateResponse(request, eligibility.enrollment_success_template) diff --git a/benefits/settings.py b/benefits/settings.py index 2d72dc8d2..829eb3b12 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -204,7 +204,11 @@ def RUNTIME_ENVIRONMENT(): USE_I18N = True -TIME_ZONE = "UTC" +# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-TIME_ZONE +# > Note that this isn’t necessarily the time zone of the server. +# > When USE_TZ is True, this is the default time zone that Django will use to display datetimes in templates +# > and to interpret datetimes entered in forms. +TIME_ZONE = "America/Los_Angeles" USE_TZ = True # https://docs.djangoproject.com/en/5.0/topics/i18n/formatting/#creating-custom-format-files diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 60e12ef7c..aaddb4ba3 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -1,6 +1,7 @@ from unittest.mock import create_autospec from django.contrib.sessions.middleware import SessionMiddleware from django.middleware.locale import LocaleMiddleware +from django.utils import timezone import pytest from pytest_socket import disable_socket @@ -261,6 +262,15 @@ def mocked_session_oauth_token(mocker): return mocker.patch("benefits.core.session.oauth_token", autospec=True, return_value="token") +@pytest.fixture +def mocked_session_enrollment_expiry(mocker): + return mocker.patch( + "benefits.core.session.enrollment_expiry", + autospec=True, + return_value=timezone.make_aware(timezone.datetime(2024, 1, 1), timezone=timezone.get_default_timezone()), + ) + + @pytest.fixture def mocked_session_verifier(mocker, model_EligibilityVerifier): return mocker.patch("benefits.core.session.verifier", autospec=True, return_value=model_EligibilityVerifier) diff --git a/tests/pytest/enrollment/test_views.py b/tests/pytest/enrollment/test_views.py index 625fb5d40..d93a92936 100644 --- a/tests/pytest/enrollment/test_views.py +++ b/tests/pytest/enrollment/test_views.py @@ -1,9 +1,13 @@ +from datetime import timedelta import time import pytest from authlib.integrations.base_client.errors import UnsupportedTokenTypeError from django.urls import reverse +from django.utils import timezone + from littlepay.api.funding_sources import FundingSourceResponse +from littlepay.api.groups import GroupFundingSourceResponse from requests import HTTPError import benefits.enrollment.views @@ -19,6 +23,10 @@ ROUTE_TOKEN, TEMPLATE_SYSTEM_ERROR, TEMPLATE_RETRY, + _get_group_funding_source, + _calculate_expiry, + _is_expired, + _is_within_reenrollment_window, ) @@ -58,6 +66,26 @@ def mocked_funding_source(): ) +@pytest.fixture +def mocked_group_funding_source_no_expiry(mocked_funding_source): + return GroupFundingSourceResponse( + id=mocked_funding_source.id, + created_date=None, + updated_date=None, + expiry_date=None, + ) + + +@pytest.fixture +def mocked_group_funding_source_with_expiry(mocked_funding_source): + return GroupFundingSourceResponse( + id=mocked_funding_source.id, + created_date="2023-01-01T00:00:00Z", + updated_date="2021-01-01T00:00:00Z", + expiry_date="2021-01-01T00:00:00Z", + ) + + @pytest.mark.django_db def test_token_ineligible(client): path = reverse(ROUTE_TOKEN) @@ -239,6 +267,7 @@ def test_index_eligible_post_valid_form_http_error_500( mocker, client, mocked_session_agency, + model_EligibilityType, mocked_analytics_module, mocked_sentry_sdk_module, card_tokenize_form_data, @@ -246,6 +275,7 @@ def test_index_eligible_post_valid_form_http_error_500( ): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.agency.return_value = mocked_session_agency.return_value + mock_session.eligibility.return_value = model_EligibilityType mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value @@ -273,7 +303,6 @@ def test_index_eligible_post_valid_form_http_error_400(mocker, client, card_toke mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value - # any 400 level status_code that isn't 409 is considered an error mock_error = {"message": "Mock error message"} mock_error_response = mocker.Mock(status_code=400, **mock_error) mock_error_response.json.return_value = mock_error @@ -299,33 +328,122 @@ def test_index_eligible_post_valid_form_failure(mocker, client, card_tokenize_fo client.post(path, card_tokenize_form_data) +@pytest.mark.django_db +@pytest.mark.usefixtures("model_EligibilityType") +def test_get_group_funding_sources_funding_source_not_enrolled_yet(mocker, mocked_funding_source): + mock_client = mocker.Mock() + mock_client.get_concession_group_linked_funding_sources.return_value = [] + + matching_group_funding_source = _get_group_funding_source(mock_client, "group123", mocked_funding_source.id) + + assert matching_group_funding_source is None + + +@pytest.mark.django_db +@pytest.mark.usefixtures("model_EligibilityType") +def test_get_group_funding_sources_funding_source_already_enrolled( + mocker, mocked_funding_source, mocked_group_funding_source_no_expiry +): + mock_client = mocker.Mock() + mock_client.get_concession_group_linked_funding_sources.return_value = [mocked_group_funding_source_no_expiry] + + matching_group_funding_source = _get_group_funding_source(mock_client, "group123", mocked_funding_source.id) + + assert matching_group_funding_source == mocked_group_funding_source_no_expiry + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_index_eligible_post_valid_form_success_does_not_support_expiration_customer_already_enrolled_no_expiry( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_does_not_support_expiration, + mocked_funding_source, + mocked_group_funding_source_no_expiry, +): + mock_client_cls = mocker.patch("benefits.enrollment.views.Client") + mock_client = mock_client_cls.return_value + mock_client.get_funding_source_by_token.return_value = mocked_funding_source + + mocker.patch("benefits.enrollment.views._get_group_funding_source", return_value=mocked_group_funding_source_no_expiry) + + path = reverse(ROUTE_INDEX) + response = client.post(path, card_tokenize_form_data) + + assert response.status_code == 200 + assert response.template_name == model_EligibilityType_does_not_support_expiration.enrollment_success_template + mocked_analytics_module.returned_success.assert_called_once() + assert ( + model_EligibilityType_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + ) + + @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") -def test_index_eligible_post_valid_form_customer_already_enrolled( - mocker, client, card_tokenize_form_data, mocked_analytics_module, model_EligibilityType, mocked_funding_source +def test_index_eligible_post_valid_form_success_does_not_support_expiration_no_expiry( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_does_not_support_expiration, + mocked_funding_source, ): mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value mock_client.get_funding_source_by_token.return_value = mocked_funding_source - mock_error_response = mocker.Mock(status_code=409) - mock_client.link_concession_group_funding_source.side_effect = HTTPError(response=mock_error_response) path = reverse(ROUTE_INDEX) response = client.post(path, card_tokenize_form_data) mock_client.link_concession_group_funding_source.assert_called_once_with( - funding_source_id=mocked_funding_source.id, group_id=model_EligibilityType.group_id + funding_source_id=mocked_funding_source.id, group_id=model_EligibilityType_does_not_support_expiration.group_id ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType.enrollment_success_template + assert response.template_name == model_EligibilityType_does_not_support_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert model_EligibilityType.group_id in mocked_analytics_module.returned_success.call_args.args + assert ( + model_EligibilityType_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + ) + + +def test_calculate_expiry(): + expiration_days = 365 + + expiry_date = _calculate_expiry(expiration_days) + + assert expiry_date == ( + timezone.localtime(timezone=timezone.get_default_timezone()) + timedelta(days=expiration_days + 1) + ).replace(hour=0, minute=0, second=0, microsecond=0) + + +def test_calculate_expiry_specific_date(mocker): + expiration_days = 14 + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware( + value=timezone.datetime(2024, 3, 1, 13, 37, 11, 5), timezone=timezone.get_fixed_timezone(offset=0) + ), + ) + + expiry_date = _calculate_expiry(expiration_days) + + assert expiry_date == timezone.make_aware( + value=timezone.datetime(2024, 3, 16, 0, 0, 0, 0), timezone=timezone.get_default_timezone() + ) @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") -def test_index_eligible_post_valid_form_success( - mocker, client, card_tokenize_form_data, mocked_analytics_module, model_EligibilityType, mocked_funding_source +def test_index_eligible_post_valid_form_success_supports_expiration( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_supports_expiration, + mocked_funding_source, + mocked_session_enrollment_expiry, ): mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value @@ -335,12 +453,293 @@ def test_index_eligible_post_valid_form_success( response = client.post(path, card_tokenize_form_data) mock_client.link_concession_group_funding_source.assert_called_once_with( - funding_source_id=mocked_funding_source.id, group_id=model_EligibilityType.group_id + funding_source_id=mocked_funding_source.id, + group_id=model_EligibilityType_supports_expiration.group_id, + expiry=mocked_session_enrollment_expiry.return_value, ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType.enrollment_success_template + assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert model_EligibilityType.group_id in mocked_analytics_module.returned_success.call_args.args + assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_index_eligible_post_valid_form_success_supports_expiration_no_expiry( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_supports_expiration, + mocked_funding_source, + mocked_group_funding_source_no_expiry, + mocked_session_enrollment_expiry, +): + mock_client_cls = mocker.patch("benefits.enrollment.views.Client") + mock_client = mock_client_cls.return_value + mock_client.get_funding_source_by_token.return_value = mocked_funding_source + + mocker.patch("benefits.enrollment.views._get_group_funding_source", return_value=mocked_group_funding_source_no_expiry) + + path = reverse(ROUTE_INDEX) + response = client.post(path, card_tokenize_form_data) + + mock_client.update_concession_group_funding_source_expiry.assert_called_once_with( + funding_source_id=mocked_funding_source.id, + group_id=model_EligibilityType_supports_expiration.group_id, + expiry=mocked_session_enrollment_expiry.return_value, + ) + assert response.status_code == 200 + assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + mocked_analytics_module.returned_success.assert_called_once() + assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + + +def test_is_expired_expiry_date_is_in_the_past(mocker): + expiry_date = timezone.make_aware(timezone.datetime(2023, 12, 31), timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware(timezone.datetime(2024, 1, 1, 10, 30), timezone.get_default_timezone()), + ) + + assert _is_expired(expiry_date) + + +def test_is_expired_expiry_date_is_in_the_future(mocker): + expiry_date = timezone.make_aware(timezone.datetime(2024, 1, 1, 17, 34), timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware(timezone.datetime(2024, 1, 1, 11, 5), timezone.get_default_timezone()), + ) + + assert not _is_expired(expiry_date) + + +def test_is_expired_expiry_date_equals_now(mocker): + expiry_date = timezone.make_aware(timezone.datetime(2024, 1, 1, 13, 37), timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware(timezone.datetime(2024, 1, 1, 13, 37), timezone.get_default_timezone()), + ) + + assert _is_expired(expiry_date) + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_index_eligible_post_valid_form_success_supports_expiration_is_expired( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_supports_expiration, + mocked_funding_source, + mocked_group_funding_source_with_expiry, + mocked_session_enrollment_expiry, +): + mock_client_cls = mocker.patch("benefits.enrollment.views.Client") + mock_client = mock_client_cls.return_value + mock_client.get_funding_source_by_token.return_value = mocked_funding_source + + # mock that a funding source already exists, doesn't matter what expiry_date is + mocker.patch("benefits.enrollment.views._get_group_funding_source", return_value=mocked_group_funding_source_with_expiry) + + mocker.patch("benefits.enrollment.views._is_expired", return_value=True) + + path = reverse(ROUTE_INDEX) + response = client.post(path, card_tokenize_form_data) + + mock_client.update_concession_group_funding_source_expiry.assert_called_once_with( + funding_source_id=mocked_funding_source.id, + group_id=model_EligibilityType_supports_expiration.group_id, + expiry=mocked_session_enrollment_expiry.return_value, + ) + assert response.status_code == 200 + assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + mocked_analytics_module.returned_success.assert_called_once() + assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + + +def test_is_within_enrollment_window_True(mocker): + enrollment_reenrollment_date = timezone.make_aware(timezone.datetime(2023, 2, 1), timezone=timezone.get_default_timezone()) + expiry_date = timezone.make_aware(timezone.datetime(2023, 3, 1), timezone=timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware(timezone.datetime(2023, 2, 15, 15, 30), timezone=timezone.get_default_timezone()), + ) + + is_within_reenrollment_window = _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date) + + assert is_within_reenrollment_window + + +def test_is_within_enrollment_window_before_window(mocker): + enrollment_reenrollment_date = timezone.make_aware(timezone.datetime(2023, 2, 1), timezone=timezone.get_default_timezone()) + expiry_date = timezone.make_aware(timezone.datetime(2023, 3, 1), timezone=timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware(timezone.datetime(2023, 1, 15, 15, 30), timezone=timezone.get_default_timezone()), + ) + + is_within_reenrollment_window = _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date) + + assert not is_within_reenrollment_window + + +def test_is_within_enrollment_window_after_window(mocker): + enrollment_reenrollment_date = timezone.make_aware(timezone.datetime(2023, 2, 1), timezone=timezone.get_default_timezone()) + expiry_date = timezone.make_aware(timezone.datetime(2023, 3, 1), timezone=timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=timezone.make_aware(timezone.datetime(2023, 3, 15, 15, 30), timezone=timezone.get_default_timezone()), + ) + + is_within_reenrollment_window = _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date) + + assert not is_within_reenrollment_window + + +def test_is_within_enrollment_window_equal_reenrollment_date(mocker): + enrollment_reenrollment_date = timezone.make_aware(timezone.datetime(2023, 2, 1), timezone=timezone.get_default_timezone()) + expiry_date = timezone.make_aware(timezone.datetime(2023, 3, 1), timezone=timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=enrollment_reenrollment_date, + ) + + is_within_reenrollment_window = _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date) + + assert is_within_reenrollment_window + + +def test_is_within_enrollment_window_equal_expiry_date(mocker): + enrollment_reenrollment_date = timezone.make_aware(timezone.datetime(2023, 2, 1), timezone=timezone.get_default_timezone()) + expiry_date = timezone.make_aware(timezone.datetime(2023, 3, 1), timezone=timezone.get_default_timezone()) + + # mock datetime of "now" to be specific date for testing + mocker.patch( + "benefits.enrollment.views.timezone.now", + return_value=expiry_date, + ) + + is_within_reenrollment_window = _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date) + + assert not is_within_reenrollment_window + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_index_eligible_post_valid_form_success_supports_expiration_is_within_reenrollment_window( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_supports_expiration, + mocked_funding_source, + mocked_group_funding_source_with_expiry, + mocked_session_enrollment_expiry, +): + mock_client_cls = mocker.patch("benefits.enrollment.views.Client") + mock_client = mock_client_cls.return_value + mock_client.get_funding_source_by_token.return_value = mocked_funding_source + + # mock that a funding source already exists, doesn't matter what expiry_date is + mocker.patch("benefits.enrollment.views._get_group_funding_source", return_value=mocked_group_funding_source_with_expiry) + + mocker.patch("benefits.enrollment.views._is_within_reenrollment_window", return_value=True) + + path = reverse(ROUTE_INDEX) + response = client.post(path, card_tokenize_form_data) + + mock_client.update_concession_group_funding_source_expiry.assert_called_once_with( + funding_source_id=mocked_funding_source.id, + group_id=model_EligibilityType_supports_expiration.group_id, + expiry=mocked_session_enrollment_expiry.return_value, + ) + assert response.status_code == 200 + assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + mocked_analytics_module.returned_success.assert_called_once() + assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_index_eligible_post_valid_form_success_supports_expiration_is_not_expired_yet( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + mocked_funding_source, + mocked_group_funding_source_with_expiry, + model_EligibilityType_supports_expiration, +): + mock_client_cls = mocker.patch("benefits.enrollment.views.Client") + mock_client = mock_client_cls.return_value + mock_client.get_funding_source_by_token.return_value = mocked_funding_source + + # mock that a funding source already exists, doesn't matter what expiry_date is + mocker.patch("benefits.enrollment.views._get_group_funding_source", return_value=mocked_group_funding_source_with_expiry) + + mocker.patch("benefits.enrollment.views._is_expired", return_value=False) + mocker.patch("benefits.enrollment.views._is_within_reenrollment_window", return_value=False) + + path = reverse(ROUTE_INDEX) + response = client.post(path, card_tokenize_form_data) + + assert response.status_code == 200 + assert response.template_name == model_EligibilityType_supports_expiration.reenrollment_error_template + mocked_analytics_module.returned_error.assert_called_once() + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_index_eligible_post_valid_form_success_does_not_support_expiration_has_expiration_date( + mocker, + client, + card_tokenize_form_data, + mocked_analytics_module, + model_EligibilityType_does_not_support_expiration, + mocked_funding_source, + mocked_group_funding_source_with_expiry, +): + mock_client_cls = mocker.patch("benefits.enrollment.views.Client") + mock_client = mock_client_cls.return_value + mock_client.get_funding_source_by_token.return_value = mocked_funding_source + + # mock that a funding source already exists, doesn't matter what expiry_date is + mocker.patch("benefits.enrollment.views._get_group_funding_source", return_value=mocked_group_funding_source_with_expiry) + + path = reverse(ROUTE_INDEX) + with pytest.raises(NotImplementedError): + client.post(path, card_tokenize_form_data) + + # this is what we would assert if removing expiration were supported + # + # mock_client.link_concession_group_funding_source.assert_called_once_with( + # funding_source_id=mocked_funding_source.id, + # group_id=model_EligibilityType_does_not_support_expiration.group_id, + # expiry_date=None, + # ) + # assert response.status_code == 200 + # assert response.template_name == model_EligibilityType_does_not_support_expiration.enrollment_success_template + # mocked_analytics_module.returned_success.assert_called_once() + # assert ( + # model_EligibilityType_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + # ) @pytest.mark.django_db @@ -428,8 +827,8 @@ def test_success_no_verifier(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_auth_verification") -def test_success_authentication_logged_in(mocker, client, model_TransitAgency, model_EligibilityType): +@pytest.mark.usefixtures("mocked_session_verifier_uses_auth_verification", "mocked_session_eligibility") +def test_success_authentication_logged_in(mocker, client, model_TransitAgency, model_EligibilityType, mocked_analytics_module): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.logged_in.return_value = True mock_session.agency.return_value = model_TransitAgency @@ -441,11 +840,14 @@ def test_success_authentication_logged_in(mocker, client, model_TransitAgency, m assert response.status_code == 200 assert response.template_name == model_EligibilityType.enrollment_success_template assert {"origin": reverse(ROUTE_LOGGED_OUT)} in mock_session.update.call_args + mocked_analytics_module.returned_success.assert_called_once() @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_auth_verification") -def test_success_authentication_not_logged_in(mocker, client, model_TransitAgency, model_EligibilityType): +@pytest.mark.usefixtures("mocked_session_verifier_uses_auth_verification", "mocked_session_eligibility") +def test_success_authentication_not_logged_in( + mocker, client, model_TransitAgency, model_EligibilityType, mocked_analytics_module +): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.logged_in.return_value = False mock_session.agency.return_value = model_TransitAgency @@ -456,15 +858,20 @@ def test_success_authentication_not_logged_in(mocker, client, model_TransitAgenc assert response.status_code == 200 assert response.template_name == model_EligibilityType.enrollment_success_template + mocked_analytics_module.returned_success.assert_called_once() @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier_does_not_use_auth_verification") -def test_success_no_authentication(mocker, client, model_EligibilityType): +@pytest.mark.usefixtures( + "mocked_session_agency", "mocked_session_verifier_does_not_use_auth_verification", "mocked_session_eligibility" +) +def test_success_no_authentication(mocker, client, model_EligibilityType, mocked_analytics_module): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.eligibility.return_value = model_EligibilityType + path = reverse(ROUTE_SUCCESS) response = client.get(path) assert response.status_code == 200 assert response.template_name == model_EligibilityType.enrollment_success_template + mocked_analytics_module.returned_success.assert_called_once() From 50579680ece6b569a41907fac3e5d4971cb30987 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 13 Jun 2024 23:16:05 +0000 Subject: [PATCH 2/2] chore: update sample fixtures to show expiry example --- benefits/core/migrations/local_fixtures.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index c66dfd8b2..a6910b280 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -131,7 +131,12 @@ "fields": { "name": "calfresh", "label": "CalFresh", - "group_id": "group123" + "group_id": "group123", + "supports_expiration": "True", + "expiration_days": 5, + "expiration_reenrollment_days": 3, + "reenrollment_error_template": "enrollment/reenrollment-error--calfresh.html", + "enrollment_success_template": "enrollment/success--mst.html" } }, {