From 4bdfeeeb9af736468733c9de3702ef01158d8efb Mon Sep 17 00:00:00 2001 From: Tadhg O'Higgins <2626258+tadhg-ohiggins@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:19:19 -0700 Subject: [PATCH 1/2] Make tests match the actual shape of the data, change validation functions to handle that shape. --- .../cross_validation/sac_validation_shape.py | 71 +++++++++++++++++-- .../test_auditee_ueis_match.py | 4 +- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/backend/audit/cross_validation/sac_validation_shape.py b/backend/audit/cross_validation/sac_validation_shape.py index 0b62cd2b23..8bc69028fe 100644 --- a/backend/audit/cross_validation/sac_validation_shape.py +++ b/backend/audit/cross_validation/sac_validation_shape.py @@ -1,3 +1,30 @@ +camel_to_snake = { + "AdditionalUEIs": "additional_ueis", + "AuditInformation": "audit_information", + "CorrectiveActionPlan": "corrective_action_plan", + "FederalAwards": "federal_awards", + "FindingsText": "findings_text", + "FindingsUniformGuidance": "findings_uniform_guidance", + "GeneralInformation": "general_information", + "NotesToSefa": "notes_to_sefa", +} +snake_to_camel = {v: k for k, v in camel_to_snake.items()} +at_root_sections = ("audit_information", "general_information") + + +def get_shaped_section(sac, section_name): + """Extract either None or the appropriate dict from the section.""" + true_name = camel_to_snake.get(section_name, section_name) + section = getattr(sac, true_name, None) + if true_name in at_root_sections: + return section + + if section: + return section.get(snake_to_camel.get(true_name), {}) + + return None + + def sac_validation_shape(sac): """ Takes an instance of SingleAuditChecklist and converts it to the shape @@ -5,15 +32,47 @@ def sac_validation_shape(sac): This function exists so that as either the SingleAuditChecklist or the validation shape changes we only have to make adjustments in one place. + + The sections that have spreadsheet workbooks all have root-level properties that + are the camel-case names of those sections. This function eliminates these names + and moves the actual values to the top level as part of returning a structure + that's appropriate for passing to the validation functions. + + For example, if the Audit Information and Notes to SEFA sections have content, + this function wil return something like: + + { + "sf_sac_sections": { + "audit_information": { + "dollar_threshold": ..., + "is_going_concern_included": ..., + "is_internal_control_deficiency_disclosed": ..., + "is_internal_control_material_weakness_disclosed": ..., + "is_material_noncompliance_disclosed": ..., + "is_aicpa_audit_guide_included": ..., + "is_low_risk_auditee": ..., + [other audit_information fields] + }, + "notes_to_sefa": { + "auditee_uei": ..., + "accounting_policies": ..., + "is_minimis_rate_used": ..., + "rate_explained": ..., + "notes_to_sefa_entries": ..., + [other notes_to_sefa fields] + + }, + "federal_awards": None, + ... + }, + "sf_sac_meta": { ... }, + } + """ + shape = { "sf_sac_sections": { - "general_information": sac.general_information, - "federal_awards": sac.federal_awards, - "corrective_action_plan": sac.corrective_action_plan, - "findings_text": sac.findings_text, - "findings_uniform_guidance": sac.findings_uniform_guidance, - "additional_ueis": sac.additional_ueis, + k: get_shaped_section(sac, k) for k in camel_to_snake.values() }, "sf_sac_meta": { "submitted_by": sac.submitted_by, diff --git a/backend/audit/cross_validation/test_auditee_ueis_match.py b/backend/audit/cross_validation/test_auditee_ueis_match.py index 27c74ee286..695c487a47 100644 --- a/backend/audit/cross_validation/test_auditee_ueis_match.py +++ b/backend/audit/cross_validation/test_auditee_ueis_match.py @@ -36,7 +36,7 @@ def test_multiple_matching_auditee_ueis(self): sac = baker.make(SingleAuditChecklist) sac.general_information = {"auditee_uei": "123456789"} - sac.federal_awards = {"auditee_uei": "123456789"} + sac.federal_awards = {"FederalAwards": {"auditee_uei": "123456789"}} shaped_sac = sac_validation_shape(sac) @@ -49,7 +49,7 @@ def test_multiple_mismatched_auditee_ueis(self): sac = baker.make(SingleAuditChecklist) sac.general_information = {"auditee_uei": "123456789"} - sac.federal_awards = {"auditee_uei": "123456780"} + sac.federal_awards = {"FederalAwards": {"auditee_uei": "123456780"}} shaped_sac = sac_validation_shape(sac) From 52c41a77b04773ac500746bc7c85c6c10950ec72 Mon Sep 17 00:00:00 2001 From: Tadhg O'Higgins <2626258+tadhg-ohiggins@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:28:10 -0700 Subject: [PATCH 2/2] Make additional_ueis check use new shape, and add tests for additional_ueis validation. --- .../audit/cross_validation/additional_ueis.py | 6 +- .../cross_validation/test_additional_ueis.py | 140 ++++++++++++++++++ 2 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 backend/audit/cross_validation/test_additional_ueis.py diff --git a/backend/audit/cross_validation/additional_ueis.py b/backend/audit/cross_validation/additional_ueis.py index 4c69973754..c2acced3f9 100644 --- a/backend/audit/cross_validation/additional_ueis.py +++ b/backend/audit/cross_validation/additional_ueis.py @@ -16,10 +16,8 @@ def additional_ueis(sac_dict): auditee_uei = all_sections["general_information"].get("auditee_uei") addl_ueis = [] if all_sections.get("additional_ueis"): - addl_ueis = ( - all_sections.get("additional_ueis", {}) - .get("AdditionalUEIs", {}) - .get("additional_ueis_entries") + addl_ueis = all_sections.get("additional_ueis", {}).get( + "additional_ueis_entries" ) if addl_ueis_checked: """ diff --git a/backend/audit/cross_validation/test_additional_ueis.py b/backend/audit/cross_validation/test_additional_ueis.py new file mode 100644 index 0000000000..5ec4d3beff --- /dev/null +++ b/backend/audit/cross_validation/test_additional_ueis.py @@ -0,0 +1,140 @@ +from django.test import TestCase +from model_bakery import baker +from audit.models import SingleAuditChecklist + +from .additional_ueis import additional_ueis +from .errors import ( + err_additional_ueis_empty, + err_additional_ueis_has_auditee_uei, + err_additional_ueis_not_empty, +) +from .sac_validation_shape import sac_validation_shape + +ERROR_EMPTY = {"error": err_additional_ueis_empty()} +ERROR_PRESENT = {"error": err_additional_ueis_not_empty()} +ERROR_AUEI = {"error": err_additional_ueis_has_auditee_uei()} + + +class AdditionalUEIsTests(TestCase): + """ + General Information asks if there are additional UEIs; this answer needs to be + consistent with the Additional UEIs section. + """ + + def test_general_information_no_addl_ueis(self): + """ + For a SAC with General Information and a no answer, there should be no + additonal UEIs in that section. "No" answer plus no section = valid. + """ + sac = baker.make(SingleAuditChecklist) + sac.general_information = {"multiple_ueis_covered": False} + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, []) + + def test_general_information_no_addl_ueis_ueis_present(self): + """ + For a SAC with General Information and a no answer, there should be no + additonal UEIs in that section. + "No" answer plus data in section: invalid + """ + sac = baker.make(SingleAuditChecklist) + sac.general_information = {"multiple_ueis_covered": False} + sac.additional_ueis = { + "AdditionalUEIs": { + "auditee_uei": "123456789", + "additional_ueis_entries": [{"additional_uei": "987654321"}], + } + } + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, [ERROR_PRESENT]) + + def test_general_information_addl_ueis_no_addl_ueis_section(self): + """ + For a SAC with General Information and a yes answer, and no Additional UEIs + data, generate errors. + """ + sac = baker.make(SingleAuditChecklist) + sac.general_information = {"multiple_ueis_covered": True} + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, [ERROR_EMPTY]) + + def test_general_information_addl_ueis_no_addl_ueis_in_section(self): + """ + For a SAC with General Information and a yes answer, and no Additional UEIs + listed in the data, generate errors. + """ + sac = baker.make(SingleAuditChecklist) + sac.general_information = {"multiple_ueis_covered": True} + sac.additional_ueis = {} + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, [ERROR_EMPTY]) + + sac.additional_ueis = { + "AdditionalUEIs": { + "auditee_uei": "123456789", + "additional_ueis_entries": [], + } + } + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, [ERROR_EMPTY]) + + def test_general_information_addl_ueis_addl_ueis_in_section(self): + """ + For a SAC with General Information and a yes answer, and Additional UEIs + listed in the data, do not generate errors. + """ + sac = baker.make(SingleAuditChecklist) + + sac.general_information = {"multiple_ueis_covered": True} + sac.additional_ueis = { + "AdditionalUEIs": { + "auditee_uei": "123456789", + "additional_ueis_entries": [{"additional_uei": "987654321"}], + } + } + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, []) + + def test_auditee_ueis_in_addl_ueis(self): + """ + For a SAC with General Information and a yes answer, if the auditee_uei is + found in the additonal_ueis, generate an error. + """ + sac = baker.make(SingleAuditChecklist) + + sac.general_information = { + "auditee_uei": "123456789", + "multiple_ueis_covered": True, + } + sac.additional_ueis = { + "AdditionalUEIs": { + "auditee_uei": "123456789", + "additional_ueis_entries": [ + {"additional_uei": "987654321"}, + {"additional_uei": "123456789"}, + ], + } + } + + shaped_sac = sac_validation_shape(sac) + validation_result = additional_ueis(shaped_sac) + + self.assertEqual(validation_result, [ERROR_AUEI])