From 456b05557136bde56a69afeda1aebdd46f5ddf9b Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 7 Aug 2024 23:03:07 +0000 Subject: [PATCH 1/8] refactor(models): rename EligibilityVerifier to EnrollmentFlow --- .env.sample | 2 +- benefits/core/admin.py | 4 +- ...name_eligibilityverifier_enrollmentflow.py | 22 +++++ benefits/core/migrations/local_fixtures.json | 14 +-- benefits/core/models.py | 34 +++---- benefits/oauth/client.py | 24 ++--- benefits/oauth/views.py | 8 +- tests/pytest/conftest.py | 66 ++++++-------- .../core/test_middleware_login_required.py | 10 +- tests/pytest/core/test_models.py | 91 +++++++++---------- tests/pytest/core/test_views.py | 18 ++-- tests/pytest/enrollment/test_views.py | 2 +- tests/pytest/oauth/test_client.py | 26 +++--- tests/pytest/oauth/test_views.py | 12 +-- 14 files changed, 168 insertions(+), 165 deletions(-) create mode 100644 benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py diff --git a/.env.sample b/.env.sample index 85576172f..0ca4d8502 100644 --- a/.env.sample +++ b/.env.sample @@ -8,7 +8,7 @@ DJANGO_DB_FILE=django.db DJANGO_DB_FIXTURES="benefits/core/migrations/local_fixtures.json" claims_provider_client_id=benefits-oauth-client-id -agency_card_verifier_api_auth_key=server-auth-token +agency_card_flow_api_auth_key=server-auth-token client_private_key='-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1pt0ZoOuPEVPJJS+5r884zcjZLkZZ2GcPwr79XOLDbOi46on\nCa79kjRnhS0VUK96SwUPS0z9J5mDA5LSNL2RoxFb5QGaevnJY828NupzTNdUd0sY\nJK3kRjKUggHWuB55hwJcH/Dx7I3DNH4NL68UAlK+VjwJkfYPrhq/bl5z8ZiurvBa\n5C1mDxhFpcTZlCfxQoas7D1d+uPACF6mEMbQNd3RaIaSREO50NvNywXIIt/OmCiR\nqI7JtOcn4eyh1I4j9WtlbMhRJLfwPMAgY5epTsWcURmhVofF2wVoFbib3JGCfA7t\nz/gmP5YoEKnf/cumKmF3e9LrZb8zwm7bTHUViwIDAQABAoIBAQCIv0XMjNvZS9DC\nXoXGQtVpcxj6dXfaiDgnc7hZDubsNCr3JtT5NqgdIYdVNQUABNDIPNEiCkzFjuwM\nuuF2+dRzM/x6UCs/cSsCjXYBCCOwMwV/fjpEJQnwMQqwTLulVsXZYYeSUtXVBf/8\n0tVULRty34apLFhsyX30UtboXQdESfpmm5ZsqsZJlYljw+M7JxRMneQclI19y/ya\nhPWlfhLB9OffVEJXGaWx1NSYnKoCMKqE/+4krROr6V62xXaNyX6WtU6XiT7C6R5A\nPBxfhmoeFdVCF6a+Qq0v2fKThYoZnV4sn2q2An9YPfynFYnlgzdfnAFSejsqxQd0\nfxYLOtMBAoGBAP1jxjHDJngZ1N+ymw9MIpRgr3HeuMP5phiSTbY2tu9lPzQd+TMX\nfhr1bQh2Fd/vU0u7X0yPnTWtUrLlCdGnWPpXivx95GNGgUUIk2HStFdrRx+f2Qvk\nG8vtLgmSbjQ26UiHzxi9Wa0a41PWIA3TixkcFrS2X29Qc4yd6pVHmicfAoGBANjR\nZ8aaDkSKLkq5Nk1T7I0E1+mtPoH1tPV/FJClXjJrvfDuYHBeOyUpipZddnZuPGWA\nIW2tFIsMgJQtgpvgs52NFI7pQGJRUPK/fTG+Ycocxo78TkLr/RIj8Kj5brXsbZ9P\n3/WBX5GAISTSp1ab8xVgK/Tm07hGupKVqnY2lCAVAoGAIql0YjhE2ecGtLcU+Qm8\nLTnwpg4GjmBnNTNGSCfB7IuYEsQK489R49Qw3xhwM5rkdRajmbCHm+Eiz+/+4NwY\nkt5I1/NMu7vYUR40MwyEuPSm3Q+bvEGu/71pL8wFIUVlshNJ5CN60fA8qqo+5kVK\n4Ntzy7Kq6WpC9Dhh75vE3ZcCgYEAty99uXtxsJD6+aEwcvcENkUwUztPQ6ggAwci\nje9Z/cmwCj6s9mN3HzfQ4qgGrZsHpk4ycCK655xhilBFOIQJ3YRUKUaDYk4H0YDe\nOsf6gTP8wtQDH2GZSNlavLk5w7UFDYQD2b47y4fw+NaOEYvjPl0p5lmb6ebAPZb8\nFbKZRd0CgYBC1HTbA+zMEqDdY4MWJJLC6jZsjdxOGhzjrCtWcIWEGMDF7oDDEoix\nW3j2hwm4C6vaNkH9XX1dr5+q6gq8vJQdbYoExl22BGMiNbfI3+sLRk0zBYL//W6c\ntSREgR4EjosqQfbkceLJ2JT1wuNjInI0eR9H3cRugvlDTeWtbdJ5qA==\n-----END RSA PRIVATE KEY-----' client_public_key='-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pt0ZoOuPEVPJJS+5r88\n4zcjZLkZZ2GcPwr79XOLDbOi46onCa79kjRnhS0VUK96SwUPS0z9J5mDA5LSNL2R\noxFb5QGaevnJY828NupzTNdUd0sYJK3kRjKUggHWuB55hwJcH/Dx7I3DNH4NL68U\nAlK+VjwJkfYPrhq/bl5z8ZiurvBa5C1mDxhFpcTZlCfxQoas7D1d+uPACF6mEMbQ\nNd3RaIaSREO50NvNywXIIt/OmCiRqI7JtOcn4eyh1I4j9WtlbMhRJLfwPMAgY5ep\nTsWcURmhVofF2wVoFbib3JGCfA7tz/gmP5YoEKnf/cumKmF3e9LrZb8zwm7bTHUV\niwIDAQAB\n-----END PUBLIC KEY-----' cst_transit_processor_client_secret=secret diff --git a/benefits/core/admin.py b/benefits/core/admin.py index 9c962b009..8d8a60a4f 100644 --- a/benefits/core/admin.py +++ b/benefits/core/admin.py @@ -60,8 +60,8 @@ def get_readonly_fields(self, request, obj=None): return super().get_readonly_fields(request, obj) -@admin.register(models.EligibilityVerifier) -class SortableEligibilityVerifierAdmin(SortableAdminMixin, admin.ModelAdmin): # pragma: no cover +@admin.register(models.EnrollmentFlow) +class SortableEnrollmentFlowAdmin(SortableAdminMixin, admin.ModelAdmin): # pragma: no cover def get_exclude(self, request, obj=None): if not request.user.is_superuser: return [ diff --git a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py new file mode 100644 index 000000000..d2549390b --- /dev/null +++ b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.7 on 2024-08-07 21:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0020_refactor_idg_config_eligibilityverifier"), + ] + + operations = [ + migrations.RenameModel( + old_name="EligibilityVerifier", + new_name="EnrollmentFlow", + ), + migrations.RenameField( + model_name="transitagency", + old_name="eligibility_verifiers", + new_name="enrollment_flows", + ), + ] diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 9ed727eea..269774172 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -84,7 +84,7 @@ } }, { - "model": "core.eligibilityverifier", + "model": "core.enrollmentflow", "pk": 1, "fields": { "name": "(CST) oauth claims via Login.gov", @@ -99,7 +99,7 @@ } }, { - "model": "core.eligibilityverifier", + "model": "core.enrollmentflow", "pk": 2, "fields": { "name": "(CST) VA.gov - veteran", @@ -114,15 +114,15 @@ } }, { - "model": "core.eligibilityverifier", + "model": "core.enrollmentflow", "pk": 3, "fields": { - "name": "(CST) eligibility server verifier", + "name": "(CST) eligibility server flow", "display_order": 4, "active": true, "eligibility_api_url": "http://server:8000/verify", "eligibility_api_auth_header": "X-Server-API-Key", - "eligibility_api_auth_key_secret_name": "agency-card-verifier-api-auth-key", + "eligibility_api_auth_key_secret_name": "agency-card-flow-api-auth-key", "eligibility_type": 3, "eligibility_api_public_key": 1, "eligibility_api_jwe_cek_enc": "A256CBC-HS512", @@ -136,7 +136,7 @@ } }, { - "model": "core.eligibilityverifier", + "model": "core.enrollmentflow", "pk": 4, "fields": { "name": "(CST) CalFresh oauth claims via Login.gov", @@ -168,7 +168,7 @@ "fields": { "active": true, "eligibility_types": [1, 2, 3, 4], - "eligibility_verifiers": [1, 2, 3, 4], + "enrollment_flows": [1, 2, 3, 4], "slug": "cst", "short_name": "CST (local)", "long_name": "California State Transit (local)", diff --git a/benefits/core/models.py b/benefits/core/models.py index 8980b32b5..da544a8d8 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -156,8 +156,8 @@ def clean(self): raise ValidationError(errors) -class EligibilityVerifier(models.Model): - """An entity that verifies eligibility.""" +class EnrollmentFlow(models.Model): + """Represents a user journey through the Benefits app for a single eligibility type.""" id = models.AutoField(primary_key=True) name = models.TextField() @@ -167,7 +167,7 @@ class EligibilityVerifier(models.Model): eligibility_api_auth_header = models.TextField(null=True, blank=True) eligibility_api_auth_key_secret_name = SecretNameField(null=True, blank=True) eligibility_type = models.ForeignKey(EligibilityType, on_delete=models.PROTECT) - # public key is used to encrypt requests targeted at this Verifier and to verify signed responses from this verifier + # public key is used to encrypt Eligibility API requests and to verify signed Eligibility API responses eligibility_api_public_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT, null=True, blank=True) # The JWE-compatible Content Encryption Key (CEK) key-length and mode eligibility_api_jwe_cek_enc = models.TextField(null=True, blank=True) @@ -178,7 +178,7 @@ class EligibilityVerifier(models.Model): claims_provider = models.ForeignKey(ClaimsProvider, on_delete=models.PROTECT, null=True, blank=True) selection_label_template = models.TextField() eligibility_start_template = models.TextField(default="eligibility/start.html") - # reference to a form class used by this Verifier, e.g. benefits.app.forms.FormClass + # reference to a form class used by this flow, e.g. benefits.eligibility.forms.FormClass eligibility_form_class = models.TextField(null=True, blank=True) eligibility_unverified_template = models.TextField(default="eligibility/unverified.html") help_template = models.TextField(null=True, blank=True) @@ -213,16 +213,16 @@ def eligibility_api_auth_key(self): @property def eligibility_api_public_key_data(self): - """This Verifier's public key as a string.""" + """This flow's Eligibility API public key as a string.""" return self.eligibility_api_public_key.data @property def uses_claims_verification(self): - """True if this Verifier verifies via the claims provider and has a scope and claim. False otherwise.""" + """True if this flow verifies via the claims provider and has a scope and claim. False otherwise.""" return self.claims_provider is not None and bool(self.claims_scope) and bool(self.claims_claim) def eligibility_form_instance(self, *args, **kwargs): - """Return an instance of this verifier's form, or None.""" + """Return an instance of this flow's EligibilityForm, or None.""" if not bool(self.eligibility_form_class): return None @@ -234,9 +234,9 @@ def eligibility_form_instance(self, *args, **kwargs): @staticmethod def by_id(id): - """Get an EligibilityVerifier instance by its ID.""" - logger.debug(f"Get {EligibilityVerifier.__name__} by id: {id}") - return EligibilityVerifier.objects.get(id=id) + """Get an EnrollmentFlow instance by its ID.""" + logger.debug(f"Get {EnrollmentFlow.__name__} by id: {id}") + return EnrollmentFlow.objects.get(id=id) @property def claims_scheme(self): @@ -269,7 +269,7 @@ class TransitAgency(models.Model): id = models.AutoField(primary_key=True) active = models.BooleanField(default=False, help_text="Determines if this Agency is enabled for users") eligibility_types = models.ManyToManyField(EligibilityType) - eligibility_verifiers = models.ManyToManyField(EligibilityVerifier) + enrollment_flows = models.ManyToManyField(EnrollmentFlow) slug = models.TextField(help_text="Used for URL navigation for this agency, e.g. the agency homepage url is /{slug}") short_name = models.TextField(help_text="The user-facing short name for this agency. Often an uppercase acronym.") long_name = models.TextField( @@ -322,17 +322,17 @@ def supports_type(self, eligibility_type): """True if the eligibility_type is one of this agency's types. False otherwise.""" return isinstance(eligibility_type, EligibilityType) and eligibility_type in self.eligibility_types.all() - def types_to_verify(self, eligibility_verifier): + def types_to_verify(self, flow: EnrollmentFlow): """List of eligibility types to verify for this agency.""" - # compute set intersection of agency and verifier type ids + # compute set intersection of agency and flow type ids agency_types = set(self.eligibility_types.values_list("id", flat=True)) - verifier_types = {eligibility_verifier.eligibility_type.id} - supported_types = list(agency_types & verifier_types) + flow_types = {flow.eligibility_type.id} + supported_types = list(agency_types & flow_types) return EligibilityType.get_many(supported_types) - def type_names_to_verify(self, verifier): + def type_names_to_verify(self, flow: EnrollmentFlow): """List of names of the eligibility types to check for this agency.""" - return EligibilityType.get_names(self.types_to_verify(verifier)) + return EligibilityType.get_names(self.types_to_verify(flow)) @property def index_url(self): diff --git a/benefits/oauth/client.py b/benefits/oauth/client.py index 651f99175..2eea46b5f 100644 --- a/benefits/oauth/client.py +++ b/benefits/oauth/client.py @@ -6,6 +6,8 @@ from authlib.integrations.django_client import OAuth +from benefits.core import models + logger = logging.getLogger(__name__) oauth = OAuth() @@ -39,32 +41,32 @@ def _authorize_params(scheme): return params -def _register_provider(oauth_registry, verifier): +def _register_provider(oauth_registry: OAuth, flow: models.EnrollmentFlow): """ - Register OAuth clients into the given registry, using configuration from ClaimsProvider and EligibilityVerifier models. + Register OAuth clients into the given registry, using configuration from ClaimsProvider and EnrollmentFlow models. Adapted from https://stackoverflow.com/a/64174413. """ - logger.debug(f"Registering OAuth client: {verifier.claims_provider.client_name}") + logger.debug(f"Registering OAuth client: {flow.claims_provider.client_name}") client = oauth_registry.register( - verifier.claims_provider.client_name, - client_id=verifier.claims_provider.client_id, - server_metadata_url=_server_metadata_url(verifier.claims_provider.authority), - client_kwargs=_client_kwargs(verifier.claims_scope), - authorize_params=_authorize_params(verifier.claims_scheme), + flow.claims_provider.client_name, + client_id=flow.claims_provider.client_id, + server_metadata_url=_server_metadata_url(flow.claims_provider.authority), + client_kwargs=_client_kwargs(flow.claims_scope), + authorize_params=_authorize_params(flow.claims_scheme), ) return client -def create_client(oauth_registry, verifier): +def create_client(oauth_registry: OAuth, flow: models.EnrollmentFlow): """ Returns an OAuth client, registering it if needed. """ - client = oauth_registry.create_client(verifier.claims_provider.client_name) + client = oauth_registry.create_client(flow.claims_provider.client_name) if client is None: - client = _register_provider(oauth_registry, verifier) + client = _register_provider(oauth_registry, flow) return client diff --git a/benefits/oauth/views.py b/benefits/oauth/views.py index 1171320ff..4686e9df2 100644 --- a/benefits/oauth/views.py +++ b/benefits/oauth/views.py @@ -6,7 +6,7 @@ from django.utils.decorators import decorator_from_middleware import sentry_sdk -from benefits.core import session +from benefits.core import models, session from benefits.core.middleware import AgencySessionRequired from . import analytics, redirects from .client import oauth, create_client @@ -24,7 +24,7 @@ TEMPLATE_SYSTEM_ERROR = "oauth/system_error.html" -def _oauth_client_or_error_redirect(request, verifier): +def _oauth_client_or_error_redirect(request, flow: models.EnrollmentFlow): """Calls `benefits.oauth.client.create_client()`. If a client is created successfully, return it; Otherwise, return a redirect response to the `oauth:system-error` route. @@ -34,12 +34,12 @@ def _oauth_client_or_error_redirect(request, verifier): exception = None try: - oauth_client = create_client(oauth, verifier) + oauth_client = create_client(oauth, flow) except Exception as ex: exception = ex if not oauth_client and not exception: - exception = Exception(f"oauth_client not registered: {verifier.claims_provider.client_name}") + exception = Exception(f"oauth_client not registered: {flow.claims_provider.client_name}") if exception: analytics.error(request, message=str(exception), operation="init") diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 0bf0d6aac..1b3bee951 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -7,7 +7,7 @@ from pytest_socket import disable_socket from benefits.core import session -from benefits.core.models import ClaimsProvider, EligibilityType, EligibilityVerifier, TransitProcessor, PemData, TransitAgency +from benefits.core.models import ClaimsProvider, EligibilityType, EnrollmentFlow, TransitProcessor, PemData, TransitAgency def pytest_runtest_setup(): @@ -121,9 +121,9 @@ def model_EligibilityType_supports_expiration(model_EligibilityType): @pytest.fixture -def model_EligibilityVerifier(model_PemData, model_EligibilityType, model_ClaimsProvider): - verifier = EligibilityVerifier.objects.create( - name="Test Verifier", +def model_EnrollmentFlow(model_PemData, model_EligibilityType, model_ClaimsProvider): + flow = EnrollmentFlow.objects.create( + name="Test Flow", active=True, eligibility_api_url="https://example.com/verify", eligibility_api_auth_header="X-API-AUTH", @@ -135,24 +135,24 @@ def model_EligibilityVerifier(model_PemData, model_EligibilityType, model_Claims claims_scheme_override="", ) - return verifier + return flow @pytest.fixture -def model_EligibilityVerifier_with_scope_and_claim(model_EligibilityVerifier): - model_EligibilityVerifier.claims_scope = "scope" - model_EligibilityVerifier.claims_claim = "claim" - model_EligibilityVerifier.save() +def model_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow): + model_EnrollmentFlow.claims_scope = "scope" + model_EnrollmentFlow.claims_claim = "claim" + model_EnrollmentFlow.save() - return model_EligibilityVerifier + return model_EnrollmentFlow @pytest.fixture -def model_EligibilityVerifier_with_claims_scheme(model_EligibilityVerifier): - model_EligibilityVerifier.claims_scheme_override = "scheme" - model_EligibilityVerifier.save() +def model_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow): + model_EnrollmentFlow.claims_scheme_override = "scheme" + model_EnrollmentFlow.save() - return model_EligibilityVerifier + return model_EnrollmentFlow @pytest.fixture @@ -169,7 +169,7 @@ def model_TransitProcessor(): @pytest.fixture -def model_TransitAgency(model_PemData, model_EligibilityType, model_EligibilityVerifier, model_TransitProcessor): +def model_TransitAgency(model_PemData, model_EligibilityType, model_EnrollmentFlow, model_TransitProcessor): agency = TransitAgency.objects.create( slug="test", short_name="TEST", @@ -191,7 +191,7 @@ def model_TransitAgency(model_PemData, model_EligibilityType, model_EligibilityV # add many-to-many relationships after creation, need ID on both sides agency.eligibility_types.add(model_EligibilityType) - agency.eligibility_verifiers.add(model_EligibilityVerifier) + agency.enrollment_flows.add(model_EnrollmentFlow) agency.save() return agency @@ -251,53 +251,39 @@ def mocked_session_enrollment_expiry(mocker): @pytest.fixture -def mocked_session_verifier(mocker, model_EligibilityVerifier): - return mocker.patch("benefits.core.session.verifier", autospec=True, return_value=model_EligibilityVerifier) +def mocked_session_verifier(mocker, model_EnrollmentFlow): + return mocker.patch("benefits.core.session.verifier", autospec=True, return_value=model_EnrollmentFlow) @pytest.fixture -def mocked_session_verifier_oauth(mocker, model_EligibilityVerifier_with_scope_and_claim): +def mocked_session_verifier_oauth(mocker, model_EnrollmentFlow_with_scope_and_claim): return mocker.patch( "benefits.core.session.verifier", autospec=True, - return_value=model_EligibilityVerifier_with_scope_and_claim, + return_value=model_EnrollmentFlow_with_scope_and_claim, ) @pytest.fixture -def mocked_session_verifier_no_scope_and_claim_oauth(mocker, model_EligibilityVerifier): +def mocked_session_verifier_no_scope_and_claim_oauth(mocker, model_EnrollmentFlow): return mocker.patch( "benefits.core.session.verifier", autospec=True, - return_value=model_EligibilityVerifier, + return_value=model_EnrollmentFlow, ) @pytest.fixture -def mocked_session_verifier_uses_claims_verification( - model_EligibilityVerifier_with_scope_and_claim, mocked_session_verifier_oauth -): - mock_verifier = model_EligibilityVerifier_with_scope_and_claim - mock_verifier.name = model_EligibilityVerifier_with_scope_and_claim.name - mock_verifier.claims_provider.sign_out_button_template = ( - model_EligibilityVerifier_with_scope_and_claim.claims_provider.sign_out_button_template - ) - mock_verifier.claims_provider.sign_out_link_template = ( - model_EligibilityVerifier_with_scope_and_claim.claims_provider.sign_out_link_template - ) - mocked_session_verifier_oauth.return_value = mock_verifier +def mocked_session_verifier_uses_claims_verification(model_EnrollmentFlow_with_scope_and_claim, mocked_session_verifier_oauth): + mocked_session_verifier_oauth.return_value = model_EnrollmentFlow_with_scope_and_claim return mocked_session_verifier_oauth @pytest.fixture def mocked_session_verifier_does_not_use_claims_verification( - model_EligibilityVerifier, mocked_session_verifier_no_scope_and_claim_oauth + model_EnrollmentFlow, mocked_session_verifier_no_scope_and_claim_oauth ): - mock_verifier = model_EligibilityVerifier - mock_verifier.name = model_EligibilityVerifier.name - mock_verifier.claims_provider.sign_out_button_template = model_EligibilityVerifier.claims_provider.sign_out_button_template - mock_verifier.claims_provider.sign_out_link_template = model_EligibilityVerifier.claims_provider.sign_out_link_template - mocked_session_verifier_no_scope_and_claim_oauth.return_value = mock_verifier + mocked_session_verifier_no_scope_and_claim_oauth.return_value = model_EnrollmentFlow return mocked_session_verifier_no_scope_and_claim_oauth diff --git a/tests/pytest/core/test_middleware_login_required.py b/tests/pytest/core/test_middleware_login_required.py index c8ef7e456..82f69dea0 100644 --- a/tests/pytest/core/test_middleware_login_required.py +++ b/tests/pytest/core/test_middleware_login_required.py @@ -16,7 +16,7 @@ def decorated_view(mocked_view): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_verifier_oauth") -def test_login_verifier_uses_claims_verification(app_request, mocked_view, decorated_view): +def test_login_flow_uses_claims_verification(app_request, mocked_view, decorated_view): response = decorated_view(app_request) mocked_view.assert_not_called() @@ -26,10 +26,10 @@ def test_login_verifier_uses_claims_verification(app_request, mocked_view, decor @pytest.mark.django_db -def test_login_verifier_does_not_use_claims_verification(app_request, model_EligibilityVerifier, mocked_view, decorated_view): - model_EligibilityVerifier.claims_provider = None - assert not model_EligibilityVerifier.uses_claims_verification - session.update(app_request, verifier=model_EligibilityVerifier) +def test_login_flow_does_not_use_claims_verification(app_request, model_EnrollmentFlow, mocked_view, decorated_view): + model_EnrollmentFlow.claims_provider = None + assert not model_EnrollmentFlow.uses_claims_verification + session.update(app_request, verifier=model_EnrollmentFlow) decorated_view(app_request) diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index b01797c0e..5738d666c 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -3,7 +3,7 @@ import pytest -from benefits.core.models import SecretNameField, EligibilityType, EligibilityVerifier, TransitAgency +from benefits.core.models import SecretNameField, EligibilityType, EnrollmentFlow, TransitAgency import benefits.secrets @@ -167,8 +167,8 @@ def test_EligibilityType_get_names(model_EligibilityType): @pytest.mark.django_db -def test_EligibilityVerifier_str(model_EligibilityVerifier): - assert str(model_EligibilityVerifier) == model_EligibilityVerifier.name +def test_EnrollmentFlow_str(model_EnrollmentFlow): + assert str(model_EnrollmentFlow) == model_EnrollmentFlow.name @pytest.mark.django_db @@ -247,18 +247,18 @@ def __init__(self, *args, **kwargs): @pytest.mark.django_db -def test_EligibilityVerifier_eligibility_start_template(model_EligibilityVerifier): - assert model_EligibilityVerifier.eligibility_start_template == "eligibility/start.html" +def test_EnrollmentFlow_eligibility_start_template(model_EnrollmentFlow): + assert model_EnrollmentFlow.eligibility_start_template == "eligibility/start.html" @pytest.mark.django_db -def test_EligibilityVerifier_eligibility_form_instance(model_EligibilityVerifier): - model_EligibilityVerifier.eligibility_form_class = f"{__name__}.SampleFormClass" - model_EligibilityVerifier.save() +def test_EnrollmentFlow_eligibility_form_instance(model_EnrollmentFlow): + model_EnrollmentFlow.eligibility_form_class = f"{__name__}.SampleFormClass" + model_EnrollmentFlow.save() args = (1, "2") kwargs = {"one": 1, "two": "2"} - form_instance = model_EligibilityVerifier.eligibility_form_instance(*args, **kwargs) + form_instance = model_EnrollmentFlow.eligibility_form_instance(*args, **kwargs) assert isinstance(form_instance, SampleFormClass) assert form_instance.args == args @@ -266,69 +266,62 @@ def test_EligibilityVerifier_eligibility_form_instance(model_EligibilityVerifier @pytest.mark.django_db -def test_EligibilityVerifier_by_id_matching(model_EligibilityVerifier): - verifier = EligibilityVerifier.by_id(model_EligibilityVerifier.id) +def test_EnrollmentFlow_by_id_matching(model_EnrollmentFlow): + flow = EnrollmentFlow.by_id(model_EnrollmentFlow.id) - assert verifier == model_EligibilityVerifier + assert flow == model_EnrollmentFlow @pytest.mark.django_db -def test_EligibilityVerifier_by_id_nonmatching(): - with pytest.raises(EligibilityVerifier.DoesNotExist): - EligibilityVerifier.by_id(99999) +def test_EnrollmentFlow_by_id_nonmatching(): + with pytest.raises(EnrollmentFlow.DoesNotExist): + EnrollmentFlow.by_id(99999) @pytest.mark.django_db -def test_EligibilityVerifier_with_scope_and_claim(model_EligibilityVerifier_with_scope_and_claim): +def test_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow_with_scope_and_claim): - assert model_EligibilityVerifier_with_scope_and_claim.uses_claims_verification + assert model_EnrollmentFlow_with_scope_and_claim.uses_claims_verification @pytest.mark.django_db -def test_EligibilityVerifier_with_scope_and_claim_no_sign_out( - model_EligibilityVerifier_with_scope_and_claim, model_ClaimsProvider_no_sign_out +def test_EnrollmentFlow_with_scope_and_claim_no_sign_out( + model_EnrollmentFlow_with_scope_and_claim, model_ClaimsProvider_no_sign_out ): - model_EligibilityVerifier_with_scope_and_claim.claims_provider = model_ClaimsProvider_no_sign_out + model_EnrollmentFlow_with_scope_and_claim.claims_provider = model_ClaimsProvider_no_sign_out - assert model_EligibilityVerifier_with_scope_and_claim.uses_claims_verification + assert model_EnrollmentFlow_with_scope_and_claim.uses_claims_verification @pytest.mark.django_db -def test_EligibilityVerifier_no_scope_and_claim(model_EligibilityVerifier): +def test_EnrollmentFlow_no_scope_and_claim(model_EnrollmentFlow): - assert not model_EligibilityVerifier.uses_claims_verification + assert not model_EnrollmentFlow.uses_claims_verification @pytest.mark.django_db -def test_EligibilityVerifier_no_scope_and_claim_no_sign_out(model_EligibilityVerifier, model_ClaimsProvider_no_sign_out): - model_EligibilityVerifier.claims_provider = model_ClaimsProvider_no_sign_out +def test_EnrollmentFlow_no_scope_and_claim_no_sign_out(model_EnrollmentFlow, model_ClaimsProvider_no_sign_out): + model_EnrollmentFlow.claims_provider = model_ClaimsProvider_no_sign_out - assert not model_EligibilityVerifier.uses_claims_verification + assert not model_EnrollmentFlow.uses_claims_verification @pytest.mark.django_db -def test_EligibilityVerifier_no_ClaimsProvider(model_EligibilityVerifier): - model_EligibilityVerifier.claims_provider = None +def test_EnrollmentFlow_eligibility_api_auth_key(model_EnrollmentFlow, mock_models_get_secret_by_name): + secret_value = model_EnrollmentFlow.eligibility_api_auth_key - assert not model_EligibilityVerifier.uses_claims_verification - - -@pytest.mark.django_db -def test_EligiblityVerifier_eligibility_api_auth_key(model_EligibilityVerifier, mock_models_get_secret_by_name): - secret_value = model_EligibilityVerifier.eligibility_api_auth_key - - mock_models_get_secret_by_name.assert_called_once_with(model_EligibilityVerifier.eligibility_api_auth_key_secret_name) + mock_models_get_secret_by_name.assert_called_once_with(model_EnrollmentFlow.eligibility_api_auth_key_secret_name) assert secret_value == mock_models_get_secret_by_name.return_value @pytest.mark.django_db -def test_EligibilityVerifier_no_claims_scheme(model_EligibilityVerifier): - assert model_EligibilityVerifier.claims_scheme == model_EligibilityVerifier.claims_provider.scheme +def test_EnrollmentFlow_no_claims_scheme(model_EnrollmentFlow): + assert model_EnrollmentFlow.claims_scheme == model_EnrollmentFlow.claims_provider.scheme @pytest.mark.django_db -def test_EligibilityVerifier_with_claims_scheme(model_EligibilityVerifier_with_claims_scheme): - assert model_EligibilityVerifier_with_claims_scheme.claims_scheme == "scheme" +def test_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow_with_claims_scheme): + assert model_EnrollmentFlow_with_claims_scheme.claims_scheme == "scheme" @pytest.mark.django_db @@ -342,10 +335,10 @@ def test_TransitAgency_str(model_TransitAgency): @pytest.mark.django_db -def test_TransitAgency_active_verifiers(model_TransitAgency, model_EligibilityVerifier): +def test_TransitAgency_active_verifiers(model_TransitAgency, model_EnrollmentFlow): # add another to the list of verifiers by cloning the original # https://stackoverflow.com/a/48149675/453168 - new_verifier = EligibilityVerifier.objects.get(pk=model_EligibilityVerifier.id) + new_verifier = EnrollmentFlow.objects.get(pk=model_EnrollmentFlow.id) new_verifier.pk = None new_verifier.active = False new_verifier.save() @@ -355,7 +348,7 @@ def test_TransitAgency_active_verifiers(model_TransitAgency, model_EligibilityVe assert model_TransitAgency.eligibility_verifiers.count() == 2 assert model_TransitAgency.active_verifiers.count() == 1 - assert model_TransitAgency.active_verifiers[0] == model_EligibilityVerifier + assert model_TransitAgency.active_verifiers[0] == model_EnrollmentFlow @pytest.mark.django_db @@ -420,19 +413,19 @@ def test_TransitAgency_types_to_verify(model_TransitAgency): model_TransitAgency.eligibility_types.add(new_eligibility) assert model_TransitAgency.eligibility_types.count() == 2 - verifier = model_TransitAgency.eligibility_verifiers.first() - assert verifier.eligibility_type == eligibility + flow = model_TransitAgency.enrollment_flows.first() + assert flow.eligibility_type == eligibility - result = model_TransitAgency.types_to_verify(verifier) + result = model_TransitAgency.types_to_verify(flow) assert len(result) == 1 assert eligibility in result @pytest.mark.django_db -def test_TransitAgency_type_names_to_verify(model_TransitAgency, model_EligibilityVerifier): - expected = [t.name for t in model_TransitAgency.types_to_verify(model_EligibilityVerifier)] +def test_TransitAgency_type_names_to_verify(model_TransitAgency, model_EnrollmentFlow): + expected = [t.name for t in model_TransitAgency.types_to_verify(model_EnrollmentFlow)] - result = model_TransitAgency.type_names_to_verify(model_EligibilityVerifier) + result = model_TransitAgency.type_names_to_verify(model_EnrollmentFlow) assert result == expected diff --git a/tests/pytest/core/test_views.py b/tests/pytest/core/test_views.py index fb544a659..dafdddee9 100644 --- a/tests/pytest/core/test_views.py +++ b/tests/pytest/core/test_views.py @@ -2,7 +2,7 @@ import pytest -from benefits.core.models import EligibilityVerifier, TransitAgency +from benefits.core.models import EnrollmentFlow, TransitAgency import benefits.core.session from benefits.core.views import ( ROUTE_HELP, @@ -63,7 +63,7 @@ def test_index_single_agency(mocker, model_TransitAgency, client, session_reset_ @pytest.mark.django_db -def test_agency_index_single_verifier(mocker, model_TransitAgency, client, session_reset_spy, mocked_session_update): +def test_agency_index_single_flow(mocker, model_TransitAgency, client, session_reset_spy, mocked_session_update): mocker.patch("benefits.core.models.TransitAgency.all_active", return_value=[model_TransitAgency]) response = client.get(model_TransitAgency.index_url) @@ -76,16 +76,16 @@ def test_agency_index_single_verifier(mocker, model_TransitAgency, client, sessi @pytest.mark.django_db -def test_agency_index_multiple_verifier( - mocker, model_TransitAgency, model_EligibilityVerifier, client, session_reset_spy, mocked_session_update +def test_agency_index_multiple_flow( + mocker, model_TransitAgency, model_EnrollmentFlow, client, session_reset_spy, mocked_session_update ): - # add another to the list of verifiers by cloning the original + # add another to the list of flows by cloning the original # https://stackoverflow.com/a/48149675/453168 - new_verifier = EligibilityVerifier.objects.get(pk=model_EligibilityVerifier.id) - new_verifier.pk = None - new_verifier.save() + new_flow = EnrollmentFlow.objects.get(pk=model_EnrollmentFlow.id) + new_flow.pk = None + new_flow.save() - model_TransitAgency.eligibility_verifiers.add(new_verifier) + model_TransitAgency.enrollment_flows.add(new_flow) mocker.patch("benefits.core.models.TransitAgency.all_active", return_value=[model_TransitAgency]) response = client.get(model_TransitAgency.index_url) diff --git a/tests/pytest/enrollment/test_views.py b/tests/pytest/enrollment/test_views.py index 8d9d6082b..da088b884 100644 --- a/tests/pytest/enrollment/test_views.py +++ b/tests/pytest/enrollment/test_views.py @@ -829,7 +829,7 @@ def test_retry_valid_form(client, mocked_analytics_module): @pytest.mark.django_db -def test_success_no_verifier(client): +def test_success_no_flow(client): path = reverse(ROUTE_SUCCESS) response = client.get(path) diff --git a/tests/pytest/oauth/test_client.py b/tests/pytest/oauth/test_client.py index 63918bbdf..597f07bca 100644 --- a/tests/pytest/oauth/test_client.py +++ b/tests/pytest/oauth/test_client.py @@ -1,6 +1,6 @@ import pytest -from benefits.core.models import EligibilityVerifier +from benefits.core.models import EnrollmentFlow from benefits.oauth.client import _client_kwargs, _server_metadata_url, _authorize_params, _register_provider, create_client @@ -40,15 +40,15 @@ def test_authorize_params_no_scheme(): @pytest.mark.django_db def test_register_provider(mocker, mocked_oauth_registry): - mocked_client_provider = mocker.Mock(spec=EligibilityVerifier) - mocked_client_provider.claims_provider.client_name = "client_name_1" - mocked_client_provider.claims_provider.client_id = "client_id_1" + mocked_flow = mocker.Mock(spec=EnrollmentFlow) + mocked_flow.claims_provider.client_name = "client_name_1" + mocked_flow.claims_provider.client_id = "client_id_1" mocker.patch("benefits.oauth.client._client_kwargs", return_value={"client": "kwargs"}) mocker.patch("benefits.oauth.client._server_metadata_url", return_value="https://metadata.url") mocker.patch("benefits.oauth.client._authorize_params", return_value={"scheme": "test_scheme"}) - _register_provider(mocked_oauth_registry, mocked_client_provider) + _register_provider(mocked_oauth_registry, mocked_flow) mocked_oauth_registry.register.assert_any_call( "client_name_1", @@ -61,11 +61,11 @@ def test_register_provider(mocker, mocked_oauth_registry): @pytest.mark.django_db def test_create_client_already_registered(mocker, mocked_oauth_registry): - mocked_client_provider = mocker.Mock(spec=EligibilityVerifier) - mocked_client_provider.claims_provider.client_name = "client_name_1" - mocked_client_provider.claims_provider.client_id = "client_id_1" + mocked_flow = mocker.Mock(spec=EnrollmentFlow) + mocked_flow.claims_provider.client_name = "client_name_1" + mocked_flow.claims_provider.client_id = "client_id_1" - create_client(mocked_oauth_registry, mocked_client_provider) + create_client(mocked_oauth_registry, mocked_flow) mocked_oauth_registry.create_client.assert_any_call("client_name_1") mocked_oauth_registry.register.assert_not_called() @@ -73,9 +73,9 @@ def test_create_client_already_registered(mocker, mocked_oauth_registry): @pytest.mark.django_db def test_create_client_already_not_registered_yet(mocker, mocked_oauth_registry): - mocked_client_provider = mocker.Mock(spec=EligibilityVerifier) - mocked_client_provider.claims_provider.client_name = "client_name_1" - mocked_client_provider.claims_provider.client_id = "client_id_1" + mocked_flow = mocker.Mock(spec=EnrollmentFlow) + mocked_flow.claims_provider.client_name = "client_name_1" + mocked_flow.claims_provider.client_id = "client_id_1" mocker.patch("benefits.oauth.client._client_kwargs", return_value={"client": "kwargs"}) mocker.patch("benefits.oauth.client._server_metadata_url", return_value="https://metadata.url") @@ -83,7 +83,7 @@ def test_create_client_already_not_registered_yet(mocker, mocked_oauth_registry) mocked_oauth_registry.create_client.return_value = None - create_client(mocked_oauth_registry, mocked_client_provider) + create_client(mocked_oauth_registry, mocked_flow) mocked_oauth_registry.create_client.assert_any_call("client_name_1") mocked_oauth_registry.register.assert_any_call( diff --git a/tests/pytest/oauth/test_views.py b/tests/pytest/oauth/test_views.py index fe01411a6..b35c76934 100644 --- a/tests/pytest/oauth/test_views.py +++ b/tests/pytest/oauth/test_views.py @@ -52,14 +52,14 @@ def mocked_oauth_client_or_error_redirect__error(mocked_oauth_create_client): @pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") def test_oauth_client_or_error_redirect_no_oauth_client( app_request, - model_EligibilityVerifier_with_scope_and_claim, + model_EnrollmentFlow_with_scope_and_claim, mocked_oauth_create_client, mocked_analytics_module, mocked_sentry_sdk_module, ): mocked_oauth_create_client.return_value = None - result = _oauth_client_or_error_redirect(app_request, model_EligibilityVerifier_with_scope_and_claim) + result = _oauth_client_or_error_redirect(app_request, model_EnrollmentFlow_with_scope_and_claim) assert result.status_code == 302 assert result.url == reverse(ROUTE_SYSTEM_ERROR) @@ -70,9 +70,9 @@ def test_oauth_client_or_error_redirect_no_oauth_client( @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") def test_oauth_client_or_error_redirect_oauth_client_exception( - app_request, model_EligibilityVerifier_with_scope_and_claim, mocked_analytics_module, mocked_sentry_sdk_module + app_request, model_EnrollmentFlow_with_scope_and_claim, mocked_analytics_module, mocked_sentry_sdk_module ): - result = _oauth_client_or_error_redirect(app_request, model_EligibilityVerifier_with_scope_and_claim) + result = _oauth_client_or_error_redirect(app_request, model_EnrollmentFlow_with_scope_and_claim) assert result.status_code == 302 assert result.url == reverse(ROUTE_SYSTEM_ERROR) @@ -83,9 +83,9 @@ def test_oauth_client_or_error_redirect_oauth_client_exception( @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_create_client") def test_oauth_client_or_error_oauth_client( - app_request, model_EligibilityVerifier_with_scope_and_claim, mocked_analytics_module, mocked_sentry_sdk_module + app_request, model_EnrollmentFlow_with_scope_and_claim, mocked_analytics_module, mocked_sentry_sdk_module ): - result = _oauth_client_or_error_redirect(app_request, model_EligibilityVerifier_with_scope_and_claim) + result = _oauth_client_or_error_redirect(app_request, model_EnrollmentFlow_with_scope_and_claim) assert hasattr(result, "authorize_redirect") mocked_analytics_module.error.assert_not_called() From 0b4c3bf2916a92fd7dddf28d27d28414fe6861e5 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 7 Aug 2024 22:06:08 +0000 Subject: [PATCH 2/8] refactor(models): remove active flag from EnrollmentFlow --- benefits/core/context_processors.py | 4 ++-- ...rename_eligibilityverifier_enrollmentflow.py | 4 ++++ benefits/core/migrations/local_fixtures.json | 4 ---- benefits/core/models.py | 6 ------ benefits/eligibility/forms.py | 2 +- tests/pytest/conftest.py | 1 - tests/pytest/core/test_models.py | 17 ----------------- tests/pytest/core/test_views.py | 5 ++--- 8 files changed, 9 insertions(+), 34 deletions(-) diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 79fdffe01..40a9cea8e 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -12,10 +12,10 @@ def unique_values(original_list): return list(dict.fromkeys(original_list)) -def _agency_context(agency): +def _agency_context(agency: models.TransitAgency): return { "eligibility_index_url": agency.eligibility_index_url, - "help_templates": unique_values([v.help_template for v in agency.active_verifiers if v.help_template]), + "help_templates": unique_values([f.help_template for f in agency.enrollment_flows.all() if f.help_template]), "info_url": agency.info_url, "long_name": agency.long_name, "phone": agency.phone, diff --git a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py index d2549390b..10917f04f 100644 --- a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py +++ b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py @@ -19,4 +19,8 @@ class Migration(migrations.Migration): old_name="eligibility_verifiers", new_name="enrollment_flows", ), + migrations.RemoveField( + model_name="enrollmentflow", + name="active", + ), ] diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 269774172..936b76c22 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -89,7 +89,6 @@ "fields": { "name": "(CST) oauth claims via Login.gov", "display_order": 1, - "active": true, "eligibility_type": 1, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--senior.html", @@ -104,7 +103,6 @@ "fields": { "name": "(CST) VA.gov - veteran", "display_order": 3, - "active": true, "eligibility_type": 2, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--veteran.html", @@ -119,7 +117,6 @@ "fields": { "name": "(CST) eligibility server flow", "display_order": 4, - "active": true, "eligibility_api_url": "http://server:8000/verify", "eligibility_api_auth_header": "X-Server-API-Key", "eligibility_api_auth_key_secret_name": "agency-card-flow-api-auth-key", @@ -141,7 +138,6 @@ "fields": { "name": "(CST) CalFresh oauth claims via Login.gov", "display_order": 2, - "active": true, "eligibility_type": 4, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--calfresh.html", diff --git a/benefits/core/models.py b/benefits/core/models.py index da544a8d8..1d7d6c39c 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -162,7 +162,6 @@ class EnrollmentFlow(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() display_order = models.PositiveSmallIntegerField(default=0, blank=False, null=False) - active = models.BooleanField(default=False) eligibility_api_url = models.TextField(null=True, blank=True) eligibility_api_auth_header = models.TextField(null=True, blank=True) eligibility_api_auth_key_secret_name = SecretNameField(null=True, blank=True) @@ -359,11 +358,6 @@ def eligibility_api_public_key_data(self): """This Agency's public key as a string.""" return self.eligibility_api_public_key.data - @property - def active_verifiers(self): - """This Agency's eligibility verifiers that are active.""" - return self.eligibility_verifiers.filter(active=True) - @property def transit_processor_client_secret(self): return get_secret_by_name(self.transit_processor_client_secret_name) diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index d239f213b..e0229b5b4 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -25,7 +25,7 @@ class EligibilityVerifierSelectionForm(forms.Form): def __init__(self, agency: models.TransitAgency, *args, **kwargs): super().__init__(*args, **kwargs) - verifiers = agency.active_verifiers + verifiers = agency.enrollment_flows.all() self.classes = "col-lg-8" # second element is not used since we render the whole label using selection_label_template, diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 1b3bee951..ee01bf2bd 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -124,7 +124,6 @@ def model_EligibilityType_supports_expiration(model_EligibilityType): def model_EnrollmentFlow(model_PemData, model_EligibilityType, model_ClaimsProvider): flow = EnrollmentFlow.objects.create( name="Test Flow", - active=True, eligibility_api_url="https://example.com/verify", eligibility_api_auth_header="X-API-AUTH", eligibility_api_auth_key_secret_name="secret-key", diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 5738d666c..aea57754e 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -334,23 +334,6 @@ def test_TransitAgency_str(model_TransitAgency): assert str(model_TransitAgency) == model_TransitAgency.long_name -@pytest.mark.django_db -def test_TransitAgency_active_verifiers(model_TransitAgency, model_EnrollmentFlow): - # add another to the list of verifiers by cloning the original - # https://stackoverflow.com/a/48149675/453168 - new_verifier = EnrollmentFlow.objects.get(pk=model_EnrollmentFlow.id) - new_verifier.pk = None - new_verifier.active = False - new_verifier.save() - - model_TransitAgency.eligibility_verifiers.add(new_verifier) - - assert model_TransitAgency.eligibility_verifiers.count() == 2 - assert model_TransitAgency.active_verifiers.count() == 1 - - assert model_TransitAgency.active_verifiers[0] == model_EnrollmentFlow - - @pytest.mark.django_db def test_TransitAgency_get_type_id_matching(model_TransitAgency): eligibility = model_TransitAgency.eligibility_types.first() diff --git a/tests/pytest/core/test_views.py b/tests/pytest/core/test_views.py index dafdddee9..5f5aa31c9 100644 --- a/tests/pytest/core/test_views.py +++ b/tests/pytest/core/test_views.py @@ -25,9 +25,8 @@ def session_reset_spy(mocker): def mocked_active_agency(mocker): mock_agency = mocker.Mock() - # ensure agency.eligibility_verifiers is iterable - eligibility_verifiers = mocker.MagicMock() - mock_agency.active_verifiers = eligibility_verifiers + # ensure agency.enrollment_flows is iterable + mock_agency.enrollment_flows = mocker.MagicMock() mock_agency.index_url = "/agency" mocker.patch("benefits.core.session.agency", return_value=mock_agency) From 0720ffba509f29b0765540891e220cbd8b4ed462 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 7 Aug 2024 22:48:11 +0000 Subject: [PATCH 3/8] refactor(session): rename verifier -> flow --- benefits/core/analytics.py | 4 +- benefits/core/context_processors.py | 10 +-- benefits/core/middleware.py | 16 ++--- benefits/core/session.py | 20 +++--- benefits/eligibility/views.py | 32 ++++----- benefits/enrollment/views.py | 12 ++-- benefits/oauth/analytics.py | 2 +- benefits/oauth/middleware.py | 24 +++---- benefits/oauth/views.py | 34 ++++----- tests/pytest/conftest.py | 30 ++------ tests/pytest/core/test_analytics.py | 2 +- .../core/test_middleware_login_required.py | 6 +- .../core/test_middleware_verifier_required.py | 10 +-- tests/pytest/core/test_session.py | 18 ++--- tests/pytest/eligibility/conftest.py | 4 +- tests/pytest/eligibility/test_verify.py | 31 ++++---- tests/pytest/eligibility/test_views.py | 17 +++-- tests/pytest/enrollment/test_views.py | 30 ++++---- tests/pytest/oauth/test_analytics.py | 16 ++--- ...erifier_required.py => test_middleware.py} | 24 +++---- tests/pytest/oauth/test_views.py | 72 +++++++++---------- 21 files changed, 197 insertions(+), 217 deletions(-) rename tests/pytest/oauth/{test_middleware_authverifier_required.py => test_middleware.py} (61%) diff --git a/benefits/core/analytics.py b/benefits/core/analytics.py index 51dc4ae09..a58a610a4 100644 --- a/benefits/core/analytics.py +++ b/benefits/core/analytics.py @@ -45,8 +45,8 @@ def __init__(self, request, event_type, **kwargs): agency = session.agency(request) agency_name = agency.long_name if agency else None - verifier = session.verifier(request) - verifier_name = verifier.name if verifier else None + flow = session.flow(request) + verifier_name = flow.name if flow else None eligibility_types = session.eligibility(request) eligibility_types = EligibilityType.get_names(eligibility_types) if eligibility_types else None diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 40a9cea8e..47b65e257 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -48,16 +48,16 @@ def analytics(request): def authentication(request): """Context processor adds authentication information to request context.""" - verifier = session.verifier(request) + flow = session.flow(request) - if verifier: + if flow: data = { "logged_in": session.logged_in(request), } - if verifier.uses_claims_verification: - data["sign_out_button_template"] = verifier.claims_provider.sign_out_button_template - data["sign_out_link_template"] = verifier.claims_provider.sign_out_link_template + if flow.uses_claims_verification: + data["sign_out_button_template"] = flow.claims_provider.sign_out_button_template + data["sign_out_link_template"] = flow.claims_provider.sign_out_link_template return {"authentication": data} else: diff --git a/benefits/core/middleware.py b/benefits/core/middleware.py index 4d2e18de3..c3328233f 100644 --- a/benefits/core/middleware.py +++ b/benefits/core/middleware.py @@ -83,15 +83,15 @@ def process_request(self, request): return self.get_response(request) -class VerifierSessionRequired(MiddlewareMixin): - """Middleware raises an exception for sessions lacking an eligibility verifier configuration.""" +class FlowSessionRequired(MiddlewareMixin): + """Middleware raises an exception for sessions lacking a configured enrollment flow.""" def process_request(self, request): - if session.verifier(request): - logger.debug("Session configured with eligibility verifier") + if session.flow(request): + logger.debug("Session configured with enrollment flow") return None else: - logger.debug("Session not configured with eligibility verifier") + logger.debug("Session not configured with enrollment flow") return user_error(request) @@ -129,9 +129,9 @@ class LoginRequired(MiddlewareMixin): """Middleware that checks whether a user is logged in.""" def process_view(self, request, view_func, view_args, view_kwargs): - # only require login if verifier uses claims verification - verifier = session.verifier(request) - if not verifier or not verifier.uses_claims_verification or session.logged_in(request): + # only require login if flow uses claims verification + flow = session.flow(request) + if not flow or not flow.uses_claims_verification or session.logged_in(request): # pass through return None diff --git a/benefits/core/session.py b/benefits/core/session.py index 6d791bfb0..a8bc5b359 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -23,13 +23,13 @@ _ENROLLMENT_TOKEN = "enrollment_token" _ENROLLMENT_TOKEN_EXP = "enrollment_token_expiry" _ENROLLMENT_EXP = "enrollment_expiry" +_FLOW = "flow" _LANG = "lang" _OAUTH_CLAIM = "oauth_claim" _OAUTH_TOKEN = "oauth_token" _ORIGIN = "origin" _START = "start" _UID = "uid" -_VERIFIER = "verifier" def agency(request): @@ -52,6 +52,7 @@ def context_dict(request): _AGENCY: agency(request).slug if active_agency(request) else None, _DEBUG: debug(request), _DID: did(request), + _FLOW: flow(request), _ELIGIBILITY: eligibility(request), _ENROLLMENT_EXP: enrollment_expiry(request), _ENROLLMENT_TOKEN: enrollment_token(request), @@ -62,7 +63,6 @@ def context_dict(request): _ORIGIN: origin(request), _START: start(request), _UID: uid(request), - _VERIFIER: verifier(request), } @@ -178,6 +178,7 @@ def reset(request): """Reset the session for the request.""" logger.debug("Reset session") request.session[_AGENCY] = None + request.session[_FLOW] = None request.session[_ELIGIBILITY] = None request.session[_ORIGIN] = reverse("core:index") request.session[_ENROLLMENT_EXP] = None @@ -185,7 +186,6 @@ def reset(request): request.session[_ENROLLMENT_TOKEN_EXP] = None request.session[_OAUTH_TOKEN] = None request.session[_OAUTH_CLAIM] = None - request.session[_VERIFIER] = None if _UID not in request.session or not request.session[_UID]: logger.debug("Reset session time and uid") @@ -238,6 +238,7 @@ def update( request, agency=None, debug=None, + flow=None, eligibility_types=None, enrollment_expiry=None, enrollment_token=None, @@ -245,7 +246,6 @@ def update( oauth_token=None, oauth_claim=None, origin=None, - verifier=None, ): """Update the request's session with non-null values.""" if agency is not None and isinstance(agency, models.TransitAgency): @@ -281,13 +281,13 @@ def update( request.session[_OAUTH_CLAIM] = oauth_claim if origin is not None: request.session[_ORIGIN] = origin - if verifier is not None and isinstance(verifier, models.EligibilityVerifier): - request.session[_VERIFIER] = verifier.id + if flow is not None and isinstance(flow, models.EnrollmentFlow): + request.session[_FLOW] = flow.id -def verifier(request): - """Get the verifier from the request's session, or None""" +def flow(request) -> models.EnrollmentFlow | None: + """Get the EnrollmentFlow from the request's session, or None""" try: - return models.EligibilityVerifier.by_id(request.session[_VERIFIER]) - except (KeyError, models.EligibilityVerifier.DoesNotExist): + return models.EnrollmentFlow.by_id(request.session[_FLOW]) + except (KeyError, models.EnrollmentFlow.DoesNotExist): return None diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py index e094e1edf..21f13b966 100644 --- a/benefits/eligibility/views.py +++ b/benefits/eligibility/views.py @@ -9,7 +9,7 @@ from django.utils.decorators import decorator_from_middleware from benefits.core import recaptcha, session -from benefits.core.middleware import AgencySessionRequired, LoginRequired, RecaptchaEnabled, VerifierSessionRequired +from benefits.core.middleware import AgencySessionRequired, LoginRequired, RecaptchaEnabled, FlowSessionRequired from benefits.core.models import EligibilityVerifier from . import analytics, forms, verify @@ -71,20 +71,20 @@ def index(request, agency=None): @decorator_from_middleware(AgencySessionRequired) -@decorator_from_middleware(VerifierSessionRequired) +@decorator_from_middleware(FlowSessionRequired) def start(request): """View handler for the eligibility verification getting started screen.""" session.update(request, eligibility_types=[], origin=reverse(ROUTE_START)) - verifier = session.verifier(request) + flow = session.flow(request) - return TemplateResponse(request, verifier.eligibility_start_template) + return TemplateResponse(request, flow.eligibility_start_template) @decorator_from_middleware(AgencySessionRequired) @decorator_from_middleware(LoginRequired) @decorator_from_middleware(RecaptchaEnabled) -@decorator_from_middleware(VerifierSessionRequired) +@decorator_from_middleware(FlowSessionRequired) def confirm(request): """View handler for the eligibility verification form.""" @@ -96,20 +96,20 @@ def confirm(request): unverified_view = reverse(ROUTE_UNVERIFIED) agency = session.agency(request) - verifier = session.verifier(request) - types_to_verify = agency.type_names_to_verify(verifier) + flow = session.flow(request) + types_to_verify = agency.type_names_to_verify(flow) # GET for OAuth verification - if request.method == "GET" and verifier.uses_claims_verification: + if request.method == "GET" and flow.uses_claims_verification: analytics.started_eligibility(request, types_to_verify) - verified_types = verify.eligibility_from_oauth(verifier, session.oauth_claim(request), agency) + verified_types = verify.eligibility_from_oauth(flow, session.oauth_claim(request), agency) if verified_types: return verified(request, verified_types) else: return redirect(unverified_view) - form = verifier.eligibility_form_instance() + form = flow.eligibility_form_instance() # GET/POST for Eligibility API verification context = {"form": form} @@ -122,7 +122,7 @@ def confirm(request): elif request.method == "POST": analytics.started_eligibility(request, types_to_verify) - form = verifier.eligibility_form_instance(data=request.POST) + form = flow.eligibility_form_instance(data=request.POST) # form was not valid, allow for correction/resubmission if not form.is_valid(): if recaptcha.has_error(form): @@ -131,7 +131,7 @@ def confirm(request): return TemplateResponse(request, TEMPLATE_CONFIRM, context) # form is valid, make Eligibility Verification request to get the verified types - verified_types = verify.eligibility_from_api(verifier, form, agency) + verified_types = verify.eligibility_from_api(flow, form, agency) # form was not valid, allow for correction/resubmission if verified_types is None: @@ -159,14 +159,14 @@ def verified(request, verified_types): @decorator_from_middleware(AgencySessionRequired) -@decorator_from_middleware(VerifierSessionRequired) +@decorator_from_middleware(FlowSessionRequired) def unverified(request): """View handler for the unverified eligibility page.""" agency = session.agency(request) - verifier = session.verifier(request) - types_to_verify = agency.type_names_to_verify(verifier) + flow = session.flow(request) + types_to_verify = agency.type_names_to_verify(flow) analytics.returned_fail(request, types_to_verify) - return TemplateResponse(request, verifier.eligibility_unverified_template) + return TemplateResponse(request, flow.eligibility_unverified_template) diff --git a/benefits/enrollment/views.py b/benefits/enrollment/views.py index 40a2b8262..e0f231d8e 100644 --- a/benefits/enrollment/views.py +++ b/benefits/enrollment/views.py @@ -15,7 +15,7 @@ import sentry_sdk from benefits.core import session -from benefits.core.middleware import EligibleSessionRequired, VerifierSessionRequired, pageview_decorator +from benefits.core.middleware import EligibleSessionRequired, FlowSessionRequired, pageview_decorator from benefits.core.views import ROUTE_LOGGED_OUT, ROUTE_SERVER_ERROR from . import analytics, forms @@ -241,12 +241,12 @@ def _calculate_expiry(expiration_days): def reenrollment_error(request): """View handler for a re-enrollment attempt that is not yet within the re-enrollment window.""" eligibility = session.eligibility(request) - verifier = session.verifier(request) + flow = session.flow(request) if eligibility.reenrollment_error_template is None: raise Exception(f"Re-enrollment error with null template on: {eligibility.label}") - if session.logged_in(request) and verifier.claims_provider.supports_sign_out: + if session.logged_in(request) and flow.claims_provider.supports_sign_out: # overwrite origin for a logged in user # if they click the logout button, they are taken to the new route session.update(request, origin=reverse(ROUTE_LOGGED_OUT)) @@ -276,16 +276,16 @@ def system_error(request): @pageview_decorator @decorator_from_middleware(EligibleSessionRequired) -@decorator_from_middleware(VerifierSessionRequired) +@decorator_from_middleware(FlowSessionRequired) def success(request): """View handler for the final success page.""" request.path = "/enrollment/success" session.update(request, origin=reverse(ROUTE_SUCCESS)) eligibility = session.eligibility(request) - verifier = session.verifier(request) + flow = session.flow(request) - if session.logged_in(request) and verifier.claims_provider.supports_sign_out: + if session.logged_in(request) and flow.claims_provider.supports_sign_out: # overwrite origin for a logged in user # if they click the logout button, they are taken to the new route session.update(request, origin=reverse(ROUTE_LOGGED_OUT)) diff --git a/benefits/oauth/analytics.py b/benefits/oauth/analytics.py index c68789483..23e956415 100644 --- a/benefits/oauth/analytics.py +++ b/benefits/oauth/analytics.py @@ -10,7 +10,7 @@ class OAuthEvent(core.Event): def __init__(self, request, event_type): super().__init__(request, event_type) - verifier = session.verifier(request) + verifier = session.flow(request) if verifier and verifier.uses_claims_verification: self.update_event_properties(auth_provider=verifier.claims_provider.client_name) diff --git a/benefits/oauth/middleware.py b/benefits/oauth/middleware.py index 69dae16b2..6fc30acb6 100644 --- a/benefits/oauth/middleware.py +++ b/benefits/oauth/middleware.py @@ -4,7 +4,7 @@ import sentry_sdk from benefits.core import session -from benefits.core.middleware import VerifierSessionRequired, user_error +from benefits.core.middleware import FlowSessionRequired, user_error from . import analytics from .redirects import ROUTE_SYSTEM_ERROR @@ -13,28 +13,28 @@ logger = logging.getLogger(__name__) -class VerifierUsesAuthVerificationSessionRequired(VerifierSessionRequired): - """Middleware raises an exception for sessions lacking an eligibility verifier that uses auth verification.""" +class FlowUsesClaimsVerificationSessionRequired(FlowSessionRequired): + """Middleware raises an exception for sessions lacking an enrollment flow that uses claims verification.""" def process_request(self, request): result = super().process_request(request) if result: - # from the base middleware class, the session didn't have a verifier + # from the base middleware class, the session didn't have an enrollment flow return result - verifier = session.verifier(request) + flow = session.flow(request) - if verifier.uses_claims_verification: - # all good, the chosen verifier is configured correctly + if flow.uses_claims_verification: + # all good, the chosen flow is configured correctly return None - elif not (verifier.eligibility_api_url or verifier.eligibility_form_class): - # the chosen verifier doesn't have Eligibility API config OR claims provider config + elif not (flow.eligibility_api_url or flow.eligibility_form_class): + # the chosen flow doesn't have Eligibility API config OR claims provider config # this is likely a misconfiguration on the backend, not a user error - message = f"Verifier with no API or IDP config: {verifier.name} (id={verifier.id})" + message = f"Flow with no API or claims config: {flow.name} (id={flow.id})" analytics.error(request, message=message, operation=request.path) sentry_sdk.capture_exception(Exception(message)) return redirect(ROUTE_SYSTEM_ERROR) else: - # the chosen verifier was for Eligibility API - logger.debug("Session not configured with eligibility verifier that uses auth verification") + # the chosen flow was for Eligibility API + logger.debug("Session not configured with enrollment flow that uses claims verification") return user_error(request) diff --git a/benefits/oauth/views.py b/benefits/oauth/views.py index 4686e9df2..a4609d99e 100644 --- a/benefits/oauth/views.py +++ b/benefits/oauth/views.py @@ -10,7 +10,7 @@ from benefits.core.middleware import AgencySessionRequired from . import analytics, redirects from .client import oauth, create_client -from .middleware import VerifierUsesAuthVerificationSessionRequired +from .middleware import FlowUsesClaimsVerificationSessionRequired logger = logging.getLogger(__name__) @@ -49,12 +49,12 @@ def _oauth_client_or_error_redirect(request, flow: models.EnrollmentFlow): return oauth_client -@decorator_from_middleware(VerifierUsesAuthVerificationSessionRequired) +@decorator_from_middleware(FlowUsesClaimsVerificationSessionRequired) def login(request): """View implementing OIDC authorize_redirect.""" - verifier = session.verifier(request) + flow = session.flow(request) - oauth_client_result = _oauth_client_or_error_redirect(request, verifier) + oauth_client_result = _oauth_client_or_error_redirect(request, flow) if hasattr(oauth_client_result, "authorize_redirect"): # this looks like an oauth_client since it has the method we need @@ -90,12 +90,12 @@ def login(request): return result -@decorator_from_middleware(VerifierUsesAuthVerificationSessionRequired) +@decorator_from_middleware(FlowUsesClaimsVerificationSessionRequired) def authorize(request): """View implementing OIDC token authorization.""" - verifier = session.verifier(request) + flow = session.flow(request) - oauth_client_result = _oauth_client_or_error_redirect(request, verifier) + oauth_client_result = _oauth_client_or_error_redirect(request, flow) if hasattr(oauth_client_result, "authorize_access_token"): # this looks like an oauth_client since it has the method we need @@ -128,23 +128,23 @@ def authorize(request): id_token = token["id_token"] # We store the returned claim in case it can be used later in eligibility verification. - verifier_claim = verifier.claims_claim + flow_claim = flow.claims_claim stored_claim = None error_claim = None - if verifier_claim: + if flow_claim: userinfo = token.get("userinfo") if userinfo: - claim_value = userinfo.get(verifier_claim) + claim_value = userinfo.get(flow_claim) # the claim comes back in userinfo like { "claim": "1" | "0" } claim_value = int(claim_value) if claim_value else None if claim_value is None: - logger.warning(f"userinfo did not contain: {verifier_claim}") + logger.warning(f"userinfo did not contain: {flow_claim}") elif claim_value == 1: # if userinfo contains our claim and the flag is 1 (true), store the *claim* - stored_claim = verifier_claim + stored_claim = flow_claim elif claim_value >= 10: error_claim = claim_value @@ -154,7 +154,7 @@ def authorize(request): return redirect(ROUTE_CONFIRM) -@decorator_from_middleware(VerifierUsesAuthVerificationSessionRequired) +@decorator_from_middleware(FlowUsesClaimsVerificationSessionRequired) def cancel(request): """View implementing cancellation of OIDC authorization.""" @@ -163,12 +163,12 @@ def cancel(request): return redirect(ROUTE_UNVERIFIED) -@decorator_from_middleware(VerifierUsesAuthVerificationSessionRequired) +@decorator_from_middleware(FlowUsesClaimsVerificationSessionRequired) def logout(request): """View implementing OIDC and application sign out.""" - verifier = session.verifier(request) + flow = session.flow(request) - oauth_client_result = _oauth_client_or_error_redirect(request, verifier) + oauth_client_result = _oauth_client_or_error_redirect(request, flow) if hasattr(oauth_client_result, "load_server_metadata"): # this looks like an oauth_client since it has the method we need @@ -194,7 +194,7 @@ def logout(request): return redirects.deauthorize_redirect(request, oauth_client, token, redirect_uri) -@decorator_from_middleware(VerifierUsesAuthVerificationSessionRequired) +@decorator_from_middleware(FlowUsesClaimsVerificationSessionRequired) def post_logout(request): """View routes the user to their origin after sign out.""" diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index ee01bf2bd..7c8ef3e40 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -250,40 +250,22 @@ def mocked_session_enrollment_expiry(mocker): @pytest.fixture -def mocked_session_verifier(mocker, model_EnrollmentFlow): - return mocker.patch("benefits.core.session.verifier", autospec=True, return_value=model_EnrollmentFlow) +def mocked_session_flow(mocker, model_EnrollmentFlow): + return mocker.patch("benefits.core.session.flow", autospec=True, return_value=model_EnrollmentFlow) @pytest.fixture -def mocked_session_verifier_oauth(mocker, model_EnrollmentFlow_with_scope_and_claim): +def mocked_session_flow_uses_claims_verification(mocker, model_EnrollmentFlow_with_scope_and_claim): return mocker.patch( - "benefits.core.session.verifier", + "benefits.core.session.flow", autospec=True, return_value=model_EnrollmentFlow_with_scope_and_claim, ) @pytest.fixture -def mocked_session_verifier_no_scope_and_claim_oauth(mocker, model_EnrollmentFlow): - return mocker.patch( - "benefits.core.session.verifier", - autospec=True, - return_value=model_EnrollmentFlow, - ) - - -@pytest.fixture -def mocked_session_verifier_uses_claims_verification(model_EnrollmentFlow_with_scope_and_claim, mocked_session_verifier_oauth): - mocked_session_verifier_oauth.return_value = model_EnrollmentFlow_with_scope_and_claim - return mocked_session_verifier_oauth - - -@pytest.fixture -def mocked_session_verifier_does_not_use_claims_verification( - model_EnrollmentFlow, mocked_session_verifier_no_scope_and_claim_oauth -): - mocked_session_verifier_no_scope_and_claim_oauth.return_value = model_EnrollmentFlow - return mocked_session_verifier_no_scope_and_claim_oauth +def mocked_session_flow_does_not_use_claims_verification(mocked_session_flow): + return mocked_session_flow @pytest.fixture diff --git a/tests/pytest/core/test_analytics.py b/tests/pytest/core/test_analytics.py index e343b55f1..193736f43 100644 --- a/tests/pytest/core/test_analytics.py +++ b/tests/pytest/core/test_analytics.py @@ -59,7 +59,7 @@ def test_Event_reads_session(app_request, mocker): session_spy.language.assert_called_once_with(app_request) session_spy.start.assert_called_once_with(app_request) session_spy.uid.assert_called_once_with(app_request) - session_spy.verifier.assert_called_once_with(app_request) + session_spy.flow.assert_called_once_with(app_request) @pytest.mark.django_db diff --git a/tests/pytest/core/test_middleware_login_required.py b/tests/pytest/core/test_middleware_login_required.py index 82f69dea0..ee7b40dca 100644 --- a/tests/pytest/core/test_middleware_login_required.py +++ b/tests/pytest/core/test_middleware_login_required.py @@ -15,7 +15,7 @@ def decorated_view(mocked_view): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_oauth") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_login_flow_uses_claims_verification(app_request, mocked_view, decorated_view): response = decorated_view(app_request) @@ -29,7 +29,7 @@ def test_login_flow_uses_claims_verification(app_request, mocked_view, decorated def test_login_flow_does_not_use_claims_verification(app_request, model_EnrollmentFlow, mocked_view, decorated_view): model_EnrollmentFlow.claims_provider = None assert not model_EnrollmentFlow.uses_claims_verification - session.update(app_request, verifier=model_EnrollmentFlow) + session.update(app_request, flow=model_EnrollmentFlow) decorated_view(app_request) @@ -37,7 +37,7 @@ def test_login_flow_does_not_use_claims_verification(app_request, model_Enrollme @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_oauth") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_logged_in(app_request, mocked_view, decorated_view): # log in session.update(app_request, oauth_token="something") diff --git a/tests/pytest/core/test_middleware_verifier_required.py b/tests/pytest/core/test_middleware_verifier_required.py index bcd876272..36d71de82 100644 --- a/tests/pytest/core/test_middleware_verifier_required.py +++ b/tests/pytest/core/test_middleware_verifier_required.py @@ -2,16 +2,16 @@ import pytest -from benefits.core.middleware import VerifierSessionRequired, TEMPLATE_USER_ERROR +from benefits.core.middleware import FlowSessionRequired, TEMPLATE_USER_ERROR @pytest.fixture def decorated_view(mocked_view): - return decorator_from_middleware(VerifierSessionRequired)(mocked_view) + return decorator_from_middleware(FlowSessionRequired)(mocked_view) @pytest.mark.django_db -def test_verifier_required_no_verifier(app_request, mocked_view, decorated_view): +def test_flow_required_no_flow(app_request, mocked_view, decorated_view): response = decorated_view(app_request) mocked_view.assert_not_called() @@ -20,8 +20,8 @@ def test_verifier_required_no_verifier(app_request, mocked_view, decorated_view) @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_oauth") -def test_verifier_required_verifier(app_request, mocked_view, decorated_view): +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") +def test_flow_required_flow(app_request, mocked_view, decorated_view): decorated_view(app_request) mocked_view.assert_called_once() diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index 17384ea3d..2602f85a5 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -304,12 +304,12 @@ def test_reset_start(app_request): @pytest.mark.django_db -def test_reset_verifier(app_request): - app_request.session[session._VERIFIER] = "verifier" +def test_reset_flow(app_request): + app_request.session[session._FLOW] = "flow" session.reset(app_request) - assert session.verifier(app_request) is None + assert session.flow(app_request) is None @pytest.mark.django_db @@ -456,14 +456,14 @@ def test_update_origin(app_request): @pytest.mark.django_db -def test_update_verifier(app_request): - verifier = models.EligibilityVerifier.objects.first() +def test_update_flow(app_request): + flow = models.EnrollmentFlow.objects.first() - session.update(app_request, verifier=verifier) + session.update(app_request, flow=flow) - assert session.verifier(app_request) == verifier + assert session.flow(app_request) == flow @pytest.mark.django_db -def test_verifier_default(app_request): - assert session.verifier(app_request) is None +def test_flow_default(app_request): + assert session.flow(app_request) is None diff --git a/tests/pytest/eligibility/conftest.py b/tests/pytest/eligibility/conftest.py index ae6cfd83f..235f892d3 100644 --- a/tests/pytest/eligibility/conftest.py +++ b/tests/pytest/eligibility/conftest.py @@ -2,9 +2,9 @@ @pytest.fixture -def mocked_eligibility_request_session(mocked_session_agency, mocked_session_verifier): +def mocked_eligibility_request_session(mocked_session_agency, mocked_session_flow): """ - Stub fixture combines mocked_session_agency and mocked_session_verifier + Stub fixture combines mocked_session_agency and mocked_session_flow so session behaves like in normal requests to the eligibility app """ pass diff --git a/tests/pytest/eligibility/test_verify.py b/tests/pytest/eligibility/test_verify.py index a92b7fd6c..401486058 100644 --- a/tests/pytest/eligibility/test_verify.py +++ b/tests/pytest/eligibility/test_verify.py @@ -53,11 +53,10 @@ def test_eligibility_from_api_no_verified_types( @pytest.mark.django_db def test_eligibility_from_oauth_does_not_use_claims_verification( - mocked_session_verifier_does_not_use_claims_verification, model_TransitAgency + mocked_session_flow_does_not_use_claims_verification, model_TransitAgency ): - # mocked_session_verifier_does_not_use_claims_verification is Mocked version of the session.verifier() function - # call it (with a None request) to return a verifier object - verifier = mocked_session_verifier_does_not_use_claims_verification(None) + # mocked_session_flow_does_not_use_claims_verification is Mocked version of the session.flow() function + verifier = mocked_session_flow_does_not_use_claims_verification.return_value types = eligibility_from_oauth(verifier, "claim", model_TransitAgency) @@ -65,27 +64,25 @@ def test_eligibility_from_oauth_does_not_use_claims_verification( @pytest.mark.django_db -def test_eligibility_from_oauth_claim_mismatch(mocked_session_verifier_uses_claims_verification, model_TransitAgency): - # mocked_session_verifier_uses_claims_verification is Mocked version of the session.verifier() function - # call it (with a None request) to return a verifier object - verifier = mocked_session_verifier_uses_claims_verification(None) - verifier.claim = "claim" +def test_eligibility_from_oauth_claim_mismatch(mocked_session_flow_uses_claims_verification, model_TransitAgency): + # mocked_session_flow_uses_claims_verification is Mocked version of the session.flow() function + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "claim" - types = eligibility_from_oauth(verifier, "some_other_claim", model_TransitAgency) + types = eligibility_from_oauth(flow, "some_other_claim", model_TransitAgency) assert types == [] @pytest.mark.django_db def test_eligibility_from_oauth_claim_match( - mocked_session_verifier_uses_claims_verification, model_EligibilityType, model_TransitAgency + mocked_session_flow_uses_claims_verification, model_EligibilityType, model_TransitAgency ): - # mocked_session_verifier_uses_claims_verification is Mocked version of the session.verifier() function - # call it (with a None request) to return a verifier object - verifier = mocked_session_verifier_uses_claims_verification.return_value - verifier.claims_claim = "claim" - verifier.eligibility_type = model_EligibilityType + # mocked_session_flow_uses_claims_verification is Mocked version of the session.flow() function + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "claim" + flow.eligibility_type = model_EligibilityType - types = eligibility_from_oauth(verifier, "claim", model_TransitAgency) + types = eligibility_from_oauth(flow, "claim", model_TransitAgency) assert types == [model_EligibilityType.name] diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py index b2a6c82a4..ffcb23d0d 100644 --- a/tests/pytest/eligibility/test_views.py +++ b/tests/pytest/eligibility/test_views.py @@ -172,7 +172,7 @@ def test_index_calls_session_logout(client, session_logout_spy): @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_verifier_uses_claims_verification" + "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_flow_uses_claims_verification" ) def test_start_verifier_uses_claims_verification_logged_in(mocker, client): mock_session = mocker.patch("benefits.eligibility.views.session") @@ -186,7 +186,7 @@ def test_start_verifier_uses_claims_verification_logged_in(mocker, client): @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_verifier_uses_claims_verification" + "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_flow_uses_claims_verification" ) def test_start_verifier_uses_claims_verification_not_logged_in(mocker, client): mock_session = mocker.patch("benefits.eligibility.views.session") @@ -200,7 +200,7 @@ def test_start_verifier_uses_claims_verification_not_logged_in(mocker, client): @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_verifier_does_not_use_claims_verification" + "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_flow_does_not_use_claims_verification" ) def test_start_verifier_does_not_use_claims_verification(client): path = reverse(ROUTE_START) @@ -220,7 +220,7 @@ def test_start_without_verifier(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow") def test_confirm_get_unverified(mocker, client): path = reverse(ROUTE_CONFIRM) response = client.get(path) @@ -230,7 +230,7 @@ def test_confirm_get_unverified(mocker, client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility", "mocked_session_verifier") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility", "mocked_session_flow") def test_confirm_get_verified(client, mocked_session_update): path = reverse(ROUTE_CONFIRM) response = client.get(path) @@ -241,7 +241,7 @@ def test_confirm_get_verified(client, mocked_session_update): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier_oauth", "mocked_session_oauth_token") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow_uses_claims_verification", "mocked_session_oauth_token") def test_confirm_get_oauth_verified(mocker, client, model_EligibilityType, mocked_session_update, mocked_analytics_module): mocker.patch("benefits.eligibility.verify.eligibility_from_oauth", return_value=[model_EligibilityType]) @@ -256,7 +256,10 @@ def test_confirm_get_oauth_verified(mocker, client, model_EligibilityType, mocke @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_session_verifier_oauth", "mocked_session_oauth_token", "mocked_session_update" + "mocked_session_agency", + "mocked_session_flow_uses_claims_verification", + "mocked_session_oauth_token", + "mocked_session_update", ) def test_confirm_get_oauth_unverified(mocker, client): mocker.patch("benefits.eligibility.verify.eligibility_from_oauth", return_value=[]) diff --git a/tests/pytest/enrollment/test_views.py b/tests/pytest/enrollment/test_views.py index da088b884..666b69748 100644 --- a/tests/pytest/enrollment/test_views.py +++ b/tests/pytest/enrollment/test_views.py @@ -234,7 +234,7 @@ def test_token_connection_error(mocker, client, mocked_analytics_module, mocked_ @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_get(client, model_EligibilityType): path = reverse(ROUTE_INDEX) response = client.get(path) @@ -253,7 +253,7 @@ def test_index_eligible_get(client, model_EligibilityType): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") @pytest.mark.parametrize("LANGUAGE_CODE, overlay_language", [("en", "en"), ("es", "es-419"), ("unsupported", "en")]) def test_index_eligible_get_changed_language(client, LANGUAGE_CODE, overlay_language): path = reverse(ROUTE_INDEX) @@ -365,7 +365,7 @@ def test_get_group_funding_sources_funding_source_already_enrolled( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_does_not_support_expiration_customer_already_enrolled_no_expiry( mocker, client, @@ -393,7 +393,7 @@ def test_index_eligible_post_valid_form_success_does_not_support_expiration_cust @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_does_not_support_expiration_no_expiry( mocker, client, @@ -447,7 +447,7 @@ def test_calculate_expiry_specific_date(mocker): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_supports_expiration( mocker, client, @@ -476,7 +476,7 @@ def test_index_eligible_post_valid_form_success_supports_expiration( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_supports_expiration_no_expiry( mocker, client, @@ -544,7 +544,7 @@ def test_is_expired_expiry_date_equals_now(mocker): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_supports_expiration_is_expired( mocker, client, @@ -654,7 +654,7 @@ def test_is_within_enrollment_window_equal_expiry_date(mocker): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_supports_expiration_is_within_reenrollment_window( mocker, client, @@ -689,7 +689,7 @@ def test_index_eligible_post_valid_form_success_supports_expiration_is_within_re @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_supports_expiration_is_not_expired_yet( mocker, client, @@ -718,7 +718,7 @@ def test_index_eligible_post_valid_form_success_supports_expiration_is_not_expir @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_index_eligible_post_valid_form_success_does_not_support_expiration_has_expiration_date( mocker, client, @@ -775,7 +775,7 @@ def test_reenrollment_error_ineligible(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") def test_reenrollment_error_eligibility_no_error_template(client): path = reverse(ROUTE_REENROLLMENT_ERROR) @@ -784,7 +784,7 @@ def test_reenrollment_error_eligibility_no_error_template(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow") def test_reenrollment_error(client, model_EligibilityType_supports_expiration, mocked_session_eligibility): mocked_session_eligibility.return_value = model_EligibilityType_supports_expiration @@ -839,7 +839,7 @@ def test_success_no_flow(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_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 @@ -856,7 +856,7 @@ def test_success_authentication_logged_in(mocker, client, model_TransitAgency, m @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_session_eligibility") def test_success_authentication_not_logged_in( mocker, client, model_TransitAgency, model_EligibilityType, mocked_analytics_module ): @@ -875,7 +875,7 @@ def test_success_authentication_not_logged_in( @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_session_verifier_does_not_use_claims_verification", "mocked_session_eligibility" + "mocked_session_agency", "mocked_session_flow_does_not_use_claims_verification", "mocked_session_eligibility" ) def test_success_no_authentication(mocker, client, model_EligibilityType, mocked_analytics_module): mock_session = mocker.patch("benefits.enrollment.views.session") diff --git a/tests/pytest/oauth/test_analytics.py b/tests/pytest/oauth/test_analytics.py index f4b26ae0b..d571b92f6 100644 --- a/tests/pytest/oauth/test_analytics.py +++ b/tests/pytest/oauth/test_analytics.py @@ -4,28 +4,26 @@ @pytest.mark.django_db -def test_OAuthEvent_verifier_client_name_when_uses_claims_verification( - app_request, mocked_session_verifier_uses_claims_verification -): - mocked_verifier = mocked_session_verifier_uses_claims_verification(app_request) - mocked_verifier.claims_provider.client_name = "ClientName" +def test_OAuthEvent_flow_client_name_when_uses_claims_verification(app_request, mocked_session_flow_uses_claims_verification): + mocked_flow = mocked_session_flow_uses_claims_verification(app_request) + mocked_flow.claims_provider.client_name = "ClientName" event = OAuthEvent(app_request, "event type") assert "auth_provider" in event.event_properties - assert event.event_properties["auth_provider"] == mocked_verifier.claims_provider.client_name + assert event.event_properties["auth_provider"] == mocked_flow.claims_provider.client_name @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_does_not_use_claims_verification") -def test_OAuthEvent_verifier_no_client_name_when_does_not_use_claims_verification(app_request): +@pytest.mark.usefixtures("mocked_session_flow_does_not_use_claims_verification") +def test_OAuthEvent_flow_no_client_name_when_does_not_use_claims_verification(app_request): event = OAuthEvent(app_request, "event type") assert "auth_provider" not in event.event_properties @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_OAuthErrorEvent(app_request): event_default = OAuthErrorEvent(app_request, "the message", "the operation") diff --git a/tests/pytest/oauth/test_middleware_authverifier_required.py b/tests/pytest/oauth/test_middleware.py similarity index 61% rename from tests/pytest/oauth/test_middleware_authverifier_required.py rename to tests/pytest/oauth/test_middleware.py index ae78f4ba5..2817fcbce 100644 --- a/tests/pytest/oauth/test_middleware_authverifier_required.py +++ b/tests/pytest/oauth/test_middleware.py @@ -4,14 +4,14 @@ import pytest from benefits.core.middleware import TEMPLATE_USER_ERROR -from benefits.oauth.middleware import VerifierUsesAuthVerificationSessionRequired +from benefits.oauth.middleware import FlowUsesClaimsVerificationSessionRequired import benefits.oauth.middleware from benefits.oauth.redirects import ROUTE_SYSTEM_ERROR @pytest.fixture def decorated_view(mocked_view): - return decorator_from_middleware(VerifierUsesAuthVerificationSessionRequired)(mocked_view) + return decorator_from_middleware(FlowUsesClaimsVerificationSessionRequired)(mocked_view) @pytest.fixture @@ -25,7 +25,7 @@ def mocked_sentry_sdk_module(mocker): @pytest.mark.django_db -def test_authverifier_required_no_verifier(app_request, mocked_view, decorated_view): +def test_flow_using_claims_verification_required__no_flow(app_request, mocked_view, decorated_view): response = decorated_view(app_request) mocked_view.assert_not_called() @@ -34,8 +34,8 @@ def test_authverifier_required_no_verifier(app_request, mocked_view, decorated_v @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_does_not_use_claims_verification") -def test_authverifier_required_no_authverifier(app_request, mocked_view, decorated_view): +@pytest.mark.usefixtures("mocked_session_flow_does_not_use_claims_verification") +def test_flow_using_claims_verification_required__no_claimsprovider(app_request, mocked_view, decorated_view): response = decorated_view(app_request) mocked_view.assert_not_called() @@ -45,19 +45,19 @@ def test_authverifier_required_no_authverifier(app_request, mocked_view, decorat @pytest.mark.django_db @pytest.mark.parametrize(("api_url", "form_class"), [(None, None), (None, ""), ("", None), ("", "")]) -def test_authverifier_required_misconfigured_verifier( +def test_flow_using_claims_verification_required__misconfigured_flow( app_request, mocked_view, decorated_view, - mocked_session_verifier_does_not_use_claims_verification, + mocked_session_flow_does_not_use_claims_verification, mocked_analytics_module, mocked_sentry_sdk_module, api_url, form_class, ): - # fake a misconfigured verifier - mocked_session_verifier_does_not_use_claims_verification.return_value.eligibility_api_url = api_url - mocked_session_verifier_does_not_use_claims_verification.return_value.eligibility_form_class = form_class + # fake a misconfigured flow + mocked_session_flow_does_not_use_claims_verification.return_value.eligibility_api_url = api_url + mocked_session_flow_does_not_use_claims_verification.return_value.eligibility_form_class = form_class response = decorated_view(app_request) @@ -69,8 +69,8 @@ def test_authverifier_required_misconfigured_verifier( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_oauth") -def test_authverifier_required_authverifier(app_request, mocked_view, decorated_view): +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") +def test_flow_using_claims_verification_required__configured(app_request, mocked_view, decorated_view): decorated_view(app_request) mocked_view.assert_called_once() diff --git a/tests/pytest/oauth/test_views.py b/tests/pytest/oauth/test_views.py index b35c76934..b00b3ac4e 100644 --- a/tests/pytest/oauth/test_views.py +++ b/tests/pytest/oauth/test_views.py @@ -49,7 +49,7 @@ def mocked_oauth_client_or_error_redirect__error(mocked_oauth_create_client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_oauth_client_or_error_redirect_no_oauth_client( app_request, model_EnrollmentFlow_with_scope_and_claim, @@ -68,7 +68,7 @@ def test_oauth_client_or_error_redirect_no_oauth_client( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") def test_oauth_client_or_error_redirect_oauth_client_exception( app_request, model_EnrollmentFlow_with_scope_and_claim, mocked_analytics_module, mocked_sentry_sdk_module ): @@ -81,7 +81,7 @@ def test_oauth_client_or_error_redirect_oauth_client_exception( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_create_client") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_oauth_create_client") def test_oauth_client_or_error_oauth_client( app_request, model_EnrollmentFlow_with_scope_and_claim, mocked_analytics_module, mocked_sentry_sdk_module ): @@ -93,7 +93,7 @@ def test_oauth_client_or_error_oauth_client( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") def test_login_oauth_client_init_error(app_request, mocked_analytics_module): result = login(app_request) @@ -103,7 +103,7 @@ def test_login_oauth_client_init_error(app_request, mocked_analytics_module): @pytest.mark.django_db -def test_login_no_session_verifier(app_request): +def test_login_no_session_flow(app_request): result = login(app_request) assert result.status_code == 200 @@ -111,7 +111,7 @@ def test_login_no_session_verifier(app_request): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_login(app_request, mocked_oauth_client_or_error_redirect__client, mocked_analytics_module): assert not session.logged_in(app_request) mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value @@ -126,7 +126,7 @@ def test_login(app_request, mocked_oauth_client_or_error_redirect__client, mocke @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_login_authorize_redirect_exception( app_request, mocked_oauth_client_or_error_redirect__client, mocked_analytics_module, mocked_sentry_sdk_module ): @@ -142,7 +142,7 @@ def test_login_authorize_redirect_exception( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") @pytest.mark.parametrize("status_code", [400, 401, 403, 404, 500, 501, 503]) def test_login_authorize_redirect_error_response( app_request, mocked_oauth_client_or_error_redirect__client, mocked_analytics_module, mocked_sentry_sdk_module, status_code @@ -159,7 +159,7 @@ def test_login_authorize_redirect_error_response( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") def test_authorize_oauth_client_init_error(app_request, mocked_analytics_module, mocked_sentry_sdk_module): result = authorize(app_request) @@ -170,7 +170,7 @@ def test_authorize_oauth_client_init_error(app_request, mocked_analytics_module, @pytest.mark.django_db -def test_authorize_no_session_verifier(app_request): +def test_authorize_no_session_flow(app_request): result = authorize(app_request) assert result.status_code == 200 @@ -178,7 +178,7 @@ def test_authorize_no_session_verifier(app_request): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_authorize_error( mocked_oauth_client_or_error_redirect__client, mocked_analytics_module, mocked_sentry_sdk_module, app_request ): @@ -198,7 +198,7 @@ def test_authorize_error( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_authorize_empty_token( mocked_oauth_client_or_error_redirect__client, mocked_analytics_module, mocked_sentry_sdk_module, app_request ): @@ -218,7 +218,7 @@ def test_authorize_empty_token( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_authorize_success(mocked_oauth_client_or_error_redirect__client, mocked_analytics_module, app_request): mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value mocked_oauth_client.authorize_access_token.return_value = {"id_token": "token"} @@ -236,10 +236,10 @@ def test_authorize_success(mocked_oauth_client_or_error_redirect__client, mocked @pytest.mark.django_db @pytest.mark.usefixtures("mocked_analytics_module") def test_authorize_success_with_claim_true( - app_request, mocked_session_verifier_uses_claims_verification, mocked_oauth_client_or_error_redirect__client + app_request, mocked_session_flow_uses_claims_verification, mocked_oauth_client_or_error_redirect__client ): - verifier = mocked_session_verifier_uses_claims_verification.return_value - verifier.claims_claim = "claim" + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "claim" mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value mocked_oauth_client.authorize_access_token.return_value = {"id_token": "token", "userinfo": {"claim": "1"}} @@ -254,10 +254,10 @@ def test_authorize_success_with_claim_true( @pytest.mark.django_db @pytest.mark.usefixtures("mocked_analytics_module") def test_authorize_success_with_claim_false( - app_request, mocked_session_verifier_uses_claims_verification, mocked_oauth_client_or_error_redirect__client + app_request, mocked_session_flow_uses_claims_verification, mocked_oauth_client_or_error_redirect__client ): - verifier = mocked_session_verifier_uses_claims_verification.return_value - verifier.claims_claim = "claim" + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "claim" mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value mocked_oauth_client.authorize_access_token.return_value = {"id_token": "token", "userinfo": {"claim": "0"}} @@ -272,12 +272,12 @@ def test_authorize_success_with_claim_false( @pytest.mark.django_db def test_authorize_success_with_claim_error( app_request, - mocked_session_verifier_uses_claims_verification, + mocked_session_flow_uses_claims_verification, mocked_oauth_client_or_error_redirect__client, mocked_analytics_module, ): - verifier = mocked_session_verifier_uses_claims_verification.return_value - verifier.claims_claim = "claim" + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "claim" mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value mocked_oauth_client.authorize_access_token.return_value = {"id_token": "token", "userinfo": {"claim": "10"}} @@ -292,11 +292,11 @@ def test_authorize_success_with_claim_error( @pytest.mark.django_db @pytest.mark.usefixtures("mocked_analytics_module") -def test_authorize_success_without_verifier_claim( - app_request, mocked_session_verifier_uses_claims_verification, mocked_oauth_client_or_error_redirect__client +def test_authorize_success_without_flow_claim( + app_request, mocked_session_flow_uses_claims_verification, mocked_oauth_client_or_error_redirect__client ): - verifier = mocked_session_verifier_uses_claims_verification.return_value - verifier.claims_claim = "" + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "" mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value mocked_oauth_client.authorize_access_token.return_value = {"id_token": "token", "userinfo": {"claim": "1"}} @@ -318,12 +318,12 @@ def test_authorize_success_without_verifier_claim( ) def test_authorize_success_without_claim_in_response( app_request, - mocked_session_verifier_uses_claims_verification, + mocked_session_flow_uses_claims_verification, mocked_oauth_client_or_error_redirect__client, access_token_response, ): - verifier = mocked_session_verifier_uses_claims_verification.return_value - verifier.claims_claim = "claim" + flow = mocked_session_flow_uses_claims_verification.return_value + flow.claims_claim = "claim" mocked_oauth_client = mocked_oauth_client_or_error_redirect__client.return_value mocked_oauth_client.authorize_access_token.return_value = access_token_response @@ -336,7 +336,7 @@ def test_authorize_success_without_claim_in_response( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_cancel(app_request, mocked_analytics_module): unverified_route = reverse(ROUTE_UNVERIFIED) @@ -348,7 +348,7 @@ def test_cancel(app_request, mocked_analytics_module): @pytest.mark.django_db -def test_cancel_no_session_verifier(app_request): +def test_cancel_no_session_flow(app_request): result = cancel(app_request) assert result.status_code == 200 @@ -356,7 +356,7 @@ def test_cancel_no_session_verifier(app_request): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_oauth_client_or_error_redirect__error") def test_logout_oauth_client_init_error(app_request): result = authorize(app_request) @@ -365,7 +365,7 @@ def test_logout_oauth_client_init_error(app_request): @pytest.mark.django_db -def test_logout_no_session_verifier(app_request): +def test_logout_no_session_flow(app_request): result = logout(app_request) assert result.status_code == 200 @@ -373,7 +373,7 @@ def test_logout_no_session_verifier(app_request): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_logout(app_request, mocker, mocked_oauth_client_or_error_redirect__client, mocked_analytics_module): # logout internally calls deauthorize_redirect # this mocks that function and a success response @@ -400,7 +400,7 @@ def test_logout(app_request, mocker, mocked_oauth_client_or_error_redirect__clie @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_verifier_uses_claims_verification") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification") def test_post_logout(app_request, mocked_analytics_module): origin = reverse(ROUTE_INDEX) session.update(app_request, origin=origin) @@ -413,7 +413,7 @@ def test_post_logout(app_request, mocked_analytics_module): @pytest.mark.django_db -def test_post_logout_no_session_verifier(app_request): +def test_post_logout_no_session_flow(app_request): result = post_logout(app_request) assert result.status_code == 200 From fb091f82dfbae0e4dcd34d4f585989d2471e03ff Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 7 Aug 2024 23:36:03 +0000 Subject: [PATCH 4/8] refactor(eligibility): use EnrollmentFlow model --- ...ion.html => flow-radio-select-option.html} | 0 ...dio-select.html => flow-radio-select.html} | 0 benefits/core/widgets.py | 8 +- benefits/eligibility/forms.py | 26 +++--- benefits/eligibility/verify.py | 20 ++--- benefits/eligibility/views.py | 16 ++-- tests/cypress/specs/benefit-select.cy.js | 6 +- tests/pytest/core/test_models.py | 2 +- tests/pytest/eligibility/test_verify.py | 18 ++-- tests/pytest/eligibility/test_views.py | 84 +++++++++---------- 10 files changed, 85 insertions(+), 95 deletions(-) rename benefits/core/templates/core/widgets/{verifier-radio-select-option.html => flow-radio-select-option.html} (100%) rename benefits/core/templates/core/widgets/{verifier-radio-select.html => flow-radio-select.html} (100%) diff --git a/benefits/core/templates/core/widgets/verifier-radio-select-option.html b/benefits/core/templates/core/widgets/flow-radio-select-option.html similarity index 100% rename from benefits/core/templates/core/widgets/verifier-radio-select-option.html rename to benefits/core/templates/core/widgets/flow-radio-select-option.html diff --git a/benefits/core/templates/core/widgets/verifier-radio-select.html b/benefits/core/templates/core/widgets/flow-radio-select.html similarity index 100% rename from benefits/core/templates/core/widgets/verifier-radio-select.html rename to benefits/core/templates/core/widgets/flow-radio-select.html diff --git a/benefits/core/widgets.py b/benefits/core/widgets.py index f7385fdd9..1bd91db0a 100644 --- a/benefits/core/widgets.py +++ b/benefits/core/widgets.py @@ -19,11 +19,11 @@ def __init__(self, pattern=None, placeholder=None, **kwargs): self.attrs.update({"placeholder": placeholder}) -class VerifierRadioSelect(widgets.RadioSelect): - """A radio select input styled for the Eligibility Verifier""" +class FlowRadioSelect(widgets.RadioSelect): + """A radio select input styled for the Enrollment Flow.""" - template_name = "core/widgets/verifier-radio-select.html" - option_template_name = "core/widgets/verifier-radio-select-option.html" + template_name = "core/widgets/flow-radio-select.html" + option_template_name = "core/widgets/flow-radio-select-option.html" def __init__(self, selection_label_templates=(), *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index e0229b5b4..a3c5a5148 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -12,28 +12,28 @@ logger = logging.getLogger(__name__) -class EligibilityVerifierSelectionForm(forms.Form): - """Form to capture eligibility verifier selection.""" +class EnrollmentFlowSelectionForm(forms.Form): + """Form to capture enrollment flow selection.""" action_url = "eligibility:index" - id = "form-verifier-selection" + id = "form-flow-selection" method = "POST" - verifier = forms.ChoiceField(label="", widget=widgets.VerifierRadioSelect) + flow = forms.ChoiceField(label="", widget=widgets.FlowRadioSelect) # sets label to empty string so the radio_select template can override the label style submit_value = _("Choose this benefit") def __init__(self, agency: models.TransitAgency, *args, **kwargs): super().__init__(*args, **kwargs) - verifiers = agency.enrollment_flows.all() + flows = agency.enrollment_flows.all() self.classes = "col-lg-8" # second element is not used since we render the whole label using selection_label_template, # therefore set to None - verifier_field = self.fields["verifier"] - verifier_field.choices = [(v.id, None) for v in verifiers] - verifier_field.widget.selection_label_templates = {v.id: v.selection_label_template for v in verifiers} - verifier_field.widget.attrs.update({"data-custom-validity": _("Please choose a transit benefit.")}) + flow_field = self.fields["flow"] + flow_field.choices = [(f.id, None) for f in flows] + flow_field.widget.selection_label_templates = {f.id: f.selection_label_template for f in flows} + flow_field.widget.attrs.update({"data-custom-validity": _("Please choose a transit benefit.")}) self.use_custom_validity = True def clean(self): @@ -71,7 +71,7 @@ def __init__( *args, **kwargs, ): - """Initialize a new EligibilityVerifier form. + """Initialize a new EligibilityVerification form. Args: title (str): The page (i.e. tab) title for the form's page. @@ -92,14 +92,14 @@ def __init__( sub_help_text (str): Extra help text for the sub form field. - name_max_length (int): The maximum length accepted for the 'name' API field before sending to this verifier + name_max_length (int): The maximum length accepted for the 'name' API field before sending an API request sub_input_mode (str): Input mode can be "numeric", "tel", "search", etc. to override default "text" keyboard on mobile devices - sub_max_length (int): The maximum length accepted for the 'sub' API field before sending to this verifier + sub_max_length (int): The maximum length accepted for the 'sub' API field before sending an API request - sub_pattern (str): A regular expression used to validate the 'sub' API field before sending to this verifier + sub_pattern (str): A regular expression used to validate the 'sub' API field before sending an API request Extra args and kwargs are passed through to the underlying django.forms.Form. """ diff --git a/benefits/eligibility/verify.py b/benefits/eligibility/verify.py index 36d465b61..f2402ca44 100644 --- a/benefits/eligibility/verify.py +++ b/benefits/eligibility/verify.py @@ -5,23 +5,23 @@ from benefits.core import models -def eligibility_from_api(verifier: models.EligibilityVerifier, form, agency: models.TransitAgency): +def eligibility_from_api(flow: models.EnrollmentFlow, form, agency: models.TransitAgency): sub, name = form.cleaned_data.get("sub"), form.cleaned_data.get("name") client = Client( - verify_url=verifier.eligibility_api_url, - headers={verifier.eligibility_api_auth_header: verifier.eligibility_api_auth_key}, + verify_url=flow.eligibility_api_url, + headers={flow.eligibility_api_auth_header: flow.eligibility_api_auth_key}, issuer=settings.ALLOWED_HOSTS[0], agency=agency.eligibility_api_id, jws_signing_alg=agency.eligibility_api_jws_signing_alg, client_private_key=agency.eligibility_api_private_key_data, - jwe_encryption_alg=verifier.eligibility_api_jwe_encryption_alg, - jwe_cek_enc=verifier.eligibility_api_jwe_cek_enc, - server_public_key=verifier.eligibility_api_public_key_data, + jwe_encryption_alg=flow.eligibility_api_jwe_encryption_alg, + jwe_cek_enc=flow.eligibility_api_jwe_cek_enc, + server_public_key=flow.eligibility_api_public_key_data, timeout=settings.REQUESTS_TIMEOUT, ) - response = client.verify(sub, name, agency.type_names_to_verify(verifier)) + response = client.verify(sub, name, agency.type_names_to_verify(flow)) if response.error and any(response.error): return None @@ -31,8 +31,8 @@ def eligibility_from_api(verifier: models.EligibilityVerifier, form, agency: mod return [] -def eligibility_from_oauth(verifier, oauth_claim, agency): - if verifier.uses_claims_verification and verifier.claims_claim == oauth_claim: - return agency.type_names_to_verify(verifier) +def eligibility_from_oauth(flow: models.EnrollmentFlow, oauth_claim, agency: models.TransitAgency): + if flow.uses_claims_verification and flow.claims_claim == oauth_claim: + return agency.type_names_to_verify(flow) else: return [] diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py index 21f13b966..dc20a3754 100644 --- a/benefits/eligibility/views.py +++ b/benefits/eligibility/views.py @@ -10,7 +10,7 @@ from benefits.core import recaptcha, session from benefits.core.middleware import AgencySessionRequired, LoginRequired, RecaptchaEnabled, FlowSessionRequired -from benefits.core.models import EligibilityVerifier +from benefits.core.models import EnrollmentFlow from . import analytics, forms, verify @@ -27,7 +27,7 @@ @decorator_from_middleware(RecaptchaEnabled) def index(request, agency=None): - """View handler for the eligibility verifier selection form.""" + """View handler for the enrollment flow selection form.""" if agency is None: # see if session has an agency @@ -43,17 +43,17 @@ def index(request, agency=None): # this may or may not require OAuth, with a different set of scope/claims than what is already stored session.logout(request) - context = {"form": forms.EligibilityVerifierSelectionForm(agency=agency)} + context = {"form": forms.EnrollmentFlowSelectionForm(agency=agency)} if request.method == "POST": - form = forms.EligibilityVerifierSelectionForm(data=request.POST, agency=agency) + form = forms.EnrollmentFlowSelectionForm(data=request.POST, agency=agency) if form.is_valid(): - verifier_id = form.cleaned_data.get("verifier") - verifier = EligibilityVerifier.objects.get(id=verifier_id) - session.update(request, verifier=verifier) + flow_id = form.cleaned_data.get("flow") + flow = EnrollmentFlow.objects.get(id=flow_id) + session.update(request, flow=flow) - types_to_verify = agency.type_names_to_verify(verifier) + types_to_verify = agency.type_names_to_verify(flow) analytics.selected_verifier(request, types_to_verify) eligibility_start = reverse(ROUTE_START) diff --git a/tests/cypress/specs/benefit-select.cy.js b/tests/cypress/specs/benefit-select.cy.js index 75101d78a..f1b74f259 100644 --- a/tests/cypress/specs/benefit-select.cy.js +++ b/tests/cypress/specs/benefit-select.cy.js @@ -1,6 +1,6 @@ const helpers = require("../plugins/helpers"); -const verifier_selection_url = "/eligibility"; +const flow_selection_url = "/eligibility"; describe("Benefit selection", () => { beforeEach(() => { @@ -17,9 +17,9 @@ describe("Benefit selection", () => { it("User must select a radio button, or else see a validation message", () => { cy.get("input:radio").should("have.length", 4); cy.get("input:radio:checked").should("have.length", 0); - cy.get("#form-verifier-selection").submit(); + cy.get("#form-flow-selection").submit(); - cy.url().should("include", verifier_selection_url); + cy.url().should("include", flow_selection_url); cy.get("input:radio:checked").should("have.length", 0); cy.get("input:invalid").should("have.length", 4); cy.get("input:radio") diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index aea57754e..54e46e8d9 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -239,7 +239,7 @@ def test_EligibilityType_enrollment_success_template(): class SampleFormClass: - """A class for testing EligibilityVerifier form references.""" + """A class for testing EligibilityVerificationForm references.""" def __init__(self, *args, **kwargs): self.args = args diff --git a/tests/pytest/eligibility/test_verify.py b/tests/pytest/eligibility/test_verify.py index 401486058..8800504c3 100644 --- a/tests/pytest/eligibility/test_verify.py +++ b/tests/pytest/eligibility/test_verify.py @@ -15,38 +15,36 @@ def mock_api_client_verify(mocker): @pytest.mark.django_db -def test_eligibility_from_api_error(mocker, model_TransitAgency, model_EligibilityVerifier, mock_api_client_verify, form): +def test_eligibility_from_api_error(mocker, model_TransitAgency, model_EnrollmentFlow, mock_api_client_verify, form): api_errors = {"name": "Name error"} api_response = mocker.Mock(error=api_errors) mock_api_client_verify.return_value = api_response - response = eligibility_from_api(model_EligibilityVerifier, form, model_TransitAgency) + response = eligibility_from_api(model_EnrollmentFlow, form, model_TransitAgency) assert response is None @pytest.mark.django_db -def test_eligibility_from_api_verified_types( - mocker, model_TransitAgency, model_EligibilityVerifier, mock_api_client_verify, form -): +def test_eligibility_from_api_verified_types(mocker, model_TransitAgency, model_EnrollmentFlow, mock_api_client_verify, form): verified_types = ["type1", "type2"] api_response = mocker.Mock(eligibility=verified_types, error=None) mock_api_client_verify.return_value = api_response - response = eligibility_from_api(model_EligibilityVerifier, form, model_TransitAgency) + response = eligibility_from_api(model_EnrollmentFlow, form, model_TransitAgency) assert response == verified_types @pytest.mark.django_db def test_eligibility_from_api_no_verified_types( - mocker, model_TransitAgency, model_EligibilityVerifier, mock_api_client_verify, form + mocker, model_TransitAgency, model_EnrollmentFlow, mock_api_client_verify, form ): verified_types = [] api_response = mocker.Mock(eligibility=verified_types, error=None) mock_api_client_verify.return_value = api_response - response = eligibility_from_api(model_EligibilityVerifier, form, model_TransitAgency) + response = eligibility_from_api(model_EnrollmentFlow, form, model_TransitAgency) assert response == verified_types @@ -56,9 +54,9 @@ def test_eligibility_from_oauth_does_not_use_claims_verification( mocked_session_flow_does_not_use_claims_verification, model_TransitAgency ): # mocked_session_flow_does_not_use_claims_verification is Mocked version of the session.flow() function - verifier = mocked_session_flow_does_not_use_claims_verification.return_value + flow = mocked_session_flow_does_not_use_claims_verification.return_value - types = eligibility_from_oauth(verifier, "claim", model_TransitAgency) + types = eligibility_from_oauth(flow, "claim", model_TransitAgency) assert types == [] diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py index ffcb23d0d..611e6a4ee 100644 --- a/tests/pytest/eligibility/test_views.py +++ b/tests/pytest/eligibility/test_views.py @@ -4,7 +4,7 @@ from benefits.core.middleware import TEMPLATE_USER_ERROR import benefits.core.session -from benefits.eligibility.forms import EligibilityVerifierSelectionForm, EligibilityVerificationForm +from benefits.eligibility.forms import EnrollmentFlowSelectionForm, EligibilityVerificationForm from benefits.eligibility.views import ( ROUTE_INDEX, ROUTE_START, @@ -37,9 +37,9 @@ def session_logout_spy(mocker): @pytest.fixture -def mocked_verifier_selection_form(mocker): - mock_form = mocker.Mock(spec=EligibilityVerifierSelectionForm) - mocker.patch("benefits.eligibility.views.forms.EligibilityVerifierSelectionForm", return_value=mock_form) +def mocked_flow_selection_form(mocker): + mock_form = mocker.Mock(spec=EnrollmentFlowSelectionForm) + mocker.patch("benefits.eligibility.views.forms.EnrollmentFlowSelectionForm", return_value=mock_form) @pytest.fixture @@ -70,25 +70,23 @@ def __init__(self, *args, **kwargs): @pytest.fixture -def model_EligibilityVerifier_with_form_class(mocker, model_EligibilityVerifier): - model_EligibilityVerifier.eligibility_form_class = f"{__name__}.SampleVerificationForm" - model_EligibilityVerifier.save() - mocker.patch("benefits.eligibility.views.session.verifier", return_value=model_EligibilityVerifier) - return model_EligibilityVerifier +def model_EnrollmentFlow_with_form_class(mocker, model_EnrollmentFlow): + model_EnrollmentFlow.eligibility_form_class = f"{__name__}.SampleVerificationForm" + model_EnrollmentFlow.save() + mocker.patch("benefits.eligibility.views.session.flow", return_value=model_EnrollmentFlow) + return model_EnrollmentFlow @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency") -def test_index_get_agency_multiple_verifiers( - mocker, model_TransitAgency, model_EligibilityVerifier, mocked_session_agency, client -): - # override the mocked session agency with a mock agency that has multiple verifiers +def test_index_get_agency_multiple_flows(mocker, model_TransitAgency, model_EnrollmentFlow, mocked_session_agency, client): + # override the mocked session agency with a mock agency that has multiple flows mock_agency = mocker.Mock(spec=model_TransitAgency) - # mock the active_verifiers property on the class - https://stackoverflow.com/a/55642462 - type(mock_agency).active_verifiers = mocker.PropertyMock( - return_value=[model_EligibilityVerifier, model_EligibilityVerifier] - ) + # mock the enrollment_flows property on the class - https://stackoverflow.com/a/55642462 + mock_manager = mocker.Mock() + mock_manager.all.return_value = [model_EnrollmentFlow, model_EnrollmentFlow] + type(mock_agency).enrollment_flows = mocker.PropertyMock(return_value=mock_manager) mock_agency.index_url = "/agency" mock_agency.eligibility_index_template = "eligibility/index.html" @@ -100,19 +98,19 @@ def test_index_get_agency_multiple_verifiers( assert response.status_code == 200 assert response.template_name == mock_agency.eligibility_index_template assert "form" in response.context_data - assert isinstance(response.context_data["form"], EligibilityVerifierSelectionForm) + assert isinstance(response.context_data["form"], EnrollmentFlowSelectionForm) @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency") -def test_index_get_agency_single_verifier( - mocker, model_TransitAgency, model_EligibilityVerifier, mocked_session_agency, client -): - # override the mocked session agency with a mock agency that has a single verifier +def test_index_get_agency_single_flow(mocker, model_TransitAgency, model_EnrollmentFlow, mocked_session_agency, client): + # override the mocked session agency with a mock agency that has a single flow mock_agency = mocker.Mock(spec=model_TransitAgency) - # mock the active_verifiers property on the class - https://stackoverflow.com/a/55642462 - type(mock_agency).active_verifiers = mocker.PropertyMock(return_value=[model_EligibilityVerifier]) + # mock the enrollment_flows property on the class - https://stackoverflow.com/a/55642462 + mock_manager = mocker.Mock() + mock_manager.all.return_value = [model_EnrollmentFlow] + type(mock_agency).enrollment_flows = mocker.PropertyMock(return_value=mock_manager) mock_agency.index_url = "/agency" mock_agency.eligibility_index_template = "eligibility/index.html" @@ -124,7 +122,7 @@ def test_index_get_agency_single_verifier( assert response.status_code == 200 assert response.template_name == mock_agency.eligibility_index_template assert "form" in response.context_data - assert isinstance(response.context_data["form"], EligibilityVerifierSelectionForm) + assert isinstance(response.context_data["form"], EnrollmentFlowSelectionForm) @pytest.mark.django_db @@ -149,14 +147,14 @@ def test_index_post_invalid_form(client): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency") -def test_index_post_valid_form(client, model_EligibilityVerifier, mocked_session_update, mocked_analytics_module): +def test_index_post_valid_form(client, model_EnrollmentFlow, mocked_session_update, mocked_analytics_module): path = reverse(ROUTE_INDEX) - response = client.post(path, {"verifier": model_EligibilityVerifier.id}) + response = client.post(path, {"flow": model_EnrollmentFlow.id}) assert response.status_code == 302 assert response.url == reverse(ROUTE_START) - assert mocked_session_update.call_args.kwargs["verifier"] == model_EligibilityVerifier + assert mocked_session_update.call_args.kwargs["flow"] == model_EnrollmentFlow mocked_analytics_module.selected_verifier.assert_called_once() @@ -171,10 +169,8 @@ def test_index_calls_session_logout(client, session_logout_spy): @pytest.mark.django_db -@pytest.mark.usefixtures( - "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_flow_uses_claims_verification" -) -def test_start_verifier_uses_claims_verification_logged_in(mocker, client): +@pytest.mark.usefixtures("mocked_session_agency", "mocked_flow_selection_form", "mocked_session_flow_uses_claims_verification") +def test_start_flow_uses_claims_verification_logged_in(mocker, client): mock_session = mocker.patch("benefits.eligibility.views.session") mock_session.logged_in.return_value = True @@ -185,10 +181,8 @@ def test_start_verifier_uses_claims_verification_logged_in(mocker, client): @pytest.mark.django_db -@pytest.mark.usefixtures( - "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_flow_uses_claims_verification" -) -def test_start_verifier_uses_claims_verification_not_logged_in(mocker, client): +@pytest.mark.usefixtures("mocked_session_agency", "mocked_flow_selection_form", "mocked_session_flow_uses_claims_verification") +def test_start_flow_uses_claims_verification_not_logged_in(mocker, client): mock_session = mocker.patch("benefits.eligibility.views.session") mock_session.logged_in.return_value = False @@ -200,9 +194,9 @@ def test_start_verifier_uses_claims_verification_not_logged_in(mocker, client): @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_verifier_selection_form", "mocked_session_flow_does_not_use_claims_verification" + "mocked_session_agency", "mocked_flow_selection_form", "mocked_session_flow_does_not_use_claims_verification" ) -def test_start_verifier_does_not_use_claims_verification(client): +def test_start_flow_does_not_use_claims_verification(client): path = reverse(ROUTE_START) response = client.get(path) @@ -211,7 +205,7 @@ def test_start_verifier_does_not_use_claims_verification(client): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency") -def test_start_without_verifier(client): +def test_start_without_flow(client): path = reverse(ROUTE_START) response = client.get(path) @@ -272,7 +266,7 @@ def test_confirm_get_oauth_unverified(mocker, client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EligibilityVerifier_with_form_class") +@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EnrollmentFlow_with_form_class") def test_confirm_post_invalid_form(client, invalid_form_data, mocked_analytics_module): path = reverse(ROUTE_CONFIRM) response = client.post(path, invalid_form_data) @@ -283,9 +277,7 @@ def test_confirm_post_invalid_form(client, invalid_form_data, mocked_analytics_m @pytest.mark.django_db -@pytest.mark.usefixtures( - "mocked_analytics_module", "mocked_eligibility_auth_request", "model_EligibilityVerifier_with_form_class" -) +@pytest.mark.usefixtures("mocked_analytics_module", "mocked_eligibility_auth_request", "model_EnrollmentFlow_with_form_class") def test_confirm_post_recaptcha_fail(mocker, client, invalid_form_data): mocker.patch("benefits.eligibility.views.recaptcha.has_error", return_value=True) messages = mocker.spy(benefits.eligibility.views, "messages") @@ -299,7 +291,7 @@ def test_confirm_post_recaptcha_fail(mocker, client, invalid_form_data): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EligibilityVerifier_with_form_class") +@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EnrollmentFlow_with_form_class") def test_confirm_post_valid_form_eligibility_error(mocker, client, form_data, mocked_analytics_module): mocker.patch("benefits.eligibility.verify.eligibility_from_api", return_value=None) @@ -312,7 +304,7 @@ def test_confirm_post_valid_form_eligibility_error(mocker, client, form_data, mo @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EligibilityVerifier_with_form_class") +@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EnrollmentFlow_with_form_class") def test_confirm_post_valid_form_eligibility_unverified(mocker, client, form_data): mocker.patch("benefits.eligibility.verify.eligibility_from_api", return_value=[]) @@ -324,7 +316,7 @@ def test_confirm_post_valid_form_eligibility_unverified(mocker, client, form_dat @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EligibilityVerifier_with_form_class") +@pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EnrollmentFlow_with_form_class") def test_confirm_post_valid_form_eligibility_verified( mocker, client, form_data, mocked_session_eligibility, mocked_session_update, mocked_analytics_module ): From ae44bd967108590fa226f634c2a0ab2590c365a4 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 8 Aug 2024 03:25:33 +0000 Subject: [PATCH 5/8] docs: verifier renamed to flow --- docs/configuration/oauth.md | 9 +++++---- docs/configuration/transit-agency.md | 20 ++++++++++---------- docs/development/commits-branches-merging.md | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/configuration/oauth.md b/docs/configuration/oauth.md index 599e9e14a..6b92cdd59 100644 --- a/docs/configuration/oauth.md +++ b/docs/configuration/oauth.md @@ -22,9 +22,9 @@ for more details about what features are available. Specifically, from Authlib w ## Django configuration -OAuth settings are configured as instances of the [`ClaimsProvider` model](../development/models-migrations.md) and [`EligibilityVerifier` model](../development/models-migrations.md). +OAuth settings are configured as instances of the [`ClaimsProvider` model](../development/models-migrations.md) and [`EnrollmentFlow` model](../development/models-migrations.md). -The [data migration file](./data.md) contains sample values for a `ClaimsProvider` and `EligibilityVerifier` configuration. You can set values for a real Open ID Connect provider in environment variables so that they are used instead of the sample values. +The [data migration file](./data.md) contains sample values for a `ClaimsProvider` and `EnrollmentFlow` configuration. You can set values for a real Open ID Connect provider in environment variables so that they are used instead of the sample values. ## Django usage @@ -33,7 +33,8 @@ use in e.g. views. - `oauth` is an `authlib.integrations.django_client.OAuth` instance -Consumers call `benefits.oauth.client.create_client(oauth, verifier)` with the name of a client to obtain an Authlib client -instance. If that client name has not been registered yet, `_register_provider(oauth_registry, verifier)` uses data from the given `ClaimsProvider` and `EligibilityVerifier` instances to register the client into this instance and returns the client object. +Consumers call `benefits.oauth.client.create_client(oauth, flow)` with an `EnrollmentFlow` to obtain an Authlib client +instance. If a client for the flow has not been registered yet, `_register_provider(oauth_registry, flow)` uses data from the +given `EnrollmentFlow` to register the client into this instance and returns the client object. [oauth-client]: https://github.com/cal-itp/benefits/blob/main/benefits/oauth/client.py diff --git a/docs/configuration/transit-agency.md b/docs/configuration/transit-agency.md index 3fbd34289..680baffb5 100644 --- a/docs/configuration/transit-agency.md +++ b/docs/configuration/transit-agency.md @@ -7,15 +7,15 @@ Then, the following steps are done by the Cal-ITP team to configure a new transi Note that a `TransitAgency` model requires: - a list of supported `EligibilityType`s -- a list of `EligibilityVerifier`s used to verify one of those supported eligibility types +- a list of `EnrollmentFlows`s available to the agency's users - a `TransitProcessor` for enrolling the user's contactless card for discounts - an `info_url` and `phone` for users to contact customer service - an SVG or PNG file of the transit agency's logo - HTML templates for various buttons, text and other user interface elements of the flow, including: - `index_template`: _Required for agencies_ - Text for agency direct entry page - `eligibility_index_template`: _Required for agencies_ - Text for Eligibility Index page - - `selection_label_template`: _Required for verifiers_ - Text and optional modals for the radio button form on the Eligibility Index page - - `eligibility_start_template`: _Required for verifiers_ - Text and optional custom styles for call to action button on the Eligibility Start page + - `selection_label_template`: _Required for enrollment flows_ - Text and optional modals for the radio button form on the Eligibility Index page + - `eligibility_start_template`: _Required for enrollment flows_ - Text and optional custom styles for call to action button on the Eligibility Start page - `enrollment_success_template`: _Required for agencies_ - Text for Enrollment Success page - `help_template`: _Required for agencies_ - Agency-specific help questions and answers - `sign_out_button_template`: _Required for claims providers_ - Sign out link button, used on any page after sign in @@ -31,8 +31,8 @@ For development and testing, only a Littlepay customer group is needed since the 1. Cal-ITP uses the transit agency's Littlepay merchant ID to create a customer group in the Littlepay QA environment for each type of eligibility (e.g. senior). 1. For each group that's created, a group ID will be returned and should be set as the `group_id` on a new `EligibilityType` in the Benefits database. (See [Configuration data](../data/) for more on loading the database.) -1. Cal-ITP creates a new `EligibilityVerifier` in the database for each supported eligibility type. This will require configuration for either [API](https://docs.calitp.org/eligibility-api/specification/)-based verification or verification through an [OAuth Open ID Connect provider](../oauth/) (e.g. sandbox Login.gov) -- either way, this resource should be meant for testing. -1. Cal-ITP creates a new `TransitAgency` in the database and associates it with the new `EligibilityType`s and `EligibilityVerifier`s as well as the existing Littlepay `TransitProcessor`. +1. Cal-ITP creates a new `EnrollmentFlow` in the database for each supported eligibility type. This will require configuration for either [API](https://docs.calitp.org/eligibility-api/specification/)-based verification or verification through an [OAuth Open ID Connect claims provider](../oauth/) (e.g. sandbox Login.gov) -- either way, this resource should be meant for testing. +1. Cal-ITP creates a new `TransitAgency` in the database and associates it with the new `EligibilityType`s and `EnrollmentFlow`s as well as the existing Littlepay `TransitProcessor`. ## Configuration for production validation @@ -45,9 +45,9 @@ For production validation, both a customer group and discount product are needed 1. Cal-ITP creates a customer group **for testing purposes** in production Littlepay. 1. Cal-ITP associates the group with the product. 1. Cal-ITP creates a new `EligibilityType` **for testing purposes** in the Benefits database and sets the `group_id` to the ID of the newly-created group. -1. Cal-ITP creates a new `EligibilityVerifier` with configuration **for a testing environment** to ensure successful eligibility verification. (For example, use sandbox Login.gov instead of production Login.gov.) +1. Cal-ITP creates a new `EnrollmentFlow` with configuration **for a testing environment** to ensure successful eligibility verification. (For example, use sandbox Login.gov instead of production Login.gov.) 1. Cal-ITP creates a new `TransitProcessor` **for testing purposes** with configuration for production Littlepay. -1. Cal-ITP updates the existing `TransitAgency` (created [previously](#configuration-for-development-and-testing)) with associations to the eligibility types, verifiers, and transit processor that were just created for testing. +1. Cal-ITP updates the existing `TransitAgency` (created [previously](#configuration-for-development-and-testing)) with associations to the eligibility types, enrollment flows, and transit processor that were just created for testing. At this point, Cal-ITP and transit agency staff can coordinate to do on-the-ground testing where a live card is tapped on a live payment validator. @@ -67,8 +67,8 @@ Once production validation is done, the transit agency can be added to the produ 1. Cal-ITP creates a customer group **for production use** in production Littlepay. 1. Cal-ITP associates the group with the discount product created [previously during production validation](#configuration-for-production-validation). 1. Cal-ITP sets that group's ID as the `group_id` for a new `EligibilityType` in the Benefits database. -1. Cal-ITP creates a new `EligibilityVerifier` with configuration for the **production** eligibility verification system. -1. Cal-ITP creates a new `TransitAgency` in the database with proper associations to eligibility types, verifiers, and transit processor. +1. Cal-ITP creates a new `EnrollmentFlow` with configuration for the **production** eligibility verification system. +1. Cal-ITP creates a new `TransitAgency` in the database with proper associations to eligibility types, enrollment flows, and transit processor. ### Cleanup @@ -76,4 +76,4 @@ At this point, the customer group that was created in production Littlepay for t 1. Remove the association between the test customer group and discount product. 1. Delete the test customer group. -1. Remove temporary `EligibilityType`s, `EligibilityVerifier`s, and `TransitProcessor` that were [created](#steps_1) in the Benefits test environment. +1. Remove temporary `EligibilityType`s, `EnrollmentFlow`s, and `TransitProcessor` that were [created](#steps_1) in the Benefits test environment. diff --git a/docs/development/commits-branches-merging.md b/docs/development/commits-branches-merging.md index 63e2ee8c7..6fb9decae 100644 --- a/docs/development/commits-branches-merging.md +++ b/docs/development/commits-branches-merging.md @@ -65,13 +65,13 @@ PR branches are typically named with a [conventional type][conventional-commits] E.g. ```bash -git checkout -b feat/verifier-radio-buttons +git checkout -b feat/flow-multi-select ``` and ```bash -git checkout -b refactor/verifier-model +git checkout -b refactor/flow-model ``` PR branches are deleted once their PR is merged. From 79eeeeed8530b4b56e36f94c0853b211b1568608 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 8 Aug 2024 03:56:25 +0000 Subject: [PATCH 6/8] chore(models): add help_text for EnrollmentFlow fields --- ...name_eligibilityverifier_enrollmentflow.py | 128 +++++++++++++++++- benefits/core/models.py | 85 +++++++++--- 2 files changed, 193 insertions(+), 20 deletions(-) diff --git a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py index 10917f04f..77b361419 100644 --- a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py +++ b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py @@ -1,6 +1,9 @@ # Generated by Django 5.0.7 on 2024-08-07 21:22 -from django.db import migrations +import benefits.core.models +import benefits.secrets +import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -23,4 +26,127 @@ class Migration(migrations.Migration): model_name="enrollmentflow", name="active", ), + migrations.AlterField( + model_name="enrollmentflow", + name="claims_provider", + field=models.ForeignKey( + blank=True, + help_text="An entity that provides claims for eligibility verification for this flow.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="core.claimsprovider", + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_auth_header", + field=models.TextField( + blank=True, help_text="The auth header to send in Eligibility API requests for this flow.", null=True + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_auth_key_secret_name", + field=benefits.core.models.SecretNameField( + blank=True, + help_text="The name of a secret containing the value of the auth header to send in Eligibility API requests for this flow.", # noqa: E501 + max_length=127, + null=True, + validators=[benefits.secrets.SecretNameValidator()], + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_jwe_cek_enc", + field=models.TextField( + blank=True, + help_text="The JWE-compatible Content Encryption Key (CEK) key-length and mode to use in Eligibility API requests for this flow.", # noqa: E501 + null=True, + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_jwe_encryption_alg", + field=models.TextField( + blank=True, + help_text="The JWE-compatible encryption algorithm to use in Eligibility API requests for this flow.", + null=True, + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_jws_signing_alg", + field=models.TextField( + blank=True, + help_text="The JWS-compatible signing algorithm to use in Eligibility API requests for this flow.", + null=True, + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_public_key", + field=models.ForeignKey( + blank=True, + help_text="The public key used to encrypt Eligibility API requests and to verify signed Eligibility API responses for this flow.", # noqa: E501 + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="core.pemdata", + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_api_url", + field=models.TextField( + blank=True, help_text="Fully qualified URL for an Eligibility API server used by this flow.", null=True + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_form_class", + field=models.TextField( + blank=True, + help_text="The fully qualified Python path of a form class used by this flow, e.g. benefits.eligibility.forms.FormClass", # noqa: E501 + null=True, + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_start_template", + field=models.TextField( + default="eligibility/start.html", + help_text="Path to a Django template for the informational page of this flow.", + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="eligibility_unverified_template", + field=models.TextField( + default="eligibility/unverified.html", + help_text="Path to a Django template that defines the page when a user fails eligibility verification for this flow.", # noqa: E501 + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="help_template", + field=models.TextField( + blank=True, + help_text="Path to a Django template that defines the help text for this enrollment flow, used in building the dynamic help page for an agency", # noqa: E501 + null=True, + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="name", + field=models.TextField( + help_text="Primary internal system name for this EnrollmentFlow instance, e.g. in analytics and Eligibility API requests." # noqa: E501 + ), + ), + migrations.AlterField( + model_name="enrollmentflow", + name="selection_label_template", + field=models.TextField( + help_text="Path to a Django template that defines the end-user UI for selecting this flow among other options." + ), + ), ] diff --git a/benefits/core/models.py b/benefits/core/models.py index 1d7d6c39c..ff1b3543d 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -160,27 +160,18 @@ class EnrollmentFlow(models.Model): """Represents a user journey through the Benefits app for a single eligibility type.""" id = models.AutoField(primary_key=True) - name = models.TextField() + name = models.TextField( + help_text="Primary internal system name for this EnrollmentFlow instance, e.g. in analytics and Eligibility API requests." # noqa: 501 + ) display_order = models.PositiveSmallIntegerField(default=0, blank=False, null=False) - eligibility_api_url = models.TextField(null=True, blank=True) - eligibility_api_auth_header = models.TextField(null=True, blank=True) - eligibility_api_auth_key_secret_name = SecretNameField(null=True, blank=True) eligibility_type = models.ForeignKey(EligibilityType, on_delete=models.PROTECT) - # public key is used to encrypt Eligibility API requests and to verify signed Eligibility API responses - eligibility_api_public_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT, null=True, blank=True) - # The JWE-compatible Content Encryption Key (CEK) key-length and mode - eligibility_api_jwe_cek_enc = models.TextField(null=True, blank=True) - # The JWE-compatible encryption algorithm - eligibility_api_jwe_encryption_alg = models.TextField(null=True, blank=True) - # The JWS-compatible signing algorithm - eligibility_api_jws_signing_alg = models.TextField(null=True, blank=True) - claims_provider = models.ForeignKey(ClaimsProvider, on_delete=models.PROTECT, null=True, blank=True) - selection_label_template = models.TextField() - eligibility_start_template = models.TextField(default="eligibility/start.html") - # reference to a form class used by this flow, e.g. benefits.eligibility.forms.FormClass - eligibility_form_class = models.TextField(null=True, blank=True) - eligibility_unverified_template = models.TextField(default="eligibility/unverified.html") - help_template = models.TextField(null=True, blank=True) + claims_provider = models.ForeignKey( + ClaimsProvider, + on_delete=models.PROTECT, + null=True, + blank=True, + help_text="An entity that provides claims for eligibility verification for this flow.", + ) claims_scope = models.TextField( null=True, blank=True, @@ -196,6 +187,62 @@ class EnrollmentFlow(models.Model): blank=True, verbose_name="Claims scheme", ) + eligibility_api_url = models.TextField( + null=True, blank=True, help_text="Fully qualified URL for an Eligibility API server used by this flow." + ) + eligibility_api_auth_header = models.TextField( + null=True, + blank=True, + help_text="The auth header to send in Eligibility API requests for this flow.", + ) + eligibility_api_auth_key_secret_name = SecretNameField( + null=True, + blank=True, + help_text="The name of a secret containing the value of the auth header to send in Eligibility API requests for this flow.", # noqa: 501 + ) + eligibility_api_public_key = models.ForeignKey( + PemData, + related_name="+", + on_delete=models.PROTECT, + null=True, + blank=True, + help_text="The public key used to encrypt Eligibility API requests and to verify signed Eligibility API responses for this flow.", # noqa: E501 + ) + eligibility_api_jwe_cek_enc = models.TextField( + null=True, + blank=True, + help_text="The JWE-compatible Content Encryption Key (CEK) key-length and mode to use in Eligibility API requests for this flow.", # noqa: E501 + ) + eligibility_api_jwe_encryption_alg = models.TextField( + null=True, + blank=True, + help_text="The JWE-compatible encryption algorithm to use in Eligibility API requests for this flow.", + ) + eligibility_api_jws_signing_alg = models.TextField( + null=True, + blank=True, + help_text="The JWS-compatible signing algorithm to use in Eligibility API requests for this flow.", + ) + selection_label_template = models.TextField( + help_text="Path to a Django template that defines the end-user UI for selecting this flow among other options." + ) + eligibility_start_template = models.TextField( + default="eligibility/start.html", help_text="Path to a Django template for the informational page of this flow." + ) + eligibility_form_class = models.TextField( + null=True, + blank=True, + help_text="The fully qualified Python path of a form class used by this flow, e.g. benefits.eligibility.forms.FormClass", # noqa: E501 + ) + eligibility_unverified_template = models.TextField( + default="eligibility/unverified.html", + help_text="Path to a Django template that defines the page when a user fails eligibility verification for this flow.", + ) + help_template = models.TextField( + null=True, + blank=True, + help_text="Path to a Django template that defines the help text for this enrollment flow, used in building the dynamic help page for an agency", # noqa: E501 + ) class Meta: ordering = ["display_order"] From 7e85dca053bde009945758241868184254b5642f Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 8 Aug 2024 04:23:03 +0000 Subject: [PATCH 7/8] chore(admin): update permission names for EnrollmentFlow --- ...name_eligibilityverifier_enrollmentflow.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py index 77b361419..6f6b1408b 100644 --- a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py +++ b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py @@ -1,10 +1,37 @@ # Generated by Django 5.0.7 on 2024-08-07 21:22 -import benefits.core.models -import benefits.secrets +from django.contrib.auth.management import create_permissions import django.db.models.deletion from django.db import migrations, models +import benefits.core.models +import benefits.secrets + + +def create_all_permissions(apps, schema_editor): + for app_config in apps.get_app_configs(): + app_config.models_module = True + create_permissions(app_config, apps=apps, verbosity=0) + app_config.models_module = None + + +def update_permissions(apps, schema_editor): + Group = apps.get_model("auth", "Group") + staff_group = Group.objects.get(name="Cal-ITP") + + Permission = apps.get_model("auth", "Permission") + + remove_permissions = ["Can view", "Can change", "Can add", "Can delete"] + for remove_permission in remove_permissions: + current_permission = Permission.objects.get(name=f"{remove_permission} eligibility verifier") + staff_group.permissions.remove(current_permission) + current_permission.delete() + + add_permissions = ["Can view", "Can change"] + for add_permission in add_permissions: + new_permission = Permission.objects.get(name=f"{add_permission} enrollment flow") + staff_group.permissions.add(new_permission) + class Migration(migrations.Migration): @@ -149,4 +176,6 @@ class Migration(migrations.Migration): help_text="Path to a Django template that defines the end-user UI for selecting this flow among other options." ), ), + migrations.RunPython(create_all_permissions), + migrations.RunPython(update_permissions), ] From bbf7ff8daea76f9ea9ecd2720cadba7044662ec9 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 8 Aug 2024 04:27:50 +0000 Subject: [PATCH 8/8] refactor(migrations): centralize permissions helper --- .../core/migrations/0014_staff_group_view_permissions.py | 8 +------- .../0016_refactor_paymentprocessor_transitprocessor.py | 9 +-------- .../0017_refactor_authprovider_claimsprovider.py | 9 +-------- .../0021_rename_eligibilityverifier_enrollmentflow.py | 9 +-------- benefits/core/migrations/__init__.py | 8 ++++++++ 5 files changed, 12 insertions(+), 31 deletions(-) diff --git a/benefits/core/migrations/0014_staff_group_view_permissions.py b/benefits/core/migrations/0014_staff_group_view_permissions.py index 384108372..b70c6e29a 100644 --- a/benefits/core/migrations/0014_staff_group_view_permissions.py +++ b/benefits/core/migrations/0014_staff_group_view_permissions.py @@ -1,12 +1,6 @@ -from django.contrib.auth.management import create_permissions from django.db import migrations - -def create_all_permissions(apps, schema_editor): - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None +from benefits.core.migrations import create_all_permissions def add_view_permissions(apps, schema_editor): diff --git a/benefits/core/migrations/0016_refactor_paymentprocessor_transitprocessor.py b/benefits/core/migrations/0016_refactor_paymentprocessor_transitprocessor.py index 33426cbab..57ea984f2 100644 --- a/benefits/core/migrations/0016_refactor_paymentprocessor_transitprocessor.py +++ b/benefits/core/migrations/0016_refactor_paymentprocessor_transitprocessor.py @@ -1,19 +1,12 @@ # Generated by Django 5.0.7 on 2024-07-31 22:41 -from django.contrib.auth.management import create_permissions from django.db import migrations, models +from benefits.core.migrations import create_all_permissions import benefits.core.models import benefits.secrets -def create_all_permissions(apps, schema_editor): - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - def update_permissions(apps, schema_editor): Group = apps.get_model("auth", "Group") staff_group = Group.objects.get(name="Cal-ITP") diff --git a/benefits/core/migrations/0017_refactor_authprovider_claimsprovider.py b/benefits/core/migrations/0017_refactor_authprovider_claimsprovider.py index 7e8da3543..d03c63f4f 100644 --- a/benefits/core/migrations/0017_refactor_authprovider_claimsprovider.py +++ b/benefits/core/migrations/0017_refactor_authprovider_claimsprovider.py @@ -1,19 +1,12 @@ # Generated by Django 5.0.7 on 2024-08-02 22:52 -from django.contrib.auth.management import create_permissions from django.db import migrations, models +from benefits.core.migrations import create_all_permissions import benefits.core.models import benefits.secrets -def create_all_permissions(apps, schema_editor): - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None - - def update_permissions(apps, schema_editor): # delete old permissions Permission = apps.get_model("auth", "Permission") diff --git a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py index 6f6b1408b..d9b571dd3 100644 --- a/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py +++ b/benefits/core/migrations/0021_rename_eligibilityverifier_enrollmentflow.py @@ -1,18 +1,11 @@ # Generated by Django 5.0.7 on 2024-08-07 21:22 -from django.contrib.auth.management import create_permissions import django.db.models.deletion from django.db import migrations, models import benefits.core.models import benefits.secrets - - -def create_all_permissions(apps, schema_editor): - for app_config in apps.get_app_configs(): - app_config.models_module = True - create_permissions(app_config, apps=apps, verbosity=0) - app_config.models_module = None +from benefits.core.migrations import create_all_permissions def update_permissions(apps, schema_editor): diff --git a/benefits/core/migrations/__init__.py b/benefits/core/migrations/__init__.py index e69de29bb..719d43b2b 100644 --- a/benefits/core/migrations/__init__.py +++ b/benefits/core/migrations/__init__.py @@ -0,0 +1,8 @@ +from django.contrib.auth.management import create_permissions + + +def create_all_permissions(apps, schema_editor): + for app_config in apps.get_app_configs(): + app_config.models_module = True + create_permissions(app_config, apps=apps, verbosity=0) + app_config.models_module = None