From 05efa816450b31290f5e5ab345bdd3a7ac32dad1 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Apr 2026 21:12:37 -0500 Subject: [PATCH 01/20] Set encumbrance type and categories to Cosmetology values --- .../cc_common/data_model/schema/common.py | 28 ++----------- .../common/common_test/test_constants.py | 2 +- .../resources/api/adverse-action-post.json | 2 +- .../test_schema/test_adverse_action.py | 2 +- .../test_schema/test_investigation.py | 2 +- .../test_handlers/test_encumbrance.py | 9 +--- .../test_handlers/test_investigation.py | 2 +- .../stacks/api_stack/v1_api/api_model.py | 41 +++++++------------ .../search_api_stack/v1_api/api_model.py | 5 ++- .../GET_PROVIDER_RESPONSE_SCHEMA.json | 10 +++++ .../LICENSE_ENCUMBRANCE_REQUEST_SCHEMA.json | 19 +++------ ..._LICENSE_INVESTIGATION_REQUEST_SCHEMA.json | 19 +++------ ...RIVILEGE_INVESTIGATION_REQUEST_SCHEMA.json | 19 +++------ .../PRIVILEGE_ENCUMBRANCE_REQUEST_SCHEMA.json | 19 +++------ .../PROVIDER_USER_RESPONSE_SCHEMA.json | 12 +++++- .../tests/smoke/encumbrance_smoke_tests.py | 26 +++++------- .../tests/smoke/investigation_smoke_tests.py | 4 +- 17 files changed, 87 insertions(+), 134 deletions(-) diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/common.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/common.py index 84441666c..957b2ae88 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/common.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/common.py @@ -378,37 +378,17 @@ class EncumbranceType(CCEnum): Enum for the allowed types of encumbrances """ - FINE = 'fine' - REPRIMAND = 'reprimand' - REQUIRED_SUPERVISION = 'required supervision' - COMPLETION_OF_CONTINUING_EDUCATION = 'completion of continuing education' - PUBLIC_REPRIMAND = 'public reprimand' - PROBATION = 'probation' - INJUNCTIVE_ACTION = 'injunctive action' SUSPENSION = 'suspension' REVOCATION = 'revocation' - DENIAL = 'denial' SURRENDER_OF_LICENSE = 'surrender of license' - MODIFICATION_OF_PREVIOUS_ACTION_EXTENSION = 'modification of previous action-extension' - MODIFICATION_OF_PREVIOUS_ACTION_REDUCTION = 'modification of previous action-reduction' - OTHER_MONITORING = 'other monitoring' - OTHER_ADJUDICATED_ACTION_NOT_LISTED = 'other adjudicated action not listed' class ClinicalPrivilegeActionCategory(CCEnum): - """ - Enum for the category of clinical privileges actions, as defined by NPDB: - https://www.npdb.hrsa.gov/software/CodeLists.pdf, Tables 41-45 - """ + """Enum for adverse action clinical privilege action categories.""" - NON_COMPLIANCE = 'Non-compliance With Requirements' - CRIMINAL_CONVICTION = 'Criminal Conviction or Adjudication' - CONFIDENTIALITY_VIOLATION = 'Confidentiality, Consent or Disclosure Violations' - MISCONDUCT_ABUSE = 'Misconduct or Abuse' - FRAUD = 'Fraud, Deception, or Misrepresentation' - UNSAFE_PRACTICE = 'Unsafe Practice or Substandard Care' - IMPROPER_SUPERVISION = 'Improper Supervision or Allowing Unlicensed Practice' - OTHER = 'Other' + FRAUD = 'fraud' + CONSUMER_HARM = 'consumer harm' + OTHER = 'other' class ChangeHashMixin: diff --git a/backend/cosmetology-app/lambdas/python/common/common_test/test_constants.py b/backend/cosmetology-app/lambdas/python/common/common_test/test_constants.py index 36d936fe9..2b5a50bf0 100644 --- a/backend/cosmetology-app/lambdas/python/common/common_test/test_constants.py +++ b/backend/cosmetology-app/lambdas/python/common/common_test/test_constants.py @@ -92,7 +92,7 @@ DEFAULT_ACTION_AGAINST_PRIVILEGE = 'privilege' DEFAULT_BLOCKS_FUTURE_PRIVILEGES = True DEFAULT_ENCUMBRANCE_TYPE = 'suspension' -DEFAULT_CLINICAL_PRIVILEGE_ACTION_CATEGORY = 'Unsafe Practice or Substandard Care' +DEFAULT_CLINICAL_PRIVILEGE_ACTION_CATEGORY = 'fraud' DEFAULT_CREATION_EFFECTIVE_DATE = '2024-02-15' DEFAULT_CREATION_DATE = '2024-02-15T10:30:00+00:00' DEFAULT_AA_SUBMITTING_USER_ID = '12a6377e-c3a5-40e5-bca5-317ec854c556' diff --git a/backend/cosmetology-app/lambdas/python/common/tests/resources/api/adverse-action-post.json b/backend/cosmetology-app/lambdas/python/common/tests/resources/api/adverse-action-post.json index bbd70db8a..d7ecff1b8 100644 --- a/backend/cosmetology-app/lambdas/python/common/tests/resources/api/adverse-action-post.json +++ b/backend/cosmetology-app/lambdas/python/common/tests/resources/api/adverse-action-post.json @@ -1,5 +1,5 @@ { "encumbranceEffectiveDate": "2023-01-15", "encumbranceType": "suspension", - "clinicalPrivilegeActionCategories": ["Unsafe Practice or Substandard Care"] + "clinicalPrivilegeActionCategories": ["fraud"] } diff --git a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_adverse_action.py b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_adverse_action.py index 12a22302f..d23ec014b 100644 --- a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_adverse_action.py +++ b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_adverse_action.py @@ -111,7 +111,7 @@ def test_adverse_action_data_class_outputs_expected_database_object(self): 'actionAgainst': 'privilege', 'adverseActionId': '98765432-9876-9876-9876-987654321098', 'encumbranceType': 'suspension', - 'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'], + 'clinicalPrivilegeActionCategories': ['fraud'], 'compact': 'cosm', 'creationDate': '2024-11-08T23:59:59+00:00', 'effectiveStartDate': '2024-02-15', diff --git a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_investigation.py b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_investigation.py index f2289ef7e..c3e33204f 100644 --- a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_investigation.py +++ b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_investigation.py @@ -143,7 +143,7 @@ def test_validate_patch_with_encumbrance(self): 'encumbrance': { 'encumbranceEffectiveDate': '2024-03-15', 'encumbranceType': 'suspension', - 'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'], + 'clinicalPrivilegeActionCategories': ['consumer harm'], } } result = InvestigationPatchRequestSchema().load(investigation_data) diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index 2830ae4d2..b8184ecd6 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -9,7 +9,6 @@ DEFAULT_AA_SUBMITTING_USER_ID, DEFAULT_COMPACT, DEFAULT_DATE_OF_UPDATE_TIMESTAMP, - DEFAULT_ENCUMBRANCE_TYPE, DEFAULT_LICENSE_JURISDICTION, DEFAULT_LICENSE_TYPE, DEFAULT_LICENSE_TYPE_ABBREVIATION, @@ -44,13 +43,11 @@ def _generate_test_body(): - from cc_common.data_model.schema.common import ClinicalPrivilegeActionCategory, EncumbranceType + from cc_common.data_model.schema.common import ClinicalPrivilegeActionCategory return { 'encumbranceEffectiveDate': TEST_ENCUMBRANCE_EFFECTIVE_DATE, - # These Enums are expected to be `str` type, so we'll directly access their .value - 'encumbranceType': EncumbranceType.SUSPENSION.value, - 'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.UNSAFE_PRACTICE.value], + 'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.FRAUD.value], } @@ -127,7 +124,6 @@ def test_privilege_encumbrance_handler_adds_adverse_action_record_in_provider_da default_adverse_action_encumbrance = self.test_data_generator.generate_default_adverse_action( value_overrides={ 'adverseActionId': item['adverseActionId'], - 'encumbranceType': DEFAULT_ENCUMBRANCE_TYPE, 'effectiveStartDate': date.fromisoformat(TEST_ENCUMBRANCE_EFFECTIVE_DATE), 'jurisdiction': DEFAULT_PRIVILEGE_JURISDICTION, } @@ -308,7 +304,6 @@ def test_license_encumbrance_handler_adds_adverse_action_record_in_provider_data value_overrides={ 'actionAgainst': 'license', 'adverseActionId': item['adverseActionId'], - 'encumbranceType': DEFAULT_ENCUMBRANCE_TYPE, 'effectiveStartDate': date.fromisoformat(TEST_ENCUMBRANCE_EFFECTIVE_DATE), 'jurisdiction': DEFAULT_LICENSE_JURISDICTION, } diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_investigation.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_investigation.py index 14992b274..62d8fcbfd 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_investigation.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_investigation.py @@ -44,7 +44,7 @@ def _generate_test_investigation_close_with_encumbrance_body(): 'encumbranceEffectiveDate': TEST_ENCUMBRANCE_EFFECTIVE_DATE, # These Enums are expected to be `str` type, so we'll directly access their .value 'encumbranceType': EncumbranceType.SUSPENSION.value, - 'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.UNSAFE_PRACTICE.value], + 'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.FRAUD.value], }, } diff --git a/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py b/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py index e4972ccf1..9671a0b99 100644 --- a/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py +++ b/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py @@ -648,11 +648,7 @@ def _provider_detail_response_schema(self): ), 'dateOfUpdate': JsonSchema(type=JsonSchemaType.STRING, format='date-time'), 'encumbranceType': JsonSchema(type=JsonSchemaType.STRING), - 'clinicalPrivilegeActionCategories': JsonSchema( - type=JsonSchemaType.ARRAY, - description='The categories of clinical privilege action', - items=JsonSchema(type=JsonSchemaType.STRING), - ), + 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, 'liftingUser': JsonSchema(type=JsonSchemaType.STRING), }, ), @@ -782,11 +778,7 @@ def _provider_detail_response_schema(self): ), 'dateOfUpdate': JsonSchema(type=JsonSchemaType.STRING, format='date-time'), 'encumbranceType': JsonSchema(type=JsonSchemaType.STRING), - 'clinicalPrivilegeActionCategories': JsonSchema( - type=JsonSchemaType.ARRAY, - description='The categories of clinical privilege action', - items=JsonSchema(type=JsonSchemaType.STRING), - ), + 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, 'liftingUser': JsonSchema(type=JsonSchemaType.STRING), }, ), @@ -825,6 +817,17 @@ def _update_type_schema(self) -> JsonSchema: ], ) + @property + def _clinical_privilege_action_categories_schema(self) -> JsonSchema: + return JsonSchema( + type=JsonSchemaType.ARRAY, + description='The categories of clinical privilege action', + items=JsonSchema( + type=JsonSchemaType.STRING, + enum=['fraud', 'consumer harm', 'other'], + ), + ) + @property def _encumbrance_request_schema(self) -> JsonSchema: """Common schema for encumbrance request data used in both POST and PATCH investigation endpoints""" @@ -841,11 +844,7 @@ def _encumbrance_request_schema(self) -> JsonSchema: pattern=cc_api.YMD_FORMAT, ), 'encumbranceType': self._encumbrance_type_schema, - 'clinicalPrivilegeActionCategories': JsonSchema( - type=JsonSchemaType.ARRAY, - description='The categories of clinical privilege action', - items=JsonSchema(type=JsonSchemaType.STRING), - ), + 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, }, ) @@ -856,21 +855,9 @@ def _encumbrance_type_schema(self) -> JsonSchema: type=JsonSchemaType.STRING, description='The type of encumbrance', enum=[ - 'fine', - 'reprimand', - 'required supervision', - 'completion of continuing education', - 'public reprimand', - 'probation', - 'injunctive action', 'suspension', 'revocation', - 'denial', 'surrender of license', - 'modification of previous action-extension', - 'modification of previous action-reduction', - 'other monitoring', - 'other adjudicated action not listed', ], ) diff --git a/backend/cosmetology-app/stacks/search_api_stack/v1_api/api_model.py b/backend/cosmetology-app/stacks/search_api_stack/v1_api/api_model.py index ee098ace3..3bdbc74ab 100644 --- a/backend/cosmetology-app/stacks/search_api_stack/v1_api/api_model.py +++ b/backend/cosmetology-app/stacks/search_api_stack/v1_api/api_model.py @@ -358,7 +358,10 @@ def _adverse_action_general_schema(self): 'encumbranceType': JsonSchema(type=JsonSchemaType.STRING), 'clinicalPrivilegeActionCategories': JsonSchema( type=JsonSchemaType.ARRAY, - items=JsonSchema(type=JsonSchemaType.STRING), + items=JsonSchema( + type=JsonSchemaType.STRING, + enum=['fraud', 'consumer harm', 'other'], + ), ), 'liftingUser': JsonSchema(type=JsonSchemaType.STRING), 'submittingUser': JsonSchema(type=JsonSchemaType.STRING), diff --git a/backend/cosmetology-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json b/backend/cosmetology-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json index a8ac71ab5..491eabab4 100644 --- a/backend/cosmetology-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json +++ b/backend/cosmetology-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json @@ -473,6 +473,11 @@ "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" @@ -994,6 +999,11 @@ "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" diff --git a/backend/cosmetology-app/tests/resources/snapshots/LICENSE_ENCUMBRANCE_REQUEST_SCHEMA.json b/backend/cosmetology-app/tests/resources/snapshots/LICENSE_ENCUMBRANCE_REQUEST_SCHEMA.json index 84b644b6c..7aea9a0f2 100644 --- a/backend/cosmetology-app/tests/resources/snapshots/LICENSE_ENCUMBRANCE_REQUEST_SCHEMA.json +++ b/backend/cosmetology-app/tests/resources/snapshots/LICENSE_ENCUMBRANCE_REQUEST_SCHEMA.json @@ -11,27 +11,20 @@ "encumbranceType": { "description": "The type of encumbrance", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", "suspension", "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "surrender of license" ], "type": "string" }, "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" diff --git a/backend/cosmetology-app/tests/resources/snapshots/PATCH_LICENSE_INVESTIGATION_REQUEST_SCHEMA.json b/backend/cosmetology-app/tests/resources/snapshots/PATCH_LICENSE_INVESTIGATION_REQUEST_SCHEMA.json index 6a60e169e..0078eabf5 100644 --- a/backend/cosmetology-app/tests/resources/snapshots/PATCH_LICENSE_INVESTIGATION_REQUEST_SCHEMA.json +++ b/backend/cosmetology-app/tests/resources/snapshots/PATCH_LICENSE_INVESTIGATION_REQUEST_SCHEMA.json @@ -19,27 +19,20 @@ "encumbranceType": { "description": "The type of encumbrance", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", "suspension", "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "surrender of license" ], "type": "string" }, "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" diff --git a/backend/cosmetology-app/tests/resources/snapshots/PATCH_PRIVILEGE_INVESTIGATION_REQUEST_SCHEMA.json b/backend/cosmetology-app/tests/resources/snapshots/PATCH_PRIVILEGE_INVESTIGATION_REQUEST_SCHEMA.json index 6a60e169e..0078eabf5 100644 --- a/backend/cosmetology-app/tests/resources/snapshots/PATCH_PRIVILEGE_INVESTIGATION_REQUEST_SCHEMA.json +++ b/backend/cosmetology-app/tests/resources/snapshots/PATCH_PRIVILEGE_INVESTIGATION_REQUEST_SCHEMA.json @@ -19,27 +19,20 @@ "encumbranceType": { "description": "The type of encumbrance", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", "suspension", "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "surrender of license" ], "type": "string" }, "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" diff --git a/backend/cosmetology-app/tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_REQUEST_SCHEMA.json b/backend/cosmetology-app/tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_REQUEST_SCHEMA.json index 84b644b6c..7aea9a0f2 100644 --- a/backend/cosmetology-app/tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_REQUEST_SCHEMA.json +++ b/backend/cosmetology-app/tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_REQUEST_SCHEMA.json @@ -11,27 +11,20 @@ "encumbranceType": { "description": "The type of encumbrance", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", "suspension", "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "surrender of license" ], "type": "string" }, "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" diff --git a/backend/cosmetology-app/tests/resources/snapshots/PROVIDER_USER_RESPONSE_SCHEMA.json b/backend/cosmetology-app/tests/resources/snapshots/PROVIDER_USER_RESPONSE_SCHEMA.json index 652e27c64..75b75f573 100644 --- a/backend/cosmetology-app/tests/resources/snapshots/PROVIDER_USER_RESPONSE_SCHEMA.json +++ b/backend/cosmetology-app/tests/resources/snapshots/PROVIDER_USER_RESPONSE_SCHEMA.json @@ -606,6 +606,11 @@ "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" @@ -1511,6 +1516,11 @@ "clinicalPrivilegeActionCategories": { "description": "The categories of clinical privilege action", "items": { + "enum": [ + "fraud", + "consumer harm", + "other" + ], "type": "string" }, "type": "array" @@ -2178,4 +2188,4 @@ ], "type": "object", "$schema": "http://json-schema.org/draft-04/schema#" -} \ No newline at end of file +} diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index 3dfbd5296..80c5715cc 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -502,7 +502,7 @@ def test_license_encumbrance_workflow(): encumbrance_body = { 'encumbranceEffectiveDate': '2024-11-11', 'encumbranceType': 'surrender of license', - 'clinicalPrivilegeActionCategories': ['Fraud, Deception, or Misrepresentation'], + 'clinicalPrivilegeActionCategories': ['fraud'], } # First encumbrance @@ -563,8 +563,8 @@ def test_license_encumbrance_workflow(): # Second encumbrance second_encumbrance_body = { 'encumbranceEffectiveDate': '2025-01-01', - 'encumbranceType': 'denial', - 'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'], + 'encumbranceType': 'suspension', + 'clinicalPrivilegeActionCategories': ['consumer harm'], } helper.encumber_license(second_encumbrance_body) logger.info('Second license encumbrance created successfully') @@ -588,7 +588,7 @@ def test_license_encumbrance_workflow(): privilege_encumbrance_body = { 'encumbranceEffectiveDate': '2025-05-09', 'encumbranceType': 'reprimand', - 'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care', 'Misconduct or Abuse'], + 'clinicalPrivilegeActionCategories': ['other'], } helper.encumber_privilege(privilege_encumbrance_body) @@ -714,8 +714,8 @@ def test_privilege_encumbrance_workflow(): encumbrance_body = { 'encumbranceEffectiveDate': '2024-12-12', - 'encumbranceType': 'fine', - 'clinicalPrivilegeActionCategories': ['Fraud, Deception, or Misrepresentation'], + 'encumbranceType': 'revocation', + 'clinicalPrivilegeActionCategories': ['fraud'], } # First encumbrance @@ -749,8 +749,8 @@ def test_privilege_encumbrance_workflow(): # Second encumbrance second_encumbrance_body = { 'encumbranceEffectiveDate': '2025-02-02', - 'encumbranceType': 'completion of continuing education', - 'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'], + 'encumbranceType': 'suspension', + 'clinicalPrivilegeActionCategories': ['consumer harm'], } helper.encumber_privilege(second_encumbrance_body) logger.info('Second privilege encumbrance created successfully') @@ -838,11 +838,9 @@ def test_privilege_encumbrance_status_changes_with_license_encumbrance_workflow( logger.info('Step 1: Creating privilege encumbrance...') privilege_encumbrance_body = { 'encumbranceEffectiveDate': '2024-01-15', - 'encumbranceType': 'probation', + 'encumbranceType': 'revocation', 'clinicalPrivilegeActionCategories': [ - 'Unsafe Practice or Substandard Care', - 'Non-compliance With Requirements', - 'Fraud, Deception, or Misrepresentation', + 'fraud', ], } @@ -867,9 +865,7 @@ def test_privilege_encumbrance_status_changes_with_license_encumbrance_workflow( 'encumbranceEffectiveDate': '2024-01-20', 'encumbranceType': 'suspension', 'clinicalPrivilegeActionCategories': [ - 'Criminal Conviction or Adjudication', - 'Improper Supervision or Allowing Unlicensed Practice', - 'Other', + 'fraud', ], } diff --git a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py index 3beebdc21..20594d9f4 100755 --- a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py @@ -395,8 +395,8 @@ def test_close_privilege_investigation_with_encumbrance(auth_headers): 'action': 'close', 'encumbrance': { 'encumbranceEffectiveDate': '2024-01-15', - 'encumbranceType': 'fine', - 'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'], + 'encumbranceType': 'revocation', + 'clinicalPrivilegeActionCategories': ['consumer harm'], }, } From c0a73a27f986a766ec19f96e6b652bb53db23d4d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Apr 2026 21:23:09 -0500 Subject: [PATCH 02/20] replace encumbrance type in tests --- .../tests/function/test_handlers/test_encumbrance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index b8184ecd6..0d011e6d9 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -43,10 +43,12 @@ def _generate_test_body(): - from cc_common.data_model.schema.common import ClinicalPrivilegeActionCategory + from cc_common.data_model.schema.common import ClinicalPrivilegeActionCategory, EncumbranceType return { 'encumbranceEffectiveDate': TEST_ENCUMBRANCE_EFFECTIVE_DATE, + # These Enums are expected to be `str` type, so we'll directly access their .value + 'encumbranceType': EncumbranceType.SUSPENSION.value, 'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.FRAUD.value], } From 9dd0dbd9309af01d675b1813a3fb2caebec9c2b0 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Apr 2026 21:37:10 -0500 Subject: [PATCH 03/20] Enforce exactly one category --- .../common/cc_common/data_model/schema/adverse_action/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/adverse_action/api.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/adverse_action/api.py index e4948db2b..dd61d8e8a 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/adverse_action/api.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/adverse_action/api.py @@ -24,8 +24,11 @@ class AdverseActionPostRequestSchema(ForgivingSchema): encumbranceEffectiveDate = Date(required=True, allow_none=False) encumbranceType = EncumbranceTypeField(required=True, allow_none=False) + # in the case of Cosmetology, we only allow one category, but we are keeping this as a list for compatibility + # with the existing code base, and to allow the potential of supporting multiple categories should this be needed + # in the future clinicalPrivilegeActionCategories = List( - ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False, validate=Length(min=1) + ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False, validate=Length(equal=1) ) From 17af03f885e3f82d190c9e1b1e4616aafc76385f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Apr 2026 22:24:44 -0500 Subject: [PATCH 04/20] WIP - refactor smoke tests to get provider data without provider login --- .../provider-data-v1/handlers/encumbrance.py | 9 +++ backend/cosmetology-app/tests/smoke/config.py | 19 ++--- .../tests/smoke/encumbrance_smoke_tests.py | 28 ++++--- .../tests/smoke/smoke_common.py | 78 ++++++++----------- .../tests/smoke/smoke_tests_env_example.json | 6 +- 5 files changed, 62 insertions(+), 78 deletions(-) diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py index a946550f1..cdd22ac90 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py @@ -36,6 +36,11 @@ ) +def _ensure_jurisdiction_live(compact: str, jurisdiction: str) -> None: + if not config.compact_configuration_client.is_jurisdiction_live_in_compact(compact, jurisdiction): + raise CCInvalidRequestException('Jurisdiction is not live in this compact') + + @api_handler @authorize_state_level_only_action(action=CCPermissionsAction.ADMIN) def encumbrance_handler(event: dict, context: LambdaContext) -> dict: @@ -146,6 +151,7 @@ def handle_privilege_encumbrance(event: dict) -> dict: # Parse event parameters compact = event['pathParameters']['compact'] jurisdiction = event['pathParameters']['jurisdiction'] + _ensure_jurisdiction_live(compact, jurisdiction) provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided') license_type_abbr = event['pathParameters']['licenseType'].lower() submitting_user = _get_submitting_user_id(event) @@ -202,6 +208,7 @@ def handle_license_encumbrance(event: dict) -> dict: # Parse event parameters compact = event['pathParameters']['compact'] jurisdiction = event['pathParameters']['jurisdiction'] + _ensure_jurisdiction_live(compact, jurisdiction) provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided') license_type_abbr = event['pathParameters']['licenseType'].lower() submitting_user = _get_submitting_user_id(event) @@ -230,6 +237,7 @@ def handle_privilege_encumbrance_lifting(event: dict) -> dict: compact = event['pathParameters']['compact'] provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided') jurisdiction = event['pathParameters']['jurisdiction'] + _ensure_jurisdiction_live(compact, jurisdiction) license_type_abbreviation = event['pathParameters']['licenseType'].lower() encumbrance_id = to_uuid(event['pathParameters']['encumbranceId'], 'Invalid encumbranceId provided') @@ -282,6 +290,7 @@ def handle_license_encumbrance_lifting(event: dict) -> dict: compact = event['pathParameters']['compact'] provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided') jurisdiction = event['pathParameters']['jurisdiction'] + _ensure_jurisdiction_live(compact, jurisdiction) license_type_abbreviation = event['pathParameters']['licenseType'].lower() encumbrance_id = to_uuid(event['pathParameters']['encumbranceId'], 'Invalid encumbranceId provided') diff --git a/backend/cosmetology-app/tests/smoke/config.py b/backend/cosmetology-app/tests/smoke/config.py index b43b3e2d2..8ac9f2d1f 100644 --- a/backend/cosmetology-app/tests/smoke/config.py +++ b/backend/cosmetology-app/tests/smoke/config.py @@ -69,20 +69,15 @@ def cognito_staff_user_pool_id(self): return os.environ['CC_TEST_COGNITO_STAFF_USER_POOL_ID'] @property - def cognito_provider_user_client_id(self): - return os.environ['CC_TEST_COGNITO_PROVIDER_USER_POOL_CLIENT_ID'] + def test_provider_id(self): + return os.environ['CC_TEST_PROVIDER_ID'] @property - def cognito_provider_user_pool_id(self): - return os.environ['CC_TEST_COGNITO_PROVIDER_USER_POOL_ID'] - - @property - def test_provider_user_username(self): - return os.environ['CC_TEST_PROVIDER_USER_USERNAME'] - - @property - def test_provider_user_password(self): - return os.environ['CC_TEST_PROVIDER_USER_PASSWORD'] + def smoke_read_general_staff_email(self): + return os.environ.get( + 'CC_TEST_SMOKE_READ_GENERAL_STAFF_EMAIL', + 'testStaffUserLicenseUploader@smokeTestFakeEmail.com', + ) @property def sandbox_authorize_net_api_login_id(self): diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index 80c5715cc..fa3e55869 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -6,17 +6,18 @@ including setting encumbrances and lifting them through the API endpoints. """ +import os import time import requests -from purchasing_privileges_smoke_tests import test_purchasing_privilege from smoke_common import ( SmokeTestFailureException, - call_provider_users_me_endpoint, + call_provider_details_endpoint, config, create_test_staff_user, delete_test_staff_user, get_all_provider_database_records, + get_license_type_abbreviation, get_provider_user_dynamodb_table, get_provider_user_records, get_staff_user_auth_headers, @@ -24,6 +25,7 @@ logger, ) +ENCUMBRANCE_SMOKE_COMPACT = 'cosm' def clean_adverse_actions(): """ @@ -31,8 +33,7 @@ def clean_adverse_actions(): """ logger.info('Cleaning up existing adverse action records...') - # Get all provider database records - all_records = get_all_provider_database_records() + all_records = get_all_provider_database_records(ENCUMBRANCE_SMOKE_COMPACT, config.test_provider_id) # Filter for adverse action records adverse_action_records = [record for record in all_records if record.get('type') == 'adverseAction'] @@ -53,8 +54,7 @@ def clean_adverse_actions(): def _remove_encumbered_status_from_license_and_provider(): - # Get all provider database records - all_records = get_all_provider_database_records() + all_records = get_all_provider_database_records(ENCUMBRANCE_SMOKE_COMPACT, config.test_provider_id) for record in all_records: if record.get('type') == 'license' or record.get('type') == 'provider': @@ -72,19 +72,16 @@ def _remove_encumbered_status_from_license_and_provider(): def setup_test_environment(): """ - Set up the test environment by cleaning adverse actions and purchasing a privilege. + Set up the test environment by cleaning any previous adverse actions. """ logger.info('Setting up test environment...') - # Clean up any existing adverse actions + clean_adverse_actions() # remove encumbered status from license and provider if present _remove_encumbered_status_from_license_and_provider() - # Purchase a privilege to ensure we have one to test with - logger.info('Purchasing a privilege for testing...') - test_purchasing_privilege() logger.info('Test environment setup complete') @@ -96,7 +93,7 @@ def __init__(self, provider_data: dict): """ Initialize the helper with provider data and set up all necessary resources. - :param provider_data: Result from call_provider_users_me_endpoint() + :param provider_details: JSON body from GET /v1/compacts/{compact}/providers/{providerId} """ self.provider_data = provider_data self.compact = provider_data['compact'] @@ -702,10 +699,10 @@ def test_privilege_encumbrance_workflow(): 3. Lift the final encumbrance (privilege should become unencumbered) """ logger.info('Starting privilege encumbrance workflow test...') - # clean adverse actions from previous test clean_adverse_actions() # Get provider data and create helper - provider_data = call_provider_users_me_endpoint() + read_headers = get_staff_user_auth_headers(config.smoke_read_general_staff_email) + provider_data = call_provider_details_endpoint(read_headers, ENCUMBRANCE_SMOKE_COMPACT, config.test_provider_id) helper = EncumbranceTestHelper(provider_data) try: @@ -830,7 +827,8 @@ def test_privilege_encumbrance_status_changes_with_license_encumbrance_workflow( logger.info('Starting complex privilege and license encumbrance workflow test...') # Get provider data and create helper - provider_data = call_provider_users_me_endpoint() + read_headers = get_staff_user_auth_headers(config.smoke_read_general_staff_email) + provider_data = call_provider_details_endpoint(read_headers, ENCUMBRANCE_SMOKE_COMPACT, config.test_provider_id) helper = EncumbranceTestHelper(provider_data) try: diff --git a/backend/cosmetology-app/tests/smoke/smoke_common.py b/backend/cosmetology-app/tests/smoke/smoke_common.py index 9bec1b3ff..9b60708b3 100644 --- a/backend/cosmetology-app/tests/smoke/smoke_common.py +++ b/backend/cosmetology-app/tests/smoke/smoke_common.py @@ -164,15 +164,7 @@ def get_user_tokens(email, password=_TEST_STAFF_USER_PASSWORD, is_staff=False): raise e -def get_provider_user_auth_headers_cached(): - provider_token = os.environ.get('TEST_PROVIDER_USER_ID_TOKEN') - if not provider_token: - tokens = get_user_tokens(config.test_provider_user_username, config.test_provider_user_password, is_staff=False) - os.environ['TEST_PROVIDER_USER_ID_TOKEN'] = tokens['IdToken'] - return { - 'Authorization': 'Bearer ' + os.environ['TEST_PROVIDER_USER_ID_TOKEN'], - } def get_staff_user_auth_headers(username: str, password: str = _TEST_STAFF_USER_PASSWORD): @@ -226,50 +218,42 @@ def load_smoke_test_env(): os.environ.update(env_vars) -def call_provider_users_me_endpoint(): - """Get the provider data from the GET '/v1/provider-users/me' endpoint. - - If a 403 response is received, the token will be refreshed and the request retried once. - - :return: The response body JSON - :raises SmokeTestFailureException: If the request fails after retry - """ - # Get the provider data from the GET '/v1/provider-users/me' endpoint. - get_provider_data_response = requests.get( - url=config.api_base_url + '/v1/provider-users/me', headers=get_provider_user_auth_headers_cached(), timeout=10 - ) +def call_provider_details_endpoint(headers: dict, compact: str, provider_id: str) -> dict: + """GET /v1/compacts/{compact}/providers/{provider_id} with staff (or other) auth headers.""" + url = f'{config.api_base_url}/v1/compacts/{compact}/providers/{provider_id}' + response = requests.get(url=url, headers=headers, timeout=10) - # If we get a 403, the token may have expired - refresh it and retry once - if get_provider_data_response.status_code == 403: - logger.info('Received 403 response, refreshing provider user token and retrying...') - # Clear the cached token to force a refresh - if 'TEST_PROVIDER_USER_ID_TOKEN' in os.environ: - del os.environ['TEST_PROVIDER_USER_ID_TOKEN'] - - # Retry with fresh token - get_provider_data_response = requests.get( - url=config.api_base_url + '/v1/provider-users/me', - headers=get_provider_user_auth_headers_cached(), - timeout=10, - ) + if response.status_code != 200: + try: + body = response.json() + except Exception: # noqa: BLE001 + body = response.text + raise SmokeTestFailureException(f'Failed to GET provider details. Response: {body}') + return response.json() - if get_provider_data_response.status_code != 200: - raise SmokeTestFailureException(f'Failed to GET provider data. Response: {get_provider_data_response.json()}') - # return the response body - return get_provider_data_response.json() +def get_all_provider_database_records(compact: str | None = None, provider_id: str | None = None): + if compact is not None and provider_id is not None: + pk = f'{compact}#PROVIDER#{provider_id}' + else: + resolved_compact = compact or 'cosm' + resolved_provider_id = provider_id or config.test_provider_id + pk = f'{resolved_compact}#PROVIDER#{resolved_provider_id}' -def get_all_provider_database_records(): - # get the provider id and compact from the response - response = call_provider_users_me_endpoint() - provider_id = response['providerId'] - compact = response['compact'] - # query the provider database for all records - query_result = config.provider_user_dynamodb_table.query( - KeyConditionExpression=Key('pk').eq(f'{compact}#PROVIDER#{provider_id}') - ) + items: list = [] + last_evaluated_key = None + while True: + pagination = {'ExclusiveStartKey': last_evaluated_key} if last_evaluated_key else {} + query_result = config.provider_user_dynamodb_table.query( + KeyConditionExpression=Key('pk').eq(pk), + **pagination, + ) + items.extend(query_result.get('Items', [])) + last_evaluated_key = query_result.get('LastEvaluatedKey') + if not last_evaluated_key: + break - return query_result['Items'] + return items def get_provider_user_records(compact: str, provider_id: str) -> ProviderUserRecords: diff --git a/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json b/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json index 9b12be036..a01445ead 100644 --- a/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json +++ b/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json @@ -12,10 +12,8 @@ "CC_TEST_STAFF_USER_DYNAMO_TABLE_NAME": "Sandbox-PersistentStack-StaffUserTable1234", "CC_TEST_COGNITO_STAFF_USER_POOL_ID": "us-east-1_12345", "CC_TEST_COGNITO_STAFF_USER_POOL_CLIENT_ID": "72612345", - "CC_TEST_COGNITO_PROVIDER_USER_POOL_ID": "us-east-1_12345", - "CC_TEST_COGNITO_PROVIDER_USER_POOL_CLIENT_ID": "72612345", - "CC_TEST_PROVIDER_USER_USERNAME": "example@example.com", - "CC_TEST_PROVIDER_USER_PASSWORD": "examplePassword", + "CC_TEST_PROVIDER_ID": "exampleProviderId", + "CC_TEST_SMOKE_READ_GENERAL_STAFF_EMAIL": "testStaffUserLicenseUploader@smokeTestFakeEmail.com", "ENVIRONMENT_NAME": "sandboxEnvironmentNamePlaceholder", "SANDBOX_AUTHORIZE_NET_API_LOGIN_ID": "your_sandbox_api_login_id", "SANDBOX_AUTHORIZE_NET_TRANSACTION_KEY": "your_sandbox_transaction_key", From 1e5c352f79d4f50ba261a2e60c34302284bcc201 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 10:24:27 -0500 Subject: [PATCH 05/20] Remove unused notification methods --- .../common/cc_common/email_service_client.py | 121 ------------------ 1 file changed, 121 deletions(-) diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py b/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py index c53d3e29e..f4e8c5947 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py @@ -6,7 +6,6 @@ import boto3 from aws_lambda_powertools.logging import Logger - from cc_common.exceptions import CCInternalException @@ -91,36 +90,6 @@ def _invoke_lambda(self, payload: dict[str, Any]) -> dict[str, Any]: self._logger.error(error_message, payload=payload, exception=str(e)) raise CCInternalException(error_message) from e - def send_license_encumbrance_provider_notification_email( - self, - *, - compact: str, - provider_email: str, - template_variables: EncumbranceNotificationTemplateVariables, - ) -> dict[str, str]: - """ - Send a license encumbrance notification email to a provider. - - :param compact: Compact name - :param provider_email: Email address of the provider - :param template_variables: Template variables for the email - :return: Response from the email notification service - """ - payload = { - 'compact': compact, - 'template': 'licenseEncumbranceProviderNotification', - 'recipientType': 'SPECIFIC', - 'specificEmails': [provider_email], - 'templateVariables': { - 'providerFirstName': template_variables.provider_first_name, - 'providerLastName': template_variables.provider_last_name, - 'encumberedJurisdiction': template_variables.encumbered_jurisdiction, - 'licenseType': template_variables.license_type, - 'effectiveStartDate': template_variables.effective_date.strftime('%B %d, %Y'), - }, - } - return self._invoke_lambda(payload) - def send_license_encumbrance_state_notification_email( self, *, @@ -155,36 +124,6 @@ def send_license_encumbrance_state_notification_email( } return self._invoke_lambda(payload) - def send_license_encumbrance_lifting_provider_notification_email( - self, - *, - compact: str, - provider_email: str, - template_variables: EncumbranceNotificationTemplateVariables, - ) -> dict[str, str]: - """ - Send a license encumbrance lifting notification email to a provider. - - :param compact: Compact name - :param provider_email: Email address of the provider - :param template_variables: Template variables for the email - :return: Response from the email notification service - """ - payload = { - 'compact': compact, - 'template': 'licenseEncumbranceLiftingProviderNotification', - 'recipientType': 'SPECIFIC', - 'specificEmails': [provider_email], - 'templateVariables': { - 'providerFirstName': template_variables.provider_first_name, - 'providerLastName': template_variables.provider_last_name, - 'liftedJurisdiction': template_variables.encumbered_jurisdiction, - 'licenseType': template_variables.license_type, - 'effectiveLiftDate': template_variables.effective_date.strftime('%B %d, %Y'), - }, - } - return self._invoke_lambda(payload) - def send_license_encumbrance_lifting_state_notification_email( self, *, @@ -219,36 +158,6 @@ def send_license_encumbrance_lifting_state_notification_email( } return self._invoke_lambda(payload) - def send_privilege_encumbrance_provider_notification_email( - self, - *, - compact: str, - provider_email: str, - template_variables: EncumbranceNotificationTemplateVariables, - ) -> dict[str, str]: - """ - Send a privilege encumbrance notification email to a provider. - - :param compact: Compact name - :param provider_email: Email address of the provider - :param template_variables: Template variables for the email - :return: Response from the email notification service - """ - payload = { - 'compact': compact, - 'template': 'privilegeEncumbranceProviderNotification', - 'recipientType': 'SPECIFIC', - 'specificEmails': [provider_email], - 'templateVariables': { - 'providerFirstName': template_variables.provider_first_name, - 'providerLastName': template_variables.provider_last_name, - 'encumberedJurisdiction': template_variables.encumbered_jurisdiction, - 'licenseType': template_variables.license_type, - 'effectiveStartDate': template_variables.effective_date.strftime('%B %d, %Y'), - }, - } - return self._invoke_lambda(payload) - def send_privilege_encumbrance_state_notification_email( self, *, @@ -283,36 +192,6 @@ def send_privilege_encumbrance_state_notification_email( } return self._invoke_lambda(payload) - def send_privilege_encumbrance_lifting_provider_notification_email( - self, - *, - compact: str, - provider_email: str, - template_variables: EncumbranceNotificationTemplateVariables, - ) -> dict[str, str]: - """ - Send a privilege encumbrance lifting notification email to a provider. - - :param compact: Compact name - :param provider_email: Email address of the provider - :param template_variables: Template variables for the email - :return: Response from the email notification service - """ - payload = { - 'compact': compact, - 'template': 'privilegeEncumbranceLiftingProviderNotification', - 'recipientType': 'SPECIFIC', - 'specificEmails': [provider_email], - 'templateVariables': { - 'providerFirstName': template_variables.provider_first_name, - 'providerLastName': template_variables.provider_last_name, - 'liftedJurisdiction': template_variables.encumbered_jurisdiction, - 'licenseType': template_variables.license_type, - 'effectiveLiftDate': template_variables.effective_date.strftime('%B %d, %Y'), - }, - } - return self._invoke_lambda(payload) - def send_privilege_encumbrance_lifting_state_notification_email( self, *, From fb591d0e9ac0f11002b162f6f8728da8c57daf1e Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 10:24:42 -0500 Subject: [PATCH 06/20] smoke test cleanup --- .../tests/smoke/encumbrance_smoke_tests.py | 42 +++++++------------ .../tests/smoke/smoke_common.py | 10 +---- .../tests/smoke/smoke_tests_env_example.json | 3 -- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index fa3e55869..5571bb177 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -4,20 +4,19 @@ This script tests the end-to-end encumbrance workflow for both licenses and privileges, including setting encumbrances and lifting them through the API endpoints. -""" -import os +This script assumes your test environment has a live jurisdiction for generating at least one privilege +record. You can set the value of the live jurisdiction in the LIVE_JURISDICTION constant +""" import time import requests from smoke_common import ( SmokeTestFailureException, - call_provider_details_endpoint, config, create_test_staff_user, delete_test_staff_user, get_all_provider_database_records, - get_license_type_abbreviation, get_provider_user_dynamodb_table, get_provider_user_records, get_staff_user_auth_headers, @@ -26,6 +25,7 @@ ) ENCUMBRANCE_SMOKE_COMPACT = 'cosm' +LIVE_JURISDICTION = 'ne' def clean_adverse_actions(): """ @@ -89,29 +89,27 @@ def setup_test_environment(): class EncumbranceTestHelper: """Helper class to manage encumbrance test operations with pre-configured staff users and URLs.""" - def __init__(self, provider_data: dict): + def __init__(self): """ Initialize the helper with provider data and set up all necessary resources. - - :param provider_details: JSON body from GET /v1/compacts/{compact}/providers/{providerId} """ - self.provider_data = provider_data - self.compact = provider_data['compact'] - self.provider_id = provider_data['providerId'] + # Get provider data + self.compact = ENCUMBRANCE_SMOKE_COMPACT + self.provider_id = config.test_provider_id - # Get jurisdiction information from Nebraska privilege (smoke tests purchase privilege in NE) + # Get jurisdiction information from privilege # Query database directly for privilege records provider_user_records = get_provider_user_records(self.compact, self.provider_id) # Find the Nebraska privilege - ne_privileges = provider_user_records.get_privilege_records( - filter_condition=lambda priv: priv.jurisdiction == 'ne' + privileges_in_live_jurisdiction = provider_user_records.get_privilege_records( + filter_condition=lambda priv: priv.jurisdiction == LIVE_JURISDICTION ) - if not ne_privileges: + if not privileges_in_live_jurisdiction: raise SmokeTestFailureException('Nebraska privilege not found for provider') - privilege_record = ne_privileges[0] + privilege_record = privileges_in_live_jurisdiction[0] self.privilege_jurisdiction = privilege_record.jurisdiction self.license_jurisdiction = privilege_record.licenseJurisdiction self.license_type = privilege_record.licenseType @@ -488,9 +486,7 @@ def test_license_encumbrance_workflow(): logger.info('Starting license encumbrance workflow test...') # remove adverse action records from previous tests clean_adverse_actions() - # Get provider data and create helper - provider_data = call_provider_users_me_endpoint() - helper = EncumbranceTestHelper(provider_data) + helper = EncumbranceTestHelper() try: # Step 1: Encumber the license twice @@ -700,10 +696,7 @@ def test_privilege_encumbrance_workflow(): """ logger.info('Starting privilege encumbrance workflow test...') clean_adverse_actions() - # Get provider data and create helper - read_headers = get_staff_user_auth_headers(config.smoke_read_general_staff_email) - provider_data = call_provider_details_endpoint(read_headers, ENCUMBRANCE_SMOKE_COMPACT, config.test_provider_id) - helper = EncumbranceTestHelper(provider_data) + helper = EncumbranceTestHelper() try: # Step 1: Encumber the privilege twice @@ -826,10 +819,7 @@ def test_privilege_encumbrance_status_changes_with_license_encumbrance_workflow( """ logger.info('Starting complex privilege and license encumbrance workflow test...') - # Get provider data and create helper - read_headers = get_staff_user_auth_headers(config.smoke_read_general_staff_email) - provider_data = call_provider_details_endpoint(read_headers, ENCUMBRANCE_SMOKE_COMPACT, config.test_provider_id) - helper = EncumbranceTestHelper(provider_data) + helper = EncumbranceTestHelper() try: # Step 1: Encumber the privilege directly diff --git a/backend/cosmetology-app/tests/smoke/smoke_common.py b/backend/cosmetology-app/tests/smoke/smoke_common.py index 9b60708b3..b4eb3fd97 100644 --- a/backend/cosmetology-app/tests/smoke/smoke_common.py +++ b/backend/cosmetology-app/tests/smoke/smoke_common.py @@ -232,20 +232,14 @@ def call_provider_details_endpoint(headers: dict, compact: str, provider_id: str return response.json() -def get_all_provider_database_records(compact: str | None = None, provider_id: str | None = None): - if compact is not None and provider_id is not None: - pk = f'{compact}#PROVIDER#{provider_id}' - else: - resolved_compact = compact or 'cosm' - resolved_provider_id = provider_id or config.test_provider_id - pk = f'{resolved_compact}#PROVIDER#{resolved_provider_id}' +def get_all_provider_database_records(compact: str = 'cosm', provider_id: str = config.test_provider_id): items: list = [] last_evaluated_key = None while True: pagination = {'ExclusiveStartKey': last_evaluated_key} if last_evaluated_key else {} query_result = config.provider_user_dynamodb_table.query( - KeyConditionExpression=Key('pk').eq(pk), + KeyConditionExpression=Key('pk').eq(f'{compact}#PROVIDER#{provider_id}'), **pagination, ) items.extend(query_result.get('Items', [])) diff --git a/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json b/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json index a01445ead..98b5c0dd5 100644 --- a/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json +++ b/backend/cosmetology-app/tests/smoke/smoke_tests_env_example.json @@ -13,10 +13,7 @@ "CC_TEST_COGNITO_STAFF_USER_POOL_ID": "us-east-1_12345", "CC_TEST_COGNITO_STAFF_USER_POOL_CLIENT_ID": "72612345", "CC_TEST_PROVIDER_ID": "exampleProviderId", - "CC_TEST_SMOKE_READ_GENERAL_STAFF_EMAIL": "testStaffUserLicenseUploader@smokeTestFakeEmail.com", "ENVIRONMENT_NAME": "sandboxEnvironmentNamePlaceholder", - "SANDBOX_AUTHORIZE_NET_API_LOGIN_ID": "your_sandbox_api_login_id", - "SANDBOX_AUTHORIZE_NET_TRANSACTION_KEY": "your_sandbox_transaction_key", "CC_TEST_SMOKE_TEST_NOTIFICATION_EMAIL": "smoke-test-notifications@example.com", "CC_TEST_ROLLBACK_STEP_FUNCTION_ARN": "arn:aws:states:us-east-1:123456789012:stateMachine:Sandbox-DisasterRecoveryStack-LicenseUploadRollbackStateMachine" } From 30357f64b0203a433f0db6ce254641ad355b16f8 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 10:26:43 -0500 Subject: [PATCH 07/20] fix encumbrance type in smoke test --- backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index 5571bb177..6a7b52205 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -580,7 +580,7 @@ def test_license_encumbrance_workflow(): # Step 3: Encumber Privilege privilege_encumbrance_body = { 'encumbranceEffectiveDate': '2025-05-09', - 'encumbranceType': 'reprimand', + 'encumbranceType': 'suspension', 'clinicalPrivilegeActionCategories': ['other'], } From 31870240e283e39257584daebf4b47cd4b129a2b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 11:24:11 -0500 Subject: [PATCH 08/20] Update smoke tests to reference valid cosm member state --- .../compact_configuration_smoke_tests.py | 2 +- .../tests/smoke/encumbrance_smoke_tests.py | 21 ++++++++----------- .../tests/smoke/investigation_smoke_tests.py | 4 ++-- .../tests/smoke/license_upload_smoke_tests.py | 8 +++---- .../rollback_license_upload_smoke_tests.py | 4 ++-- .../tests/smoke/smoke_common.py | 2 +- .../smoke/ssn_read_throttling_smoke_tests.py | 2 +- 7 files changed, 20 insertions(+), 23 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py b/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py index 194ec810a..200c1156f 100644 --- a/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py @@ -213,7 +213,7 @@ def test_compact_configuration(): delete_test_staff_user(test_email, user_sub, compact) -def test_jurisdiction_configuration(jurisdiction: str = 'ne', recreate_compact_config: bool = False): +def test_jurisdiction_configuration(jurisdiction: str = 'az', recreate_compact_config: bool = False): """ Test that a state admin can update and retrieve jurisdiction configuration. diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index 6a7b52205..0dca0287c 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -25,7 +25,7 @@ ) ENCUMBRANCE_SMOKE_COMPACT = 'cosm' -LIVE_JURISDICTION = 'ne' +LIVE_JURISDICTION = 'az' def clean_adverse_actions(): """ @@ -101,19 +101,16 @@ def __init__(self): # Query database directly for privilege records provider_user_records = get_provider_user_records(self.compact, self.provider_id) - # Find the Nebraska privilege - privileges_in_live_jurisdiction = provider_user_records.get_privilege_records( - filter_condition=lambda priv: priv.jurisdiction == LIVE_JURISDICTION - ) + # Get license record + provider_license = provider_user_records.find_best_license_in_current_known_licenses() - if not privileges_in_live_jurisdiction: - raise SmokeTestFailureException('Nebraska privilege not found for provider') + if not provider_license: + raise SmokeTestFailureException('License not found for provider') - privilege_record = privileges_in_live_jurisdiction[0] - self.privilege_jurisdiction = privilege_record.jurisdiction - self.license_jurisdiction = privilege_record.licenseJurisdiction - self.license_type = privilege_record.licenseType - self.license_type_abbreviation = privilege_record.licenseTypeAbbreviation + self.privilege_jurisdiction = LIVE_JURISDICTION + self.license_jurisdiction = provider_license.jurisdiction + self.license_type = provider_license.licenseType + self.license_type_abbreviation = provider_license.licenseTypeAbbreviation # Track created users for cleanup self.created_staff_users = [] diff --git a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py index 20594d9f4..e47d3a587 100755 --- a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py @@ -452,8 +452,8 @@ def main(): staff_user_sub = create_test_staff_user( email=staff_user_email, compact='cosm', - jurisdiction='ne', - permissions={'actions': {'admin'}, 'jurisdictions': {'ne': {'admin'}, 'co': {'admin'}, 'ky': {'admin'}}}, + jurisdiction='az', + permissions={'actions': {'admin'}, 'jurisdictions': {'az': {'admin'}, 'co': {'admin'}, 'ky': {'admin'}}}, ) # Get staff user auth headers once for reuse diff --git a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py index 7dbc37c21..bb6889a22 100644 --- a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py @@ -18,7 +18,7 @@ MOCK_SSN = '999-99-9999' COMPACT = 'cosm' -JURISDICTION = 'ne' +JURISDICTION = 'az' TEST_PROVIDER_GIVEN_NAME = 'Joe' TEST_PROVIDER_FAMILY_NAME = 'Dokes' @@ -62,14 +62,14 @@ def upload_licenses_record(): Verifies that a license record can be uploaded to the Compact Connect API and the appropriate records are created in the provider table as well as the data events table. - Step 1: Upload a license record through the POST '/v1/compacts/cosm/jurisdictions/ne/licenses' endpoint. + Step 1: Upload a license record through the POST '/v1/compacts/cosm/jurisdictions/az/licenses' endpoint. Step 2: Verify the provider records are added by querying the API. Step 3: Verify the license record is recorded in the data events table. """ headers = get_staff_user_auth_headers(TEST_STAFF_USER_EMAIL) - # Step 1: Upload a license record through the POST '/v1/compacts/cosm/jurisdictions/ne/licenses' endpoint. + # Step 1: Upload a license record through the POST '/v1/compacts/cosm/jurisdictions/az/licenses' endpoint. post_body = [ { 'licenseNumber': 'A0608337260', @@ -179,7 +179,7 @@ def upload_licenses_record(): license_ingest_record_response = data_events_table.query( KeyConditionExpression='pk = :pk AND sk BETWEEN :start_time AND :end_time', ExpressionAttributeValues={ - ':pk': 'COMPACT#cosm#JURISDICTION#ne', + ':pk': 'COMPACT#cosm#JURISDICTION#az', ':start_time': f'TYPE#license.ingest#TIME#{int(start_time.timestamp())}', ':end_time': f'TYPE#license.ingest#TIME#{int(event_time.timestamp())}', }, diff --git a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py index 2c92f4889..9eee59c39 100644 --- a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py @@ -23,7 +23,7 @@ ) COMPACT = 'cosm' -JURISDICTION = 'ne' +JURISDICTION = 'az' TEST_STAFF_USER_EMAIL = 'testStaffUserLicenseRollback@smokeTestFakeEmail.com' TEST_APP_CLIENT_NAME = 'test-license-rollback-client' @@ -66,7 +66,7 @@ def upload_test_license_batch( 'ssn': f'999-50-{i:04d}', # Incrementing SSN with padded zeros 'licenseType': LICENSE_TYPE, 'dateOfExpiration': '2050-12-10', - 'homeAddressState': 'NE', + 'homeAddressState': 'AZ', 'homeAddressCity': 'Omaha', 'compactEligibility': 'eligible', 'licenseStatus': 'active', diff --git a/backend/cosmetology-app/tests/smoke/smoke_common.py b/backend/cosmetology-app/tests/smoke/smoke_common.py index b4eb3fd97..7899bc1ba 100644 --- a/backend/cosmetology-app/tests/smoke/smoke_common.py +++ b/backend/cosmetology-app/tests/smoke/smoke_common.py @@ -301,7 +301,7 @@ def upload_license_record(staff_headers: dict, compact: str, jurisdiction: str, 'ssn': '999-99-9999', 'licenseType': 'cosmetologist', 'dateOfExpiration': '2050-01-01', - 'homeAddressState': 'ne', + 'homeAddressState': 'AZ', 'homeAddressCity': 'Omaha', 'licenseStatus': 'active', 'compactEligibility': 'eligible', diff --git a/backend/cosmetology-app/tests/smoke/ssn_read_throttling_smoke_tests.py b/backend/cosmetology-app/tests/smoke/ssn_read_throttling_smoke_tests.py index 9101dea46..c98e44544 100644 --- a/backend/cosmetology-app/tests/smoke/ssn_read_throttling_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/ssn_read_throttling_smoke_tests.py @@ -18,7 +18,7 @@ ) COMPACT = 'cosm' -JURISDICTION = 'ne' +JURISDICTION = 'az' TEST_PROVIDER_GIVEN_NAME = 'Joe' TEST_PROVIDER_FAMILY_NAME = 'Dokes' From 378a09dcdcb5d5c103ce5ceab2c3d248679fce36 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 11:24:47 -0500 Subject: [PATCH 09/20] Add permission to lambda to get live state jurisdiction --- .../stacks/api_lambda_stack/provider_management.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/cosmetology-app/stacks/api_lambda_stack/provider_management.py b/backend/cosmetology-app/stacks/api_lambda_stack/provider_management.py index 5e8ea4d95..cb9749020 100644 --- a/backend/cosmetology-app/stacks/api_lambda_stack/provider_management.py +++ b/backend/cosmetology-app/stacks/api_lambda_stack/provider_management.py @@ -320,6 +320,7 @@ def _add_provider_encumbrance_handler( ) self.persistent_stack.provider_table.grant_read_write_data(handler) self.persistent_stack.staff_users.user_table.grant_read_data(handler) + self.persistent_stack.compact_configuration_table.grant_read_data(handler) self.data_event_bus.grant_put_events_to(handler) NagSuppressions.add_resource_suppressions_by_path( From 3e6caac8e44d7ada8a075784e8805ea8af7d5275 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 12:36:42 -0500 Subject: [PATCH 10/20] Refactor investigation smoke tests to work with multi-state license model --- .../tests/smoke/investigation_smoke_tests.py | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py index e47d3a587..a2ada5e5b 100755 --- a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py @@ -11,7 +11,7 @@ import requests from smoke_common import ( SmokeTestFailureException, - call_provider_users_me_endpoint, + call_provider_details_endpoint, config, create_test_staff_user, delete_test_staff_user, @@ -23,6 +23,15 @@ logger, ) +INVESTIGATION_SMOKE_COMPACT = 'cosm' + + +def _fetch_provider_details(auth_headers: dict) -> dict: + """Staff GET provider details for the smoke-test provider (CC_TEST_PROVIDER_ID).""" + return call_provider_details_endpoint( + auth_headers, INVESTIGATION_SMOKE_COMPACT, config.test_provider_id + ) + def clean_investigation_records(): """ @@ -142,7 +151,9 @@ def _get_privilege_data_from_provider_response(provider_data: dict, jurisdiction ) -def _verify_no_investigation_exists(record_type: str, jurisdiction: str, license_type: str): +def _verify_no_investigation_exists( + record_type: str, jurisdiction: str, license_type: str, auth_headers: dict +): """ Verify that no open investigation records exist in the database and no investigation status or objects on the record. @@ -150,6 +161,7 @@ def _verify_no_investigation_exists(record_type: str, jurisdiction: str, license :param record_type: 'privilege' or 'license' :param jurisdiction: The jurisdiction of the record :param license_type: The license type of the record + :param auth_headers: Staff user auth headers """ # Check database for open investigation records all_records = get_all_provider_database_records() @@ -161,7 +173,7 @@ def _verify_no_investigation_exists(record_type: str, jurisdiction: str, license raise SmokeTestFailureException('Open investigation already exists before creation test') # Check API for investigation status - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) if record_type == 'privilege': record_data = _get_privilege_data_from_provider_response(provider_data, jurisdiction, license_type) @@ -181,13 +193,16 @@ def _verify_no_investigation_exists(record_type: str, jurisdiction: str, license raise SmokeTestFailureException('Investigation objects still exist in API response') -def _verify_investigation_exists(record_type: str, jurisdiction: str, license_type: str): +def _verify_investigation_exists( + record_type: str, jurisdiction: str, license_type: str, auth_headers: dict +): """ Verify that an open investigation exists and the record has investigation status. :param record_type: 'privilege' or 'license' :param jurisdiction: The jurisdiction of the record :param license_type: The license type of the record + :param auth_headers: Staff Bearer auth headers :return: The investigation ID """ # Check database for investigation records @@ -206,7 +221,7 @@ def _verify_investigation_exists(record_type: str, jurisdiction: str, license_ty raise SmokeTestFailureException(f'No open {record_type} investigation found to close') # Check API for investigation status - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) if record_type == 'privilege': record_data = _get_privilege_data_from_provider_response(provider_data, jurisdiction, license_type) @@ -232,14 +247,14 @@ def test_create_privilege_investigation(auth_headers): """Test creating a privilege investigation.""" logger.info('Testing privilege investigation creation...') - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) provider_id = provider_data['providerId'] compact = provider_data['compact'] jurisdiction = provider_data['privileges'][0]['jurisdiction'] license_type = provider_data['privileges'][0]['licenseType'] license_type_abbreviation = get_license_type_abbreviation(license_type) - _verify_no_investigation_exists('privilege', jurisdiction, license_type) + _verify_no_investigation_exists('privilege', jurisdiction, license_type, auth_headers) # Create investigation (empty body required) response = requests.post( @@ -260,21 +275,21 @@ def test_create_privilege_investigation(auth_headers): # Wait for the investigation to be processed and DynamoDB eventual consistency time.sleep(5) - _verify_investigation_exists('privilege', jurisdiction, license_type) + _verify_investigation_exists('privilege', jurisdiction, license_type, auth_headers) def test_create_license_investigation(auth_headers): """Test creating a license investigation.""" logger.info('Testing license investigation creation...') - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) provider_id = provider_data['providerId'] compact = provider_data['compact'] jurisdiction = provider_data['licenseJurisdiction'] license_type = provider_data['licenses'][0]['licenseType'] license_type_abbreviation = get_license_type_abbreviation(license_type) - _verify_no_investigation_exists('license', jurisdiction, license_type) + _verify_no_investigation_exists('license', jurisdiction, license_type, auth_headers) # Create investigation (empty body required) response = requests.post( @@ -295,21 +310,21 @@ def test_create_license_investigation(auth_headers): # Wait for the investigation to be processed and DynamoDB eventual consistency time.sleep(5) - _verify_investigation_exists('license', jurisdiction, license_type) + _verify_investigation_exists('license', jurisdiction, license_type, auth_headers) def test_close_privilege_investigation(auth_headers): """Test closing a privilege investigation.""" logger.info('Testing privilege investigation closing...') - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) provider_id = provider_data['providerId'] compact = provider_data['compact'] jurisdiction = provider_data['privileges'][0]['jurisdiction'] license_type = provider_data['privileges'][0]['licenseType'] license_type_abbreviation = get_license_type_abbreviation(license_type) - investigation_id = _verify_investigation_exists('privilege', jurisdiction, license_type) + investigation_id = _verify_investigation_exists('privilege', jurisdiction, license_type, auth_headers) # Close investigation (no encumbrance) response = requests.patch( @@ -330,21 +345,21 @@ def test_close_privilege_investigation(auth_headers): # Wait for the investigation to be processed and DynamoDB eventual consistency time.sleep(5) - _verify_no_investigation_exists('privilege', jurisdiction, license_type) + _verify_no_investigation_exists('privilege', jurisdiction, license_type, auth_headers) def test_close_license_investigation(auth_headers): """Test closing a license investigation.""" logger.info('Testing license investigation closing...') - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) provider_id = provider_data['providerId'] compact = provider_data['compact'] jurisdiction = provider_data['licenseJurisdiction'] license_type = provider_data['licenses'][0]['licenseType'] license_type_abbreviation = get_license_type_abbreviation(license_type) - investigation_id = _verify_investigation_exists('license', jurisdiction, license_type) + investigation_id = _verify_investigation_exists('license', jurisdiction, license_type, auth_headers) # Close investigation (no encumbrance) response = requests.patch( @@ -365,14 +380,14 @@ def test_close_license_investigation(auth_headers): # Wait for the investigation to be processed and DynamoDB eventual consistency time.sleep(5) - _verify_no_investigation_exists('license', jurisdiction, license_type) + _verify_no_investigation_exists('license', jurisdiction, license_type, auth_headers) def test_close_privilege_investigation_with_encumbrance(auth_headers): """Test closing a privilege investigation with encumbrance creation.""" logger.info('Testing privilege investigation closing with encumbrance...') - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) provider_id = provider_data['providerId'] compact = provider_data['compact'] jurisdiction = provider_data['privileges'][0]['jurisdiction'] @@ -380,7 +395,7 @@ def test_close_privilege_investigation_with_encumbrance(auth_headers): license_type_abbreviation = get_license_type_abbreviation(license_type) # Verify initial state: an open investigation should exist - investigation_id = _verify_investigation_exists('privilege', jurisdiction, license_type) + investigation_id = _verify_investigation_exists('privilege', jurisdiction, license_type, auth_headers) # Verify privilege is not already encumbered (no adverse actions) privilege_data = _get_privilege_data_from_provider_response(provider_data, jurisdiction, license_type) @@ -418,9 +433,9 @@ def test_close_privilege_investigation_with_encumbrance(auth_headers): # Wait for the investigation to be processed and DynamoDB eventual consistency time.sleep(5) - _verify_no_investigation_exists('privilege', jurisdiction, license_type) + _verify_no_investigation_exists('privilege', jurisdiction, license_type, auth_headers) # Verify encumbrance was created (adverse action exists) - provider_data = call_provider_users_me_endpoint() + provider_data = _fetch_provider_details(auth_headers) privilege_data = _get_privilege_data_from_provider_response(provider_data, jurisdiction, license_type) if not privilege_data.get('adverseActions'): @@ -453,7 +468,10 @@ def main(): email=staff_user_email, compact='cosm', jurisdiction='az', - permissions={'actions': {'admin'}, 'jurisdictions': {'az': {'admin'}, 'co': {'admin'}, 'ky': {'admin'}}}, + permissions={ + 'actions': {'admin'}, + 'jurisdictions': {'al': {'admin'}, 'az': {'admin'}, 'co': {'admin'}, 'ky': {'admin'}}, + }, ) # Get staff user auth headers once for reuse From 8f63fc23ed9d8f2e8aada47c67f6ef3c2d970ffa Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 13:16:58 -0500 Subject: [PATCH 11/20] Refactor encumbrance smoke tests to work with multi-state license model --- .../tests/smoke/encumbrance_smoke_tests.py | 344 ++++-------------- 1 file changed, 69 insertions(+), 275 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index 0dca0287c..7874071ad 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -14,6 +14,7 @@ from smoke_common import ( SmokeTestFailureException, config, + call_provider_details_endpoint, create_test_staff_user, delete_test_staff_user, get_all_provider_database_records, @@ -139,6 +140,14 @@ def _create_license_jurisdiction_staff_user(self) -> dict: return {'email': email, 'user_sub': user_sub, 'headers': headers} + def get_provider_details(self) -> dict: + """Get provider details for the smoke-test provider.""" + return call_provider_details_endpoint( + self.get_license_staff_admin_headers(), + self.compact, + self.provider_id, + ) + def get_privilege_staff_admin_headers(self) -> dict: """Get authentication headers for privilege jurisdiction staff user.""" return self.privilege_jurisdiction_staff_user['headers'] @@ -250,82 +259,6 @@ def validate_license_encumbered_state(self, expected_status: str = 'encumbered') return license_record - def validate_privilege_encumbered_state( - self, expected_status: str = 'encumbered', max_wait_time: int = 60, check_interval: int = 10 - ): - """ - Validate that the privilege encumberedStatus matches the expected value. - - This method will poll the provider records every check_interval seconds - for up to max_wait_time seconds, checking if the privilege has the expected - encumberedStatus. This accounts for eventual consistency in downstream processing. - - :param expected_status: The expected encumberedStatus value ('licenseEncumbered', 'unencumbered', etc.) - :param max_wait_time: Maximum time to wait in seconds (default: 60) - :param check_interval: Time between checks in seconds (default: 10) - - :raises: - :class:`~smoke_common.SmokeTestFailureException`: If the privilege status doesn't match within max_wait_time - """ - logger.info( - f'Validating privilege encumbered status is "{expected_status}" ' - f'for jurisdiction "{self.privilege_jurisdiction}"...' - ) - - start_time = time.time() - attempts = 0 - max_attempts = max_wait_time // check_interval - - while attempts < max_attempts: - attempts += 1 - - try: - # Get current provider records directly from DynamoDB - provider_user_records = get_provider_user_records(self.compact, self.provider_id) - - # Find the privilege that matches the license jurisdiction and type - matching_privilege = provider_user_records.get_specific_privilege_record( - self.privilege_jurisdiction, self.license_type_abbreviation - ) - - if not matching_privilege: - logger.warning( - f'Attempt {attempts}/{max_attempts}: No privilege found matching jurisdiction ' - f'"{self.privilege_jurisdiction}" and license type "{self.license_type_abbreviation}"' - ) - else: - logger.info('matching privilege found', matching_privilege=matching_privilege) - actual_status = matching_privilege.encumberedStatus - logger.info( - f'Attempt {attempts}/{max_attempts}: Privilege encumberedStatus is "{actual_status}", ' - f'expecting "{expected_status}"' - ) - - if actual_status == expected_status: - elapsed_time = time.time() - start_time - logger.info( - f'✅ Privilege encumbered status validation successful after {elapsed_time:.1f} seconds' - ) - return matching_privilege - - # If not the last attempt, wait before trying again - if attempts < max_attempts: - logger.info(f'Waiting {check_interval} seconds before next check...') - time.sleep(check_interval) - - except Exception as e: # noqa: BLE001 - logger.warning(f'Attempt {attempts}/{max_attempts}: Error checking privilege status: {e}') - if attempts < max_attempts: - time.sleep(check_interval) - - # If we get here, validation failed - elapsed_time = time.time() - start_time - raise SmokeTestFailureException( - f'Privilege encumbered status validation failed after {elapsed_time:.1f} seconds. ' - f'Expected "{expected_status}" but status did not update within {max_wait_time} seconds. ' - f'This suggests the downstream processing is not working correctly.' - ) - def validate_provider_encumbered_state(self, expected_status: str = 'encumbered'): """Validate provider encumbered status.""" # Get all provider records directly from DynamoDB @@ -421,6 +354,38 @@ def verify_privilege_adverse_action_matches_request(self, request_payload: dict) privilege_adverse_actions = self.get_privilege_adverse_actions() return self.verify_adverse_action_matches_request(privilege_adverse_actions, request_payload) + def get_privilege_adverse_action_by_id(self, adverse_action_id: str): + privilege_adverse_actions = self.get_privilege_adverse_actions() + matching_actions = [aa for aa in privilege_adverse_actions if aa['adverseActionId'] == adverse_action_id] + if not matching_actions: + raise SmokeTestFailureException(f'No matching adverse action found for ID: {adverse_action_id}') + return matching_actions[0] + + def verify_privilege_adverse_action_not_lifted(self, adverse_action_id: str) -> None: + """ + Verify that a privilege adverse action has not been lifted. + + :param adverse_action_id: The id of the adverse action + :return: The matching adverse action record + """ + matching_adverse_action = self.get_privilege_adverse_action_by_id(adverse_action_id) + lift_date = matching_adverse_action.get('effectiveLiftDate') + if lift_date is not None: + raise SmokeTestFailureException(f'Adverse action has unexpected lift date for ID: ' + f'{adverse_action_id}. effectiveLiftDate: {lift_date}') + + def verify_privilege_adverse_action_lifted(self, adverse_action_id: str) -> None: + """ + Verify that a privilege adverse action has been lifted. + + :param adverse_action_id: The id of the adverse action + :return: The matching adverse action record + """ + matching_adverse_action = self.get_privilege_adverse_action_by_id(adverse_action_id) + lift_date = matching_adverse_action.get('effectiveLiftDate') + if lift_date is None: + raise SmokeTestFailureException(f'Adverse action is missing expected lift date for ID:{adverse_action_id}') + def _generate_license_encumbrance_url(self, encumbrance_id: str = None): """Generate license encumbrance URL.""" base_url = ( @@ -472,13 +437,12 @@ def test_license_encumbrance_workflow(): """ Test the complete license encumbrance workflow: 1. Encumber a license twice - 2. Verify that the associated privilege is also encumbered with a 'licenseEncumbered' encumberedStatus - 3. Encumber privilege and ensure it is updated to an 'encumbered' encumberedStatus - 4. Lift one encumbrance (license should remain encumbered) - 5. Lift the final encumbrance (license should become unencumbered) - 6. Verify that the associated privilege is still encumbered (has an 'encumbered' encumberedStatus) - 7. Lift encumbrance from privilege - 8. Verify privilege is unencumbered + 2. Encumber privilege and ensure it is updated to an 'encumbered' encumberedStatus + 3. Lift one encumbrance (license should remain encumbered) + 4. Lift the final encumbrance (license should become unencumbered) + 5. Verify that the associated privilege is still encumbered (has an 'encumbered' encumberedStatus) + 6. Lift encumbrance from privilege + 7. Verify privilege is unencumbered """ logger.info('Starting license encumbrance workflow test...') # remove adverse action records from previous tests @@ -545,11 +509,6 @@ def test_license_encumbrance_workflow(): helper.verify_license_adverse_action_matches_request(encumbrance_body) logger.info('First license encumbrance verified successfully') - # Step 2: Verify that the associated privilege is also encumbered with 'licenseEncumbered' status - logger.info('Verifying associated privilege is encumbered...') - helper.validate_privilege_encumbered_state('licenseEncumbered') - logger.info('Verified privilege is encumbered with licenseEncumbered status') - # Second encumbrance second_encumbrance_body = { 'encumbranceEffectiveDate': '2025-01-01', @@ -574,7 +533,7 @@ def test_license_encumbrance_workflow(): helper.verify_license_adverse_action_matches_request(second_encumbrance_body) logger.info('Second license encumbrance verified successfully') - # Step 3: Encumber Privilege + # Step 2: Encumber privilege privilege_encumbrance_body = { 'encumbranceEffectiveDate': '2025-05-09', 'encumbranceType': 'suspension', @@ -584,20 +543,12 @@ def test_license_encumbrance_workflow(): helper.encumber_privilege(privilege_encumbrance_body) logger.info('Privilege encumbrance created successfully') - # privilege should now be encumbered - helper.validate_privilege_encumbered_state( - expected_status='encumbered', - # only need to check once - max_wait_time=1, - check_interval=1, - ) - # Verify the privilege adverse action matches the request payload helper.verify_privilege_adverse_action_matches_request(privilege_encumbrance_body) logger.info('Privilege encumbrance verified successfully') - # Step 4: Lift first encumbrance (license should remain encumbered) - logger.info('Step 4: Lifting first license encumbrance...') + # Step 3: Lift first encumbrance (license should remain encumbered) + logger.info('Step 3: Lifting first license encumbrance...') lift_body = { 'effectiveLiftDate': '2025-05-05', @@ -620,8 +571,8 @@ def test_license_encumbrance_workflow(): # this keeps the lifting events isolated from each other helper.wait_for_downstream_processing() - # Step 5: Lift final encumbrance (license should become unencumbered) - logger.info('Step 5: Lifting final license encumbrance...') + # Step 4: Lift final encumbrance (license should become unencumbered) + logger.info('Step 4: Lifting final license encumbrance...') lift_body = { 'effectiveLiftDate': '2025-05-25', @@ -636,15 +587,8 @@ def test_license_encumbrance_workflow(): # Verify provider is still encumbered (due to privilege encumbrance) helper.validate_provider_encumbered_state('encumbered') - # Step 6: Verify that the associated privilege is still encumbered + # Step 5: Verify that the associated privilege is still encumbered logger.info('Verifying associated privilege is still encumbered...') - helper.validate_privilege_encumbered_state( - expected_status='encumbered', - # only check once - max_wait_time=1, - check_interval=1, - ) - logger.info('Verified privilege is still encumbered after lifting all license encumbrances') privilege_adverse_actions = helper.get_privilege_adverse_actions() @@ -654,21 +598,20 @@ def test_license_encumbrance_workflow(): ) privilege_adverse_action_id = privilege_adverse_actions[0]['adverseActionId'] + helper.verify_privilege_adverse_action_not_lifted(privilege_adverse_action_id) - # Step 7: Lift the privilege encumbrance - logger.info('Step 7: Lifting privilege encumbrance...') + # Step 6: Lift the privilege encumbrance + logger.info('Step 6: Lifting privilege encumbrance...') lift_body = {'effectiveLiftDate': '2023-01-25'} helper.lift_privilege_encumbrance(lift_body, privilege_adverse_action_id) logger.info('Privilege encumbrance lifted successfully') - # Step 8: Verify privilege becomes 'unencumbered' - logger.info('Step 8: Verifying privilege becomes unencumbered...') - helper.validate_privilege_encumbered_state( - expected_status='unencumbered', - # should be instantly set to unencumbered - max_wait_time=1, - check_interval=1, + # Step 7: Verify privilege becomes 'unencumbered' + logger.info('Step 7: Verifying privilege becomes unencumbered...') + helper.verify_privilege_adverse_action_lifted( + adverse_action_id=privilege_adverse_action_id ) + helper.verify_privilege_adverse_action_lifted(privilege_adverse_action_id) logger.info('Verified privilege is now unencumbered') logger.info('License encumbrance workflow test completed successfully') @@ -709,15 +652,7 @@ def test_privilege_encumbrance_workflow(): helper.encumber_privilege(encumbrance_body) logger.info('First privilege encumbrance created successfully') - # Verify provider state after first encumbrance - helper.validate_privilege_encumbered_state( - expected_status='encumbered', - # only need to check once - max_wait_time=1, - check_interval=1, - ) - - # Check provider status to ensure it is encumbered as well + # Check provider status to ensure it shows encumbered status helper.validate_provider_encumbered_state('encumbered') # Verify adverse action exists @@ -769,8 +704,11 @@ def test_privilege_encumbrance_workflow(): helper.lift_privilege_encumbrance(lift_body, first_adverse_action_id) logger.info('First privilege encumbrance lifted successfully') - # Verify privilege is still encumbered - helper.validate_privilege_encumbered_state('encumbered') + # Verify first privilege encumbrance is lifted + helper.verify_privilege_adverse_action_lifted(first_adverse_action_id) + + # Verify second privilege is still encumbered + helper.verify_privilege_adverse_action_not_lifted(second_adverse_action_id) # Also verify the provider record is still encumbered helper.validate_provider_encumbered_state('encumbered') @@ -791,8 +729,8 @@ def test_privilege_encumbrance_workflow(): helper.lift_privilege_encumbrance(lift_body, second_adverse_action_id) logger.info('Final privilege encumbrance lifted successfully') - # Verify privilege is now unencumbered - helper.validate_privilege_encumbered_state('unencumbered') + # Verify second encumbrance is now lifted + helper.verify_privilege_adverse_action_lifted(second_adverse_action_id) # Also verify the provider record is now unencumbered helper.validate_provider_encumbered_state('unencumbered') @@ -804,147 +742,6 @@ def test_privilege_encumbrance_workflow(): helper.cleanup_staff_users() -def test_privilege_encumbrance_status_changes_with_license_encumbrance_workflow(): - """ - Test privilege encumbrance status values that can occur in the various encumbrance scenarios: - 1. Encumber a privilege directly - 2. Encumber the associated license - 3. Verify privilege remains 'encumbered' (not 'licenseEncumbered') - 4. Lift the privilege encumbrance - 5. Verify privilege becomes 'licenseEncumbered' - 6. Lift license encumbrance and verify privilege encumbrance is lifted automatically and set to 'unencumbered' - """ - logger.info('Starting complex privilege and license encumbrance workflow test...') - - helper = EncumbranceTestHelper() - - try: - # Step 1: Encumber the privilege directly - logger.info('Step 1: Creating privilege encumbrance...') - privilege_encumbrance_body = { - 'encumbranceEffectiveDate': '2024-01-15', - 'encumbranceType': 'revocation', - 'clinicalPrivilegeActionCategories': [ - 'fraud', - ], - } - - helper.encumber_privilege(privilege_encumbrance_body) - logger.info('Privilege encumbrance created successfully') - - # Verify privilege is encumbered - helper.validate_privilege_encumbered_state( - expected_status='encumbered', - # should be instantly set to encumbered - max_wait_time=1, - check_interval=1, - ) - - # Verify the privilege adverse action matches the request payload - helper.verify_privilege_adverse_action_matches_request(privilege_encumbrance_body) - logger.info('Verified privilege is directly encumbered') - - # Step 2: Encumber the associated license - logger.info('Step 2: Creating license encumbrance...') - license_encumbrance_body = { - 'encumbranceEffectiveDate': '2024-01-20', - 'encumbranceType': 'suspension', - 'clinicalPrivilegeActionCategories': [ - 'fraud', - ], - } - - helper.encumber_license(license_encumbrance_body) - logger.info('License encumbrance created successfully') - - # Verify the license adverse action matches the request payload - helper.verify_license_adverse_action_matches_request(license_encumbrance_body) - - # wait 1 minute for downstream processing to complete - # to ensure it doesn't change the privilege record - helper.wait_for_downstream_processing() - - # Step 3: Verify privilege remains 'encumbered' (not 'licenseEncumbered') - logger.info('Step 3: Verifying privilege remains directly encumbered...') - helper.validate_privilege_encumbered_state( - expected_status='encumbered', - # only need to check once - max_wait_time=1, - check_interval=1, - ) - logger.info('Verified privilege remains directly encumbered (not licenseEncumbered)') - - # Get the privilege adverse action ID for lifting - privilege_adverse_actions = helper.get_privilege_adverse_actions() - - if len(privilege_adverse_actions) != 1: - raise SmokeTestFailureException( - f'Expected 1 privilege adverse action, found: {len(privilege_adverse_actions)}' - ) - - privilege_adverse_action_id = privilege_adverse_actions[0]['adverseActionId'] - - # Step 4: Lift the privilege encumbrance - logger.info('Step 4: Lifting privilege encumbrance...') - lift_body = {'effectiveLiftDate': '2024-01-25'} - helper.lift_privilege_encumbrance(lift_body, privilege_adverse_action_id) - logger.info('Privilege encumbrance lifted successfully') - - # Step 5: Verify privilege becomes 'licenseEncumbered' - logger.info('Step 5: Verifying privilege becomes licenseEncumbered...') - helper.validate_privilege_encumbered_state( - expected_status='licenseEncumbered', - # should be instantly set to licenseEncumbered - max_wait_time=1, - check_interval=1, - ) - logger.info('Verified privilege is now licenseEncumbered') - - # Get the license adverse action ID for lifting - license_adverse_actions = helper.get_license_adverse_actions() - - if len(license_adverse_actions) != 1: - raise SmokeTestFailureException(f'Expected 1 license adverse action, found: {len(license_adverse_actions)}') - - license_adverse_action_id = license_adverse_actions[0]['adverseActionId'] - - # Step 6: Lift the license encumbrance - logger.info('Step 6: Lifting license encumbrance...') - lift_body = {'effectiveLiftDate': '2024-01-30'} - helper.lift_license_encumbrance(lift_body, license_adverse_action_id) - logger.info('License encumbrance lifted successfully') - - # Wait for downstream processing to complete, including notification handlers - # This prevents race conditions where notification handlers might still be processing - logger.info('Waiting for downstream processing and notification handlers to complete...') - helper.wait_for_downstream_processing() - - # Step 7: Verify privilege becomes 'unencumbered' - logger.info('Step 7: Verifying privilege becomes unencumbered...') - helper.validate_privilege_encumbered_state(expected_status='unencumbered') - logger.info('Verified privilege is now fully unencumbered') - - # Final verification: Check that provider is also unencumbered - provider_user_records = get_provider_user_records(helper.compact, helper.provider_id) - provider_record = provider_user_records.get_provider_record() - - if provider_record.encumberedStatus != 'unencumbered': - raise SmokeTestFailureException( - f"Provider encumberedStatus should be 'unencumbered', got: {provider_record.encumberedStatus}" - ) - - if provider_record.compactEligibility != 'eligible': - raise SmokeTestFailureException( - f"Provider compactEligibility should be 'eligible', got: {provider_record.compactEligibility}" - ) - - logger.info('Complex privilege and license encumbrance workflow test completed successfully') - - finally: - # Clean up all created staff users - helper.cleanup_staff_users() - - def run_encumbrance_smoke_tests(): """ Run the complete suite of encumbrance smoke tests. @@ -955,9 +752,6 @@ def run_encumbrance_smoke_tests(): # Setup test environment setup_test_environment() - # Run privilege and license encumbrance tests - test_privilege_encumbrance_status_changes_with_license_encumbrance_workflow() - # Run license encumbrance tests test_license_encumbrance_workflow() From ac4d7d91217684c0dd9ee108f328fc25661dd593 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 15:32:09 -0500 Subject: [PATCH 12/20] Add additional state to compact config smoke tests --- .../tests/smoke/compact_configuration_smoke_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py b/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py index 200c1156f..0d62948d0 100644 --- a/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/compact_configuration_smoke_tests.py @@ -437,4 +437,6 @@ def test_jurisdiction_configuration(jurisdiction: str = 'az', recreate_compact_c load_smoke_test_env() test_active_member_jurisdictions() test_compact_configuration() - test_jurisdiction_configuration() + # for the smoke tests, we set two jurisdictions to live for license and privilege smoke tests + test_jurisdiction_configuration('az') + test_jurisdiction_configuration('al') From 7003ba7af8b3924673df9ea38dfe5b7c3c045451 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 15:38:34 -0500 Subject: [PATCH 13/20] formatting/linter --- .../data_model/provider_record_util.py | 20 ++++++++----------- .../common/cc_common/email_service_client.py | 1 + .../stacks/api_stack/v1_api/api_model.py | 4 ++-- .../tests/smoke/encumbrance_smoke_tests.py | 15 +++++++------- .../tests/smoke/investigation_smoke_tests.py | 12 +++-------- .../tests/smoke/smoke_common.py | 3 --- 6 files changed, 21 insertions(+), 34 deletions(-) diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py index 73daa1eeb..5030729a0 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py @@ -482,18 +482,14 @@ def generate_privileges_for_provider(self, include_inactive_privileges: bool = F inv_records = self.get_investigation_records_for_privilege( jurisdiction, license_type_abbr, include_closed=False ) - if ( - not is_eligible - and not include_inactive_privileges - and not privilege_aa - and not inv_records - ): - logger.debug('Not returning a privilege for this jurisdiction because the home ' - 'license is not compact eligible and there are no matching privilege adverse ' - 'actions or open investigations.', - jurisdiction=jurisdiction, - home_jurisdiction=home_jurisdiction, - license_type_abbr=license_type_abbr, + if not is_eligible and not include_inactive_privileges and not privilege_aa and not inv_records: + logger.debug( + 'Not returning a privilege for this jurisdiction because the home ' + 'license is not compact eligible and there are no matching privilege adverse ' + 'actions or open investigations.', + jurisdiction=jurisdiction, + home_jurisdiction=home_jurisdiction, + license_type_abbr=license_type_abbr, ) continue privilege_dict = { diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py b/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py index f4e8c5947..755a130b9 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py @@ -6,6 +6,7 @@ import boto3 from aws_lambda_powertools.logging import Logger + from cc_common.exceptions import CCInternalException diff --git a/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py b/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py index 9671a0b99..29d27519b 100644 --- a/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py +++ b/backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py @@ -648,7 +648,7 @@ def _provider_detail_response_schema(self): ), 'dateOfUpdate': JsonSchema(type=JsonSchemaType.STRING, format='date-time'), 'encumbranceType': JsonSchema(type=JsonSchemaType.STRING), - 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, + 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, # noqa: E501 'liftingUser': JsonSchema(type=JsonSchemaType.STRING), }, ), @@ -778,7 +778,7 @@ def _provider_detail_response_schema(self): ), 'dateOfUpdate': JsonSchema(type=JsonSchemaType.STRING, format='date-time'), 'encumbranceType': JsonSchema(type=JsonSchemaType.STRING), - 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, + 'clinicalPrivilegeActionCategories': self._clinical_privilege_action_categories_schema, # noqa: E501 'liftingUser': JsonSchema(type=JsonSchemaType.STRING), }, ), diff --git a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py index 7874071ad..5c5d701e4 100644 --- a/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/encumbrance_smoke_tests.py @@ -8,13 +8,14 @@ This script assumes your test environment has a live jurisdiction for generating at least one privilege record. You can set the value of the live jurisdiction in the LIVE_JURISDICTION constant """ + import time import requests from smoke_common import ( SmokeTestFailureException, - config, call_provider_details_endpoint, + config, create_test_staff_user, delete_test_staff_user, get_all_provider_database_records, @@ -28,6 +29,7 @@ ENCUMBRANCE_SMOKE_COMPACT = 'cosm' LIVE_JURISDICTION = 'az' + def clean_adverse_actions(): """ Clean up any existing adverse action records for the provider to start in a clean state. @@ -77,13 +79,11 @@ def setup_test_environment(): """ logger.info('Setting up test environment...') - clean_adverse_actions() # remove encumbered status from license and provider if present _remove_encumbered_status_from_license_and_provider() - logger.info('Test environment setup complete') @@ -371,8 +371,9 @@ def verify_privilege_adverse_action_not_lifted(self, adverse_action_id: str) -> matching_adverse_action = self.get_privilege_adverse_action_by_id(adverse_action_id) lift_date = matching_adverse_action.get('effectiveLiftDate') if lift_date is not None: - raise SmokeTestFailureException(f'Adverse action has unexpected lift date for ID: ' - f'{adverse_action_id}. effectiveLiftDate: {lift_date}') + raise SmokeTestFailureException( + f'Adverse action has unexpected lift date for ID: {adverse_action_id}. effectiveLiftDate: {lift_date}' + ) def verify_privilege_adverse_action_lifted(self, adverse_action_id: str) -> None: """ @@ -608,9 +609,7 @@ def test_license_encumbrance_workflow(): # Step 7: Verify privilege becomes 'unencumbered' logger.info('Step 7: Verifying privilege becomes unencumbered...') - helper.verify_privilege_adverse_action_lifted( - adverse_action_id=privilege_adverse_action_id - ) + helper.verify_privilege_adverse_action_lifted(adverse_action_id=privilege_adverse_action_id) helper.verify_privilege_adverse_action_lifted(privilege_adverse_action_id) logger.info('Verified privilege is now unencumbered') diff --git a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py index a2ada5e5b..80fe82a43 100755 --- a/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/investigation_smoke_tests.py @@ -28,9 +28,7 @@ def _fetch_provider_details(auth_headers: dict) -> dict: """Staff GET provider details for the smoke-test provider (CC_TEST_PROVIDER_ID).""" - return call_provider_details_endpoint( - auth_headers, INVESTIGATION_SMOKE_COMPACT, config.test_provider_id - ) + return call_provider_details_endpoint(auth_headers, INVESTIGATION_SMOKE_COMPACT, config.test_provider_id) def clean_investigation_records(): @@ -151,9 +149,7 @@ def _get_privilege_data_from_provider_response(provider_data: dict, jurisdiction ) -def _verify_no_investigation_exists( - record_type: str, jurisdiction: str, license_type: str, auth_headers: dict -): +def _verify_no_investigation_exists(record_type: str, jurisdiction: str, license_type: str, auth_headers: dict): """ Verify that no open investigation records exist in the database and no investigation status or objects on the record. @@ -193,9 +189,7 @@ def _verify_no_investigation_exists( raise SmokeTestFailureException('Investigation objects still exist in API response') -def _verify_investigation_exists( - record_type: str, jurisdiction: str, license_type: str, auth_headers: dict -): +def _verify_investigation_exists(record_type: str, jurisdiction: str, license_type: str, auth_headers: dict): """ Verify that an open investigation exists and the record has investigation status. diff --git a/backend/cosmetology-app/tests/smoke/smoke_common.py b/backend/cosmetology-app/tests/smoke/smoke_common.py index 7899bc1ba..0ea4264d2 100644 --- a/backend/cosmetology-app/tests/smoke/smoke_common.py +++ b/backend/cosmetology-app/tests/smoke/smoke_common.py @@ -164,9 +164,6 @@ def get_user_tokens(email, password=_TEST_STAFF_USER_PASSWORD, is_staff=False): raise e - - - def get_staff_user_auth_headers(username: str, password: str = _TEST_STAFF_USER_PASSWORD): tokens = get_user_tokens(username, password, is_staff=True) return { From b8a2a0a0b4f19248c88e5f24e3780bf87bd75d5f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 21:55:32 -0500 Subject: [PATCH 14/20] Update license rollback smoke tests --- .../rollback_license_upload_smoke_tests.py | 189 ++++++++++-------- 1 file changed, 101 insertions(+), 88 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py index 9eee59c39..3770374b8 100644 --- a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py @@ -2,6 +2,7 @@ #!/usr/bin/env python3 import json import time +import uuid from datetime import UTC, datetime, timedelta import boto3 @@ -22,6 +23,12 @@ load_smoke_test_env, ) +""" +Test to verify that license records can be rolled back using rollback step function + +Note that these tests upload license records into the system +""" + COMPACT = 'cosm' JURISDICTION = 'az' TEST_STAFF_USER_EMAIL = 'testStaffUserLicenseRollback@smokeTestFakeEmail.com' @@ -30,15 +37,27 @@ LICENSE_TYPE = 'cosmetologist' # Test configuration -NUM_LICENSES_TO_UPLOAD = 300 -BATCH_SIZE = 100 # Upload in batches of 100 +NUM_LICENSES_TO_UPLOAD = 100 +BATCH_SIZE = 100 # Upload in batches of 100 (single batch at default scale) +# First upload: API returns after SQS enqueue; provider rows appear as preprocess + ingest drain (queue + batch size). +FIRST_UPLOAD_PROVIDER_INGEST_MAX_WAIT_SEC = 180 +# Second upload re-uses existing providers; wait_for_all_providers_created returns immediately while ingest +# (SQS/Lambda) may still be writing license_update rows — buffer before polling, then retry below. +SECOND_UPLOAD_INGEST_BUFFER_SEC = 30 +LICENSE_UPDATE_VERIFY_MAX_RETRIES = 12 +LICENSE_UPDATE_VERIFY_RETRY_SLEEP_SEC = 20 # Global list to track all provider IDs for cleanup ALL_PROVIDER_IDS = [] def upload_test_license_batch( - auth_headers: dict, batch_start_index: int, batch_size: int, street_address: str = '123 Test Street' + auth_headers: dict, + batch_start_index: int, + batch_size: int, + street_address: str = '123 Test Street', + *, + family_name: str, ): """ Upload a batch of test license records. @@ -57,17 +76,16 @@ def upload_test_license_batch( 'licenseNumber': f'ROLLBACK-TEST-{i:04d}', 'homeAddressPostalCode': '68001', 'givenName': f'TestProvider{i:04d}', - # keep the family name consistent so we can query for all the providers which requires an exact - # match on the family name - 'familyName': 'RollbackTest', + # Per-run family name isolates the provider query from leftover "RollbackTest*" rows in shared sandboxes. + 'familyName': family_name, 'homeAddressStreet1': street_address, 'dateOfBirth': '1985-01-01', 'dateOfIssuance': '2020-01-01', - 'ssn': f'999-50-{i:04d}', # Incrementing SSN with padded zeros + 'ssn': f'555-50-{i:04d}', # Incrementing SSN with padded zeros 'licenseType': LICENSE_TYPE, 'dateOfExpiration': '2050-12-10', 'homeAddressState': 'AZ', - 'homeAddressCity': 'Omaha', + 'homeAddressCity': 'Phoenix', 'compactEligibility': 'eligible', 'licenseStatus': 'active', } @@ -96,7 +114,12 @@ def upload_test_license_batch( def upload_test_licenses( - auth_headers: dict, num_licenses: int, batch_size: int, street_address: str = '123 Test Street' + auth_headers: dict, + num_licenses: int, + batch_size: int, + street_address: str = '123 Test Street', + *, + family_name: str, ): """ Upload test license records in batches. @@ -113,7 +136,9 @@ def upload_test_licenses( for batch_start in range(0, num_licenses, batch_size): current_batch_size = min(batch_size, num_licenses - batch_start) - batch_licenses = upload_test_license_batch(auth_headers, batch_start, current_batch_size, street_address) + batch_licenses = upload_test_license_batch( + auth_headers, batch_start, current_batch_size, street_address, family_name=family_name + ) all_licenses.extend(batch_licenses) # Small delay between batches to avoid rate limiting @@ -142,42 +167,50 @@ def verify_license_update_records_created(provider_ids, retry_count: int = 0): provider_ids_to_retry.append(provider_id) if provider_ids_to_retry: - if retry_count >= 3: + if retry_count >= LICENSE_UPDATE_VERIFY_MAX_RETRIES: raise SmokeTestFailureException( - f'failed to find license update records for {len(provider_ids_to_retry)} providers after 3 retries' + f'failed to find license update records for {len(provider_ids_to_retry)} providers after ' + f'{LICENSE_UPDATE_VERIFY_MAX_RETRIES} retries ' + f'({LICENSE_UPDATE_VERIFY_RETRY_SLEEP_SEC}s between retries)' ) - time.sleep(10) - logger.info(f'retrying {len(provider_ids_to_retry)} providers after 10 seconds...') + time.sleep(LICENSE_UPDATE_VERIFY_RETRY_SLEEP_SEC) + logger.info( + f'retrying {len(provider_ids_to_retry)} providers after {LICENSE_UPDATE_VERIFY_RETRY_SLEEP_SEC} seconds...' + ) verify_license_update_records_created(provider_ids_to_retry, retry_count + 1) else: logger.info('all license update records found') -def wait_for_all_providers_created(staff_headers: dict, expected_count: int, max_wait_time: int = 120): +def wait_for_all_providers_created( + staff_headers: dict, + expected_count: int, + max_wait_time: int = 120, + *, + family_name: str, +): """ Wait for all provider records to be created from uploaded licenses. :param staff_headers: Authentication headers for staff user :param expected_count: Expected number of providers to be created - :param max_wait_time: Maximum time to wait in seconds (default: 900 = 15 minutes) - :return: List of provider IDs that were created + :param max_wait_time: Maximum time to wait in seconds (default: 120) + :return: List of all provider IDs matching family_name+jurisdiction (length should match expected_count when + the run is isolated via a unique family_name). """ - logger.info(f'Waiting for {expected_count} provider records to be created...') + logger.info(f'Waiting for {expected_count} provider records to be created (familyName={family_name})...') start_time = time.time() check_interval = 5 - # Query using the common family name prefix 'RollbackTest' - # The API will return all providers with family names starting with this prefix - - last_key = None - page_num = 1 all_provider_ids: set[str] = set() while time.time() - start_time < max_wait_time: + page_num = 1 + last_key = None # Collect all providers across all pages while True: query_body = { - 'query': {'familyName': 'RollbackTest'}, + 'query': {'familyName': family_name, 'jurisdiction': JURISDICTION}, 'pagination': {'pageSize': 100}, } if last_key: @@ -219,12 +252,18 @@ def wait_for_all_providers_created(staff_headers: dict, expected_count: int, max num_found = len(all_provider_ids) logger.info( - f'Found {num_found}/{expected_count} providers with family name "RollbackTest" (across {page_num} pages)' + f'Found {num_found}/{expected_count} providers with family name "{family_name}" ' + f'in jurisdiction "{JURISDICTION}" (across {page_num} pages)' ) if num_found >= expected_count: + if num_found > expected_count: + logger.warning( + f'More providers ({num_found}) than uploads ({expected_count}) matched the query; ' + 'use a unique run family_name if this is unexpected.' + ) logger.info(f'All {expected_count} providers found!') - return list(all_provider_ids) # Return only the expected count + return list(all_provider_ids) elapsed = time.time() - start_time if elapsed < max_wait_time: @@ -341,41 +380,6 @@ def get_rollback_results_from_s3(results_s3_key: str): return results -def create_privilege_for_provider(provider_id: str, compact: str): - """ - Manually create a privilege record for a provider to test skip conditions. - - :param provider_id: The provider ID to create privilege for - :param compact: The compact abbreviation - """ - from datetime import date - - # Create a privilege record for a different jurisdiction (e.g., 'co' for Colorado) - privilege_jurisdiction = 'co' - license_type_abbr = 'cos' - - privilege_record = { - 'pk': f'{compact}#PROVIDER#{provider_id}', - 'sk': f'{compact}#PROVIDER#privilege/{privilege_jurisdiction}/{license_type_abbr}#', - 'type': 'privilege', - 'providerId': provider_id, - 'compact': compact, - 'jurisdiction': privilege_jurisdiction, - 'licenseJurisdiction': JURISDICTION, - 'licenseType': LICENSE_TYPE, - 'dateOfIssuance': datetime.now(tz=UTC).isoformat(), - 'dateOfRenewal': datetime.now(tz=UTC).isoformat(), - 'dateOfExpiration': date(2050, 12, 10).isoformat(), - 'dateOfUpdate': datetime.now(tz=UTC).isoformat(), - 'privilegeId': f'{license_type_abbr.upper()}-{privilege_jurisdiction.upper()}-12345', - 'administratorSetStatus': 'active', - 'compactTransactionId': 'test-transaction-12345', - } - - config.provider_user_dynamodb_table.put_item(Item=privilege_record) - logger.info(f'Created privilege record for provider {provider_id}') - - def create_encumbrance_update_for_provider(provider_id: str, compact: str, license_jurisdiction: str): """ Manually create a license encumbrance update record to test skip conditions. @@ -613,6 +617,9 @@ def rollback_license_upload_smoke_test(): # Get authentication headers using app client auth_headers = get_client_auth_headers(client_id, client_secret, COMPACT, JURISDICTION) + run_family_name = f'RollbackTest-{uuid.uuid4().hex[:8]}' + logger.info(f'Run-scoped familyName for uploads and queries: {run_family_name}') + # Step 1: Upload test licenses (first time) logger.info('=' * 80) logger.info('STEP 1: Uploading test licenses (first time)') @@ -624,6 +631,7 @@ def rollback_license_upload_smoke_test(): NUM_LICENSES_TO_UPLOAD, BATCH_SIZE, street_address='123 Test Street', + family_name=run_family_name, ) first_upload_end_time = datetime.now(tz=UTC) logger.info( @@ -635,7 +643,12 @@ def rollback_license_upload_smoke_test(): logger.info('Waiting for first upload providers and license records to be created...') logger.info('=' * 80) time.sleep(10) - wait_for_all_providers_created(staff_headers, len(uploaded_licenses)) + wait_for_all_providers_created( + staff_headers, + len(uploaded_licenses), + max_wait_time=FIRST_UPLOAD_PROVIDER_INGEST_MAX_WAIT_SEC, + family_name=run_family_name, + ) logger.info('✅ All first upload license records have been created') # Step 2: Upload test licenses again with different address to create update records @@ -648,6 +661,7 @@ def rollback_license_upload_smoke_test(): NUM_LICENSES_TO_UPLOAD, BATCH_SIZE, street_address='456 Updated Street', + family_name=run_family_name, ) logger.info('Second upload completed - update records should be created') @@ -657,7 +671,16 @@ def rollback_license_upload_smoke_test(): logger.info('STEP 3: Waiting for provider records and update records to be created') logger.info('=' * 80) - provider_ids = wait_for_all_providers_created(staff_headers, len(uploaded_licenses)) + logger.info( + f'Waiting {SECOND_UPLOAD_INGEST_BUFFER_SEC}s for second-upload ingest' + ) + time.sleep(SECOND_UPLOAD_INGEST_BUFFER_SEC) + + provider_ids = wait_for_all_providers_created( + staff_headers, + len(uploaded_licenses), + family_name=run_family_name, + ) # Store all provider IDs globally for cleanup ALL_PROVIDER_IDS = provider_ids.copy() @@ -669,19 +692,9 @@ def rollback_license_upload_smoke_test(): logger.info(f'Found {len(provider_ids)} provider records') - # Step 4: Create privilege for first provider (should be skipped in rollback) - logger.info('=' * 80) - logger.info('STEP 4: Creating privilege for first provider to test skip condition') - logger.info('=' * 80) - - first_provider_id = provider_ids[0] - create_privilege_for_provider(first_provider_id, COMPACT) - skipped_provider_ids.append(first_provider_id) - logger.info(f'Created privilege for provider {first_provider_id} - should be skipped in rollback') - - # Step 5: Create encumbrance update for second provider (should be skipped in rollback) + # Step 4: Create encumbrance update for second provider (should be skipped in rollback) logger.info('=' * 80) - logger.info('STEP 5: Creating encumbrance update for second provider to test skip condition') + logger.info('STEP 4: Creating encumbrance update for second provider to test skip condition') logger.info('=' * 80) second_provider_id = provider_ids[1] @@ -693,9 +706,9 @@ def rollback_license_upload_smoke_test(): logger.info('Waiting briefly for test records to propagate...') time.sleep(5) - # Step 6: Start rollback step function + # Step 5: Start rollback step function logger.info('=' * 80) - logger.info('STEP 6: Starting rollback step function') + logger.info('STEP 5: Starting rollback step function') logger.info('=' * 80) rollback_start = first_upload_start_time @@ -710,18 +723,18 @@ def rollback_license_upload_smoke_test(): end_datetime=rollback_end, ) - # Step 7: Wait for step function completion + # Step 6: Wait for step function completion logger.info('=' * 80) - logger.info('STEP 7: Waiting for step function to complete') + logger.info('STEP 6: Waiting for step function to complete') logger.info('=' * 80) status, output = wait_for_step_function_completion(execution_arn) logger.info(f'Step function output: {json.dumps(output, indent=2)}') - # Step 8: Retrieve and verify results from S3 + # Step 7: Retrieve and verify results from S3 logger.info('=' * 80) - logger.info('STEP 8: Retrieving and verifying results from S3') + logger.info('STEP 7: Retrieving and verifying results from S3') logger.info('=' * 80) results_s3_key = output.get('resultsS3Key') @@ -730,21 +743,21 @@ def rollback_license_upload_smoke_test(): results = get_rollback_results_from_s3(results_s3_key) - # Expect all providers reverted except for the 2 skipped - expected_reverted = NUM_LICENSES_TO_UPLOAD - 2 - expected_skipped = 2 + # Expect all providers reverted except for the 1 skipped + expected_reverted = NUM_LICENSES_TO_UPLOAD - 1 + expected_skipped = 1 verify_rollback_results(results, expected_reverted, expected_skipped) - # Step 9: Verify providers deleted from database (except the 2 skipped ones) + # Step 8: Verify providers deleted from database (except the skipped one) logger.info('=' * 80) - logger.info('STEP 9: Verifying providers were deleted from database') + logger.info('STEP 8: Verifying providers were deleted from database') logger.info('=' * 80) verify_providers_deleted_from_database(results, COMPACT) - # Step 10: Clean up the 2 skipped provider records + # Step 9: Clean up the skipped provider records logger.info('=' * 80) - logger.info('STEP 10: Cleaning up skipped provider records') + logger.info('STEP 9: Cleaning up skipped provider records') logger.info('=' * 80) delete_all_provider_records(skipped_provider_ids, COMPACT) From c6fcf710aaa8f4cb33e33301a0fe33208021e177 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Apr 2026 22:35:24 -0500 Subject: [PATCH 15/20] Complete smoke test cleanup --- backend/cosmetology-app/tests/smoke/README.md | 142 ++++++++++++++++++ backend/cosmetology-app/tests/smoke/config.py | 15 -- .../tests/smoke/license_upload_smoke_tests.py | 50 +++--- 3 files changed, 171 insertions(+), 36 deletions(-) create mode 100644 backend/cosmetology-app/tests/smoke/README.md diff --git a/backend/cosmetology-app/tests/smoke/README.md b/backend/cosmetology-app/tests/smoke/README.md new file mode 100644 index 000000000..3d81bae30 --- /dev/null +++ b/backend/cosmetology-app/tests/smoke/README.md @@ -0,0 +1,142 @@ +# Smoke Tests + +This directory contains smoke tests for the Compact Connect Cosmetology API. Smoke tests are end-to-end integration tests that run against a test environment to verify that critical functionality works as expected. + +## Overview + +Smoke tests validate that key features of the Compact Connect API are working correctly in a test environment. They make real API calls and interact with actual AWS services (DynamoDB, Cognito, etc.) to ensure the system behaves correctly end-to-end. + +## Prerequisites + +Before running smoke tests, you must complete the following setup: + +### 1. Sandbox/Test Environment + +You must have access to a deployed sandbox environment of the Compact Connect Cosmetology API. The sandbox should be deployed with the following configuration: + +- **Security Profile**: Your `cdk.context.json` file must have `"security_profile": "VULNERABLE"` set. This allows the smoke tests to create users programmatically using the boto3 Cognito client. +- +### 2. AWS Credentials + +Ensure your AWS credentials are configured with appropriate permissions to: +- Access DynamoDB tables in the sandbox environment +- Access Cognito user pools in the sandbox environment +- Access other AWS services used by the smoke tests + +1. Configure your AWS profile to use SSO: + ```bash + aws configure sso + ``` + Follow the prompts to set up your SSO profile using the values from your IAM identity center login + (see [AWS CLI SSO Configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html#sso-configure-profile-token-auto-sso)) + +2. Log in to AWS SSO: + ```bash + aws sso login --profile + ``` + +3. Set your AWS profile environment variable (if not using the default profile): + ```bash + export AWS_PROFILE= + ``` + +### 3. Python Dependencies + +Install the required Python packages. The smoke tests use the same dependencies as the main codebase. Ensure you have: +- Python 3.x +- All dependencies from the project's requirements files + +### 4. Upload Test License Record + +You must have a test license record uploaded in your sandbox environment to generate a provider record. This license/provider will be used by the smoke tests to perform various tests. Once you have uploaded the license record into your environment, you will need to look up the provider id generated for that record in the Provider DynamoDB table and set it in your environment variables (see Environment Variables Setup below). + + +## Environment Variables Setup + +1. **Copy the example environment file:** + ```bash + cp smoke_tests_env_example.json smoke_tests_env.json + ``` + +2. **Edit `smoke_tests_env.json`** with your sandbox environment values: + + **Required Variables:** + - `CC_TEST_API_BASE_URL`: Base URL for the Compact Connect API (e.g., `https://api.sandbox.compactconnect.org`) + - `CC_TEST_STATE_API_BASE_URL`: Base URL for the state API + - `CC_TEST_STATE_AUTH_URL`: OAuth2 token endpoint for state authentication + - `CC_TEST_COGNITO_STATE_AUTH_USER_POOL_ID`: Cognito user pool ID for state auth + - `CC_TEST_PROVIDER_DYNAMO_TABLE_NAME`: DynamoDB table name for provider data + - `CC_TEST_COMPACT_CONFIGURATION_DYNAMO_TABLE_NAME`: DynamoDB table name for compact configuration + - `CC_TEST_DATA_EVENT_DYNAMO_TABLE_NAME`: DynamoDB table name for data events + - `CC_TEST_STAFF_USER_DYNAMO_TABLE_NAME`: DynamoDB table name for staff users + - `CC_TEST_COGNITO_STAFF_USER_POOL_ID`: Cognito user pool ID for staff users + - `CC_TEST_COGNITO_STAFF_USER_POOL_CLIENT_ID`: Cognito client ID for staff users + - `CC_TEST_PROVIDER_ID`: Provider id of your test provider user + - `ENVIRONMENT_NAME`: Name of your sandbox environment + - `AWS_DEFAULT_REGION`: AWS region where your sandbox is deployed (e.g., `us-east-1`) + + **Optional Variables (for specific tests):** + - `CC_TEST_ROLLBACK_STEP_FUNCTION_ARN`: Step function ARN for rollback tests + - `CC_TEST_RATE_LIMITING_DYNAMO_TABLE_NAME`: DynamoDB table name for rate limiting + - `CC_TEST_SSN_DYNAMO_TABLE_NAME`: DynamoDB table name for SSN data + - `CC_TEST_GET_PROVIDER_SSN_LAMBDA_NAME`: Lambda function name for SSN retrieval + +3. **Important:** Never commit `smoke_tests_env.json` to version control. It contains sensitive credentials and should be in `.gitignore`. + +## Running Smoke Tests + +### Running Individual Test Files + +Each test file can be run independently from the cosmetology-app folder: + +```bash +# Navigate to the compact-connect directory +cd backend/cosmetology-app + +# Run a specific test file +python3 tests/smoke/encumbrance_smoke_tests.py +``` + +## Special Test Requirements + +### Tests Creating Test Data + +Many tests create temporary test data (staff users, configurations, etc.) and clean it up automatically. However, if a test fails partway through, you may need to manually clean up test data. + +## Troubleshooting + +### Common Issues + +1. **"ResourceNotFoundException" when accessing DynamoDB tables** + - Verify that your `smoke_tests_env.json` has the correct table names for your sandbox environment + - Ensure your AWS credentials have permissions to access the tables + - Check that the tables exist in the specified region + +2. **"Failed to authenticate" or Cognito errors** + - Check that `security_profile: "VULNERABLE"` is set in your `cdk.context.json` + + +### Triage Test Failures + +If a test fails, you can consider the following steps to triage the cause of the failures: + +1. Review CloudWatch logs for Lambda functions that were invoked +2. Check DynamoDB tables directly using the AWS Console or CLI +3. Check Cognito user pools to see if test users were created + +## Contributing + +When adding new smoke tests: + +1. Follow the existing pattern in other test files +2. Use `SmokeTestFailureException` for test failures +3. Include cleanup logic for any test data created +4. Add appropriate docstrings explaining what the test does +5. Update this README with information about your new test if there are any special requirements + +## Additional Resources + +- See individual test files for specific requirements and usage examples +- Check `smoke_common.py` for shared utilities and helper functions +- Review `config.py` to understand how environment variables are loaded + diff --git a/backend/cosmetology-app/tests/smoke/config.py b/backend/cosmetology-app/tests/smoke/config.py index 8ac9f2d1f..a86ee8bde 100644 --- a/backend/cosmetology-app/tests/smoke/config.py +++ b/backend/cosmetology-app/tests/smoke/config.py @@ -72,21 +72,6 @@ def cognito_staff_user_pool_id(self): def test_provider_id(self): return os.environ['CC_TEST_PROVIDER_ID'] - @property - def smoke_read_general_staff_email(self): - return os.environ.get( - 'CC_TEST_SMOKE_READ_GENERAL_STAFF_EMAIL', - 'testStaffUserLicenseUploader@smokeTestFakeEmail.com', - ) - - @property - def sandbox_authorize_net_api_login_id(self): - return os.environ['SANDBOX_AUTHORIZE_NET_API_LOGIN_ID'] - - @property - def sandbox_authorize_net_transaction_key(self): - return os.environ['SANDBOX_AUTHORIZE_NET_TRANSACTION_KEY'] - @property def smoke_test_notification_email(self): return os.environ['CC_TEST_SMOKE_TEST_NOTIFICATION_EMAIL'] diff --git a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py index bb6889a22..5eb8b6d42 100644 --- a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py @@ -4,12 +4,15 @@ from datetime import UTC, datetime, timedelta import requests -from config import logger +from config import config, logger from smoke_common import ( SmokeTestFailureException, + create_test_app_client, create_test_staff_user, + delete_test_app_client, delete_test_staff_user, get_api_base_url, + get_client_auth_headers, get_data_events_dynamodb_table, get_provider_user_dynamodb_table, get_staff_user_auth_headers, @@ -22,16 +25,16 @@ TEST_PROVIDER_GIVEN_NAME = 'Joe' TEST_PROVIDER_FAMILY_NAME = 'Dokes' -# This script can be run locally to test the license upload/ingest flow against a sandbox environment -# of the Compact Connect API. -# Your sandbox account must be deployed with the "security_profile": "VULNERABLE" setting in your cdk.context.json -# To run this script, create a smoke_tests_env.json file in the same directory as this script using the -# 'smoke_tests_env_example.json' file as a template. +# This script can be run locally to test the license upload/ingest flow against a sandbox environment. +# License POST uses the state API (CC_TEST_STATE_API_BASE_URL) with a short-lived Cognito app client +# (CC_TEST_STATE_AUTH_URL, CC_TEST_COGNITO_STATE_AUTH_USER_POOL_ID); provider query/GET use the internal API +# (CC_TEST_API_BASE_URL) with a staff user. Configure smoke_tests_env.json from smoke_tests_env_example.json. # Note that by design, developers do not have the ability to delete records from the SSN DynamoDB table, # so this script does not delete the created SSN records as part of cleanup. TEST_STAFF_USER_EMAIL = 'testStaffUserLicenseUploader@smokeTestFakeEmail.com' +TEST_APP_CLIENT_NAME = 'test-license-upload-smoke-client' def _cleanup_test_generated_records(provider_id: str, license_ingest_record_response: dict): @@ -57,19 +60,18 @@ def _cleanup_test_generated_records(provider_id: str, license_ingest_record_resp logger.info('Successfully deleted license ingest record from data events table') -def upload_licenses_record(): +def upload_licenses_record(license_upload_auth_headers: dict): """ Verifies that a license record can be uploaded to the Compact Connect API and the appropriate records are created in the provider table as well as the data events table. - Step 1: Upload a license record through the POST '/v1/compacts/cosm/jurisdictions/az/licenses' endpoint. - Step 2: Verify the provider records are added by querying the API. + Step 1: Upload a license via the state API (POST .../licenses) using state app client credentials. + Step 2: Verify the provider records are added by querying the internal staff API. Step 3: Verify the license record is recorded in the data events table. """ + staff_headers = get_staff_user_auth_headers(TEST_STAFF_USER_EMAIL) - headers = get_staff_user_auth_headers(TEST_STAFF_USER_EMAIL) - - # Step 1: Upload a license record through the POST '/v1/compacts/cosm/jurisdictions/az/licenses' endpoint. + # Step 1: State-authenticated license upload (see stacks/state_api_stack). post_body = [ { 'licenseNumber': 'A0608337260', @@ -90,10 +92,10 @@ def upload_licenses_record(): ] post_response = requests.post( - url=get_api_base_url() + f'/v1/compacts/{COMPACT}/jurisdictions/{JURISDICTION}/licenses', - headers=headers, + url=f'{config.state_api_base_url}/v1/compacts/{COMPACT}/jurisdictions/{JURISDICTION}/licenses', + headers=license_upload_auth_headers, json=post_body, - timeout=10, + timeout=60, ) if post_response.status_code != 200: @@ -112,7 +114,7 @@ def upload_licenses_record(): query_response = requests.post( url=get_api_base_url() + f'/v1/compacts/{COMPACT}/providers/query', - headers=headers, + headers=staff_headers, json=query_body, timeout=10, ) @@ -147,7 +149,7 @@ def upload_licenses_record(): # Now get the provider details to verify the license record provider_details_response = requests.get( url=get_api_base_url() + f'/v1/compacts/{COMPACT}/providers/{provider_id}', - headers=headers, + headers=staff_headers, timeout=10, ) @@ -171,7 +173,7 @@ def upload_licenses_record(): # Step 3: Verify the license record is recorded in the data events table. # we don't loop here because the record should be available in the data events table by the time the - # provider table record is available + # provider table record is available. We use a consistent read to ensure that we get the latest record. data_events_table = get_data_events_dynamodb_table() event_time = datetime.now(tz=UTC) start_time = event_time - timedelta(minutes=15) @@ -179,10 +181,11 @@ def upload_licenses_record(): license_ingest_record_response = data_events_table.query( KeyConditionExpression='pk = :pk AND sk BETWEEN :start_time AND :end_time', ExpressionAttributeValues={ - ':pk': 'COMPACT#cosm#JURISDICTION#az', + ':pk': f'COMPACT#{COMPACT}#JURISDICTION#{JURISDICTION}', ':start_time': f'TYPE#license.ingest#TIME#{int(start_time.timestamp())}', ':end_time': f'TYPE#license.ingest#TIME#{int(event_time.timestamp())}', }, + ConsistentRead=True, ) if not license_ingest_record_response.get('Items'): @@ -200,18 +203,23 @@ def upload_licenses_record(): if __name__ == '__main__': load_smoke_test_env() - # Create staff user with permission to upload licenses + # Create staff user with permission to query providers (internal API) test_user_sub = create_test_staff_user( email=TEST_STAFF_USER_EMAIL, compact=COMPACT, jurisdiction=JURISDICTION, permissions={'actions': {'admin'}, 'jurisdictions': {JURISDICTION: {'write', 'admin'}}}, ) + client_credentials = create_test_app_client(TEST_APP_CLIENT_NAME, COMPACT, JURISDICTION) + client_id = client_credentials['client_id'] + client_secret = client_credentials['client_secret'] try: - upload_licenses_record() + license_upload_headers = get_client_auth_headers(client_id, client_secret, COMPACT, JURISDICTION) + upload_licenses_record(license_upload_headers) logger.info('License record upload smoke test passed') except SmokeTestFailureException as e: logger.error(f'License record upload smoke test failed: {str(e)}') finally: + delete_test_app_client(client_id) # Clean up the test staff user delete_test_staff_user(TEST_STAFF_USER_EMAIL, user_sub=test_user_sub, compact=COMPACT) From e5a061a0658e176c5d59ca06dc99014bef8a51b4 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 09:24:32 -0500 Subject: [PATCH 16/20] Using cached config property to check live jurisdictions --- .../provider-data-v1/handlers/encumbrance.py | 4 +++- .../test_handlers/test_encumbrance.py | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py index cdd22ac90..6f0d53abd 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/encumbrance.py @@ -37,7 +37,9 @@ def _ensure_jurisdiction_live(compact: str, jurisdiction: str) -> None: - if not config.compact_configuration_client.is_jurisdiction_live_in_compact(compact, jurisdiction): + live_jurisdictions = config.live_compact_jurisdictions.get(compact, []) + normalized = jurisdiction.lower() + if normalized not in {j.lower() for j in live_jurisdictions}: raise CCInvalidRequestException('Jurisdiction is not live in this compact') diff --git a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index 0d011e6d9..ff8e5eff5 100644 --- a/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -58,6 +58,12 @@ def _generate_test_body(): class TestPostPrivilegeEncumbrance(TstFunction): """Test suite for privilege encumbrance endpoints.""" + def setUp(self): + super().setUp() + self.set_live_compact_jurisdictions_for_test( + {'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]} + ) + def _when_testing_privilege_encumbrance(self, body_overrides: dict | None = None): self.test_data_generator.put_default_provider_record_in_provider_table() self.test_data_generator.put_default_license_record_in_provider_table() @@ -238,6 +244,12 @@ def test_privilege_encumbrance_handler_handles_event_publishing_failure(self, mo class TestPostLicenseEncumbrance(TstFunction): """Test suite for license encumbrance endpoints.""" + def setUp(self): + super().setUp() + self.set_live_compact_jurisdictions_for_test( + {'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]} + ) + def _when_testing_valid_license_encumbrance(self, body_overrides: dict | None = None): self.test_data_generator.put_default_provider_record_in_provider_table() test_license_record = self.test_data_generator.put_default_license_record_in_provider_table() @@ -450,6 +462,12 @@ def test_license_encumbrance_handler_returns_400_if_encumbrance_date_in_future(s class TestPatchPrivilegeEncumbranceLifting(TstFunction): """Test suite for privilege encumbrance lifting endpoints.""" + def setUp(self): + super().setUp() + self.set_live_compact_jurisdictions_for_test( + {'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]} + ) + def _setup_privilege_with_adverse_action( self, adverse_action_overrides=None, @@ -763,6 +781,12 @@ def test_privilege_encumbrance_lifting_handler_handles_event_publishing_failure( class TestPatchLicenseEncumbranceLifting(TstFunction): """Test suite for license encumbrance lifting endpoints.""" + def setUp(self): + super().setUp() + self.set_live_compact_jurisdictions_for_test( + {'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]} + ) + def _setup_license_with_adverse_action(self, adverse_action_overrides=None, license_overrides=None): """Helper method to set up a license with an adverse action for testing.""" self.test_data_generator.put_default_provider_record_in_provider_table( From 0bf2a88e7a740bb1e11b58d3be182baa7251a269 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 12:09:38 -0500 Subject: [PATCH 17/20] PR feedback - smoke test refinement --- .../tests/smoke/license_upload_smoke_tests.py | 31 +++++++++++-------- .../rollback_license_upload_smoke_tests.py | 4 ++- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py index 5eb8b6d42..e20771230 100644 --- a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py @@ -203,23 +203,28 @@ def upload_licenses_record(license_upload_auth_headers: dict): if __name__ == '__main__': load_smoke_test_env() - # Create staff user with permission to query providers (internal API) - test_user_sub = create_test_staff_user( - email=TEST_STAFF_USER_EMAIL, - compact=COMPACT, - jurisdiction=JURISDICTION, - permissions={'actions': {'admin'}, 'jurisdictions': {JURISDICTION: {'write', 'admin'}}}, - ) - client_credentials = create_test_app_client(TEST_APP_CLIENT_NAME, COMPACT, JURISDICTION) - client_id = client_credentials['client_id'] - client_secret = client_credentials['client_secret'] + + test_user_sub = None + client_id = None try: + # Create staff user with permission to query providers (internal API) + test_user_sub = create_test_staff_user( + email=TEST_STAFF_USER_EMAIL, + compact=COMPACT, + jurisdiction=JURISDICTION, + permissions={'actions': {'admin'}, 'jurisdictions': {JURISDICTION: {'write', 'admin'}}}, + ) + client_credentials = create_test_app_client(TEST_APP_CLIENT_NAME, COMPACT, JURISDICTION) + client_id = client_credentials['client_id'] + client_secret = client_credentials['client_secret'] license_upload_headers = get_client_auth_headers(client_id, client_secret, COMPACT, JURISDICTION) upload_licenses_record(license_upload_headers) logger.info('License record upload smoke test passed') except SmokeTestFailureException as e: logger.error(f'License record upload smoke test failed: {str(e)}') finally: - delete_test_app_client(client_id) - # Clean up the test staff user - delete_test_staff_user(TEST_STAFF_USER_EMAIL, user_sub=test_user_sub, compact=COMPACT) + if client_id: + delete_test_app_client(client_id) + if test_user_sub: + # Clean up the test staff user + delete_test_staff_user(TEST_STAFF_USER_EMAIL, user_sub=test_user_sub, compact=COMPACT) diff --git a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py index 3770374b8..3a8ae5332 100644 --- a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py @@ -513,7 +513,9 @@ def verify_rollback_results(results: dict, expected_provider_count: int, expecte # Verify we got the expected number of reverted providers if num_reverted != expected_provider_count: - logger.warning(f'Expected {expected_provider_count} reverted providers but found {num_reverted}') + raise SmokeTestFailureException( + f'Expected {expected_provider_count} reverted providers but found {num_reverted}' + ) # Verify the reverted provider has the expected structure for i, summary in enumerate(reverted): From 1ad1fe19eb599d4965f788416657c15b47ce7a64 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 9 Apr 2026 16:52:00 -0500 Subject: [PATCH 18/20] PR feedback - fail fast with smoke test check --- .../smoke/rollback_license_upload_smoke_tests.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py index 3a8ae5332..204252022 100644 --- a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py @@ -256,12 +256,13 @@ def wait_for_all_providers_created( f'in jurisdiction "{JURISDICTION}" (across {page_num} pages)' ) - if num_found >= expected_count: - if num_found > expected_count: - logger.warning( - f'More providers ({num_found}) than uploads ({expected_count}) matched the query; ' - 'use a unique run family_name if this is unexpected.' - ) + if num_found > expected_count: + raise SmokeTestFailureException( + f'More providers ({num_found}) than uploads ({expected_count}) matched the query; ' + '(family_name="{family_name}", jurisdiction="{JURISDICTION}", compact="{COMPACT}"). ' + ) + + if num_found == expected_count: logger.info(f'All {expected_count} providers found!') return list(all_provider_ids) From ef246e7acf30e8da4e50a6032d0f1d6579ab99f0 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 9 Apr 2026 16:53:05 -0500 Subject: [PATCH 19/20] formatting --- .../cosmetology-app/tests/smoke/license_upload_smoke_tests.py | 2 +- .../tests/smoke/rollback_license_upload_smoke_tests.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py index e20771230..41f6fe1e8 100644 --- a/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py @@ -203,7 +203,7 @@ def upload_licenses_record(license_upload_auth_headers: dict): if __name__ == '__main__': load_smoke_test_env() - + test_user_sub = None client_id = None try: diff --git a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py index 204252022..f58c8f05f 100644 --- a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py @@ -674,9 +674,7 @@ def rollback_license_upload_smoke_test(): logger.info('STEP 3: Waiting for provider records and update records to be created') logger.info('=' * 80) - logger.info( - f'Waiting {SECOND_UPLOAD_INGEST_BUFFER_SEC}s for second-upload ingest' - ) + logger.info(f'Waiting {SECOND_UPLOAD_INGEST_BUFFER_SEC}s for second-upload ingest') time.sleep(SECOND_UPLOAD_INGEST_BUFFER_SEC) provider_ids = wait_for_all_providers_created( From b2d5d31eb1fb84102400c2be2937e1991a66a67b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 13 Apr 2026 08:52:53 -0500 Subject: [PATCH 20/20] PR feedback - fix smoke test print statement --- .../tests/smoke/rollback_license_upload_smoke_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py index f58c8f05f..37319334e 100644 --- a/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py +++ b/backend/cosmetology-app/tests/smoke/rollback_license_upload_smoke_tests.py @@ -259,7 +259,7 @@ def wait_for_all_providers_created( if num_found > expected_count: raise SmokeTestFailureException( f'More providers ({num_found}) than uploads ({expected_count}) matched the query; ' - '(family_name="{family_name}", jurisdiction="{JURISDICTION}", compact="{COMPACT}"). ' + f'(family_name="{family_name}", jurisdiction="{JURISDICTION}", compact="{COMPACT}"). ' ) if num_found == expected_count: