From d231bda7c26caa53a2b863a3e6250bd026b37f98 Mon Sep 17 00:00:00 2001 From: Anastasia Gradova <108748167+anagradova@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:45:38 -0700 Subject: [PATCH] Updates to test_views.py from old branch (#4546) * Updates to test_views.py from old branch 4213-improve-views-coverage * Updated to match new status for test and format * Updated for flake8 --- backend/audit/test_views.py | 595 ++++++++++++++++++++++++++++++++++-- 1 file changed, 565 insertions(+), 30 deletions(-) diff --git a/backend/audit/test_views.py b/backend/audit/test_views.py index 1c5b0dfcf..4ad864b08 100644 --- a/backend/audit/test_views.py +++ b/backend/audit/test_views.py @@ -1,44 +1,34 @@ -from datetime import datetime, timezone import json +from datetime import datetime, timezone from pathlib import Path from tempfile import NamedTemporaryFile from unittest.mock import patch -from django.test import Client, TestCase, TransactionTestCase -from django.contrib.auth import get_user_model -from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse - -from faker import Faker - -from model_bakery import baker - -from openpyxl import load_workbook -from openpyxl.cell import Cell - +from audit.cross_validation.naming import SECTION_NAMES as SN from audit.fixtures.excel import ( - ADDITIONAL_UEIS_TEMPLATE, + ADDITIONAL_EINS_ENTRY_FIXTURES, ADDITIONAL_EINS_TEMPLATE, - FEDERAL_AWARDS_TEMPLATE, + ADDITIONAL_UEIS_ENTRY_FIXTURES, + ADDITIONAL_UEIS_TEMPLATE, + CORRECTIVE_ACTION_PLAN_ENTRY_FIXTURES, CORRECTIVE_ACTION_PLAN_TEMPLATE, + FEDERAL_AWARDS_ENTRY_FIXTURES, + FEDERAL_AWARDS_TEMPLATE, + FINDINGS_TEXT_ENTRY_FIXTURES, FINDINGS_TEXT_TEMPLATE, + FINDINGS_UNIFORM_GUIDANCE_ENTRY_FIXTURES, FINDINGS_UNIFORM_GUIDANCE_TEMPLATE, - SECONDARY_AUDITORS_TEMPLATE, + FORM_SECTIONS, + NOTES_TO_SEFA_ENTRY_FIXTURES, NOTES_TO_SEFA_TEMPLATE, - CORRECTIVE_ACTION_PLAN_ENTRY_FIXTURES, - FINDINGS_TEXT_ENTRY_FIXTURES, - FINDINGS_UNIFORM_GUIDANCE_ENTRY_FIXTURES, - FEDERAL_AWARDS_ENTRY_FIXTURES, - ADDITIONAL_UEIS_ENTRY_FIXTURES, - ADDITIONAL_EINS_ENTRY_FIXTURES, SECONDARY_AUDITORS_ENTRY_FIXTURES, - NOTES_TO_SEFA_ENTRY_FIXTURES, - FORM_SECTIONS, + SECONDARY_AUDITORS_TEMPLATE, ) from audit.fixtures.single_audit_checklist import ( - fake_auditor_certification, fake_auditee_certification, + fake_auditor_certification, ) +from audit.forms import AuditeeCertificationStep2Form, AuditorCertificationStep1Form from audit.models import ( Access, SingleAuditChecklist, @@ -47,9 +37,17 @@ generate_sac_report_id, ) from audit.models.models import STATUS -from audit.cross_validation.naming import SECTION_NAMES as SN -from audit.views import MySubmissions +from audit.views import AuditeeCertificationStep2View, MySubmissions from dissemination.models import FederalAward, General +from django.contrib.auth import get_user_model +from django.core.exceptions import PermissionDenied +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import Client, RequestFactory, TestCase, TransactionTestCase +from django.urls import reverse +from faker import Faker +from model_bakery import baker +from openpyxl import load_workbook +from openpyxl.cell import Cell User = get_user_model() @@ -113,6 +111,7 @@ def _authed_post(client, user, view_str, kwargs=None, data=None): def _make_user_and_sac(**kwargs): + """Helper function for to make a user and basic sac""" user = baker.make(User) sac = baker.make(SingleAuditChecklist, **kwargs) return user, sac @@ -125,6 +124,7 @@ def _load_json(target): def _mock_gen_report_id(): + """Helper function for generate a sac report id""" return generate_sac_report_id(end_date=datetime.now().date().isoformat()) @@ -134,6 +134,7 @@ def _merge_dict_seq(seq): def _build_auditor_cert_dict(certification: dict, signature: dict) -> dict: + """Helper function for building a dictionary for auditor certification""" return { "auditor_certification": certification, "auditor_signature": signature, @@ -141,6 +142,7 @@ def _build_auditor_cert_dict(certification: dict, signature: dict) -> dict: def _build_auditee_cert_dict(certification: dict, signature: dict) -> dict: + """Helper function for building a dictionary for auditee certification""" return { "auditee_certification": certification, "auditee_signature": signature, @@ -217,21 +219,25 @@ def test_no_robots(self): class MySubmissionsViewTests(TestCase): def setUp(self): + """Setup function for users and client""" self.user = baker.make(User) self.user2 = baker.make(User) self.client = Client() def test_redirect_if_not_logged_in(self): + """Test that accessing submission page redirects if user is not logged in""" result = self.client.get(SUBMISSIONS_PATH) self.assertAlmostEqual(result.status_code, 302) def test_no_submissions_returns_empty_list(self): + """Test that an authenticated user with no submissions gets empty list""" self.client.force_login(user=self.user) data = MySubmissions.fetch_my_submissions(self.user) self.assertEqual(len(data), 0) def test_user_with_submissions_should_return_expected_data_columns(self): + """Test that a user with submissions gets data with expected columns""" self.client.force_login(user=self.user) self.user.profile.entry_form_data = ( VALID_ELIGIBILITY_DATA | VALID_AUDITEE_INFO_DATA @@ -251,6 +257,7 @@ def test_user_with_submissions_should_return_expected_data_columns(self): self.assertTrue("fiscal_year_end_date" in keys) def test_user_with_no_submissions_should_return_no_data(self): + """Test that another user with no submissions gets no data""" self.client.force_login(user=self.user) self.user.profile.entry_form_data = ( VALID_ELIGIBILITY_DATA | VALID_AUDITEE_INFO_DATA @@ -264,9 +271,40 @@ def test_user_with_no_submissions_should_return_no_data(self): class EditSubmissionViewTests(TestCase): - def test_redirect_if_not_logged_in(self): - result = self.client.get(reverse(EDIT_PATH, args=["SOME_REPORT_ID"])) - self.assertAlmostEqual(result.status_code, 302) + def setUp(self): + """Setup test factory, client, user, and report ID""" + self.factory = RequestFactory() + self.client = Client() + self.user = baker.make(User) + self.sac = baker.make( + SingleAuditChecklist, submission_status=STATUS.READY_FOR_CERTIFICATION + ) + self.url_name = "audit:EditSubmission" + self.report_id = "TEST_REPORT_ID" + self.url = reverse( + "audit:EditSubmission", kwargs={"report_id": self.sac.report_id} + ) + self.client.force_login(self.user) + baker.make( + "audit.Access", + sac=self.sac, + user=self.user, + role="certifying_auditee_contact", + ) + self.session = self.client.session + + def test_redirect_not_logged_in(self): + """Test that accessing edit submission page redirects if not authenticated""" + url = reverse(self.url_name, args=[self.report_id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + + def test_redirects_to_singleauditchecklist(self): + """Test that accessing edit submission redirects to SAC view""" + response = self.client.get(self.url) + self.assertRedirects( + response, reverse("singleauditchecklist", args=[self.sac.report_id]) + ) class SubmissionViewTests(TestCase): @@ -274,6 +312,115 @@ class SubmissionViewTests(TestCase): Testing for the final step: submitting. """ + def setUp(self): + """Set up test client, user, SAC, and URL""" + self.client = Client() + self.user = baker.make(User) + self.sac = baker.make( + SingleAuditChecklist, submission_status=STATUS.AUDITEE_CERTIFIED + ) + self.url = reverse("audit:Submission", kwargs={"report_id": self.sac.report_id}) + self.client.force_login(self.user) + baker.make( + "audit.Access", + sac=self.sac, + user=self.user, + role="certifying_auditee_contact", + ) + + def test_get_renders_template(self): + """Test that GET renders the submission template with correct context""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "audit/submission.html") + self.assertIn("report_id", response.context) + self.assertIn("submission_status", response.context) + self.assertEqual(response.context["report_id"], self.sac.report_id) + self.assertEqual( + response.context["submission_status"], self.sac.submission_status + ) + + def test_get_permission_denied_if_no_sac(self): + """Test that GET returns 403 if SAC does not exist""" + invalid_url = reverse("audit:Submission", kwargs={"report_id": "INVALID"}) + response = self.client.get(invalid_url) + self.assertEqual(response.status_code, 403) + + def test_get_access_denied_for_unauthorized_user(self): + """Test that GET returns 403 if user is unauthorized""" + self.client.logout() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + @patch("audit.models.SingleAuditChecklist.validate_full") + @patch("audit.views.views.sac_transition") + @patch("audit.views.views.remove_workbook_artifacts") + @patch("audit.views.views.SingleAuditChecklist.disseminate") + def test_post_successful( + self, mock_disseminate, mock_remove, mock_transition, mock_validate + ): + """Test that a valid submission transitions SAC to a disseminated state""" + mock_validate.return_value = [] + mock_disseminate.return_value = None + response = self.client.post(self.url) + + mock_validate.assert_called_once() + mock_disseminate.assert_called_once() + mock_transition.assert_called_with( + response.wsgi_request, self.sac, transition_to=STATUS.DISSEMINATED + ) + mock_remove.assert_called_once() + + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("audit:MySubmissions")) + + @patch("audit.views.views.SingleAuditChecklist.validate_full") + @patch("audit.views.views.sac_transition") + @patch("audit.views.views.SingleAuditChecklist.disseminate") + def test_post_validation_errors( + self, mock_disseminate, mock_transition, mock_validate + ): + """Test that validation errors are displayed if submission is invalid""" + mock_validate.return_value = ["Error 1", "Error 2"] + + self.sac.submission_status = STATUS.AUDITEE_CERTIFIED + self.sac.save() + + response = self.client.post(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed( + response, "audit/cross-validation/cross-validation-results.html" + ) + self.assertIn("errors", response.context) + self.assertListEqual(response.context["errors"], ["Error 1", "Error 2"]) + + mock_disseminate.assert_not_called() + mock_transition.assert_not_called() + + @patch("audit.views.views.General.objects.get") + @patch("audit.views.views.SingleAuditChecklist.validate_full") + def test_post_transaction_error(self, mock_validate, mock_general_get): + """Test that a transaction error during a submission is handled properly""" + self.sac.submission_status = STATUS.AUDITEE_CERTIFIED + self.sac.save() + + mock_validate.return_value = [] + mock_general_get.return_value = True + + response = self.client.post(self.url) + + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("audit:MySubmissions")) + + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, reverse("audit:MySubmissions")) + + def test_post_permission_denied_if_no_sac(self): + """Test that POST returns 403 if SAC does not exist""" + invalid_url = reverse("audit:Submission", kwargs={"report_id": "INVALID"}) + response = self.client.post(invalid_url) + self.assertEqual(response.status_code, 403) + def test_post_redirect(self): """ The status should be "disseminated" after the post. @@ -330,6 +477,26 @@ def test_post_redirect(self): self.assertEqual(sac_after.submission_status, STATUSES.DISSEMINATED) +class SubmissionGetTest(TestCase): + def setUp(self): + """Setup test user and client""" + self.user = baker.make(User) + self.client = Client() + self.url = reverse("audit:MySubmissions") + + def testValidSubmission(self): + """Test that a valid submission is displayed on the submissions page""" + self.client.force_login(self.user) + sac1 = baker.make(SingleAuditChecklist, submission_status=STATUS.IN_PROGRESS) + sac2 = baker.make(SingleAuditChecklist, submission_status=STATUS.DISSEMINATED) + baker.make(Access, user=self.user, sac=sac1) + baker.make(Access, user=self.user, sac=sac2) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "audit/my_submissions.html") + + class SubmissionStatusTests(TransactionTestCase): """ Tests the expected order of progression for submission_status. @@ -342,6 +509,7 @@ class SubmissionStatusTests(TransactionTestCase): """ def setUp(self): + """Setup user and client""" self.user = baker.make(User) self.client = Client() @@ -1348,6 +1516,34 @@ def test_late_file_upload(self, mock_scan_file): "no_late_changes", response.content.decode("utf-8") ) + def test_get_login_required(self): + """Test that uploading files requires user authentication""" + for form_section in FORM_SECTIONS: + response = self.client.get( + reverse( + f"audit:{form_section}", + kwargs={"report_id": "12345", "form_section": form_section}, + ) + ) + self.assertEqual(response.status_code, 403) + + def test_get_bad_report_id_returns_403(self): + """Test that uploading with a malformed or nonexistant report_id reutrns 403""" + user = baker.make(User) + self.client.force_login(user) + + for form_section in FORM_SECTIONS: + response = self.client.get( + reverse( + f"audit:{form_section}", + kwargs={ + "report_id": "this is not a report id", + "form_section": form_section, + }, + ) + ) + self.assertEqual(response.status_code, 403) + class SingleAuditReportFileHandlerViewTests(TestCase): def test_login_required(self): @@ -1411,6 +1607,7 @@ def test_no_file_attached_returns_400(self): @patch("audit.validators._scan_file") def test_valid_file_upload(self, mock_scan_file): + """Test that uploading a valid SAR update the SAC accordingly""" sac = _mock_login_and_scan( self.client, mock_scan_file, @@ -1656,3 +1853,341 @@ def test_valid_file_upload_for_notes_to_sefa(self, mock_scan_file): submission_events[event_count - 1].event, SubmissionEvent.EventType.NOTES_TO_SEFA_UPDATED, ) + + +class EditSubmissionTest(TestCase): + def setUp(self): + """Setup factory, client, user, SAC, and URL""" + self.factory = RequestFactory() + self.client = Client() + self.user = baker.make(User) + self.sac = baker.make( + SingleAuditChecklist, submission_status=STATUS.READY_FOR_CERTIFICATION + ) + self.url = reverse( + "audit:EditSubmission", kwargs={"report_id": self.sac.report_id} + ) + self.client.force_login(self.user) + baker.make( + "audit.Access", + sac=self.sac, + user=self.user, + role="certifying_auditee_contact", + ) + self.session = self.client.session + + def test_redirects_to_singleauditchecklist(self): + """Test that accessing edit submission redirects to SAC view""" + response = self.client.get(self.url) + self.assertRedirects( + response, reverse("singleauditchecklist", args=[self.sac.report_id]) + ) + + +class AuditorCertificationStep1ViewTests(TestCase): + def setUp(self): + """Setup client, user, SAC, and URL""" + self.client = Client() + self.user = baker.make(User) + self.sac = baker.make( + SingleAuditChecklist, submission_status=STATUS.READY_FOR_CERTIFICATION + ) + self.url = reverse( + "audit:AuditorCertification", kwargs={"report_id": self.sac.report_id} + ) + self.client.force_login(self.user) + baker.make( + "audit.Access", + sac=self.sac, + user=self.user, + role="certifying_auditor_contact", + ) + self.session = self.client.session + + def test_get_redirects_if_status_not_ready_for_certification(self): + """Test that GET redirects if SAC status is not READY_FOR_CERTIFICATION""" + self.sac.submission_status = STATUS.IN_PROGRESS + self.sac.save() + response = self.client.get(self.url) + self.assertRedirects( + response, f"/audit/submission-progress/{self.sac.report_id}" + ) + + def test_get_renders_template_if_valid_state(self): + """ + Test that GET renders the auditor certification setp 1 template when SAC + is in a valid state. + """ + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "audit/auditor-certification-step-1.html") + self.assertIn("form", response.context) + self.assertIsInstance(response.context["form"], AuditorCertificationStep1Form) + + def test_get_permission_denied_if_sac_not_found(self): + """Test that POST redirects if SAC report_id is not found""" + response = self.client.get( + reverse("audit:AuditorCertification", kwargs={"report_id": "INVALID"}) + ) + self.assertEqual(response.status_code, 403) + + def test_post_redirects_if_status_not_ready_for_certification(self): + """Test that POST redirects if SAC status is not READY_FOR_CERTIFICATION""" + self.sac.submission_status = STATUS.IN_PROGRESS + self.sac.save() + response = self.client.post(self.url, {"field": "value"}) + self.assertRedirects( + response, f"/audit/submission-progress/{self.sac.report_id}" + ) + + def test_post_valid_form(self): + """ + Test that submitting a valid form updates the session + and redirects correctly + """ + self.session["AuditorCertificationStep1Session"] = {"field": "value"} + self.session.save() + + form_data = { + "is_OMB_limited": True, + "is_auditee_responsible": True, + "has_used_auditors_report": True, + "has_no_auditee_procedures": True, + "is_FAC_releasable": True, + "is_accurate_and_complete": True, + } + + response = self.client.post(self.url, form_data) + + self.assertEqual(response.status_code, 302) + self.sac.refresh_from_db() + + self.assertRedirects( + response, + reverse("audit:AuditorCertificationConfirm", args=[self.sac.report_id]), + ) + self.assertIn("AuditorCertificationStep1Session", self.client.session) + self.assertEqual( + self.client.session["AuditorCertificationStep1Session"], form_data + ) + + def test_post_invalid_form(self): + """Test that submitting and invalid form renders an error template""" + response = self.client.post(self.url, {"invalid_field": ""}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "audit/auditor-certification-step-1.html") + self.assertIn("form", response.context) + self.assertIsInstance(response.context["form"], AuditorCertificationStep1Form) + self.assertTrue(response.context["form"].errors) + + def test_post_permission_denied_if_sac_not_found(self): + """Test that results in 403 if SAC report_id is not found""" + response = self.client.post( + reverse("audit:AuditorCertification", kwargs={"report_id": "INVALID"}) + ) + self.assertEqual(response.status_code, 403) + + +class AuditeeCertificationStep2ViewTests(TestCase): + def setUp(self): + """Setup client, user, SAC, and URL""" + self.client = Client() + self.user = baker.make(User) + self.sac = baker.make( + SingleAuditChecklist, submission_status=STATUS.AUDITOR_CERTIFIED + ) + self.url = reverse( + "audit:AuditeeCertificationConfirm", + kwargs={"report_id": self.sac.report_id}, + ) + self.client.force_login(self.user) + baker.make( + "audit.Access", + sac=self.sac, + user=self.user, + role="certifying_auditee_contact", + ) + self.session = self.client.session + + def test_get_redirects_if_no_step_1(self): + """Test that GET redirects if AuditeeCertificationStep1Session is missing""" + response = self.client.get(self.url) + self.assertRedirects( + response, reverse("audit:AuditeeCertification", args=[self.sac.report_id]) + ) + + def test_get_renders_template_if_valid_session(self): + """ + Test that GET renders the Auditee certification step 2 + if session is valid + """ + self.session["AuditeeCertificationStep1Session"] = {"field": "value"} + self.session.save() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "audit/auditee-certification-step-2.html") + self.assertIn("form", response.context) + self.assertIsInstance(response.context["form"], AuditeeCertificationStep2Form) + + def test_get_redirects_if_not_auditor_certified(self): + """Test that GET will redirect if SAC status is not AUDITOR_CERTIFIED""" + self.sac.submission_status = STATUS.IN_PROGRESS + self.sac.save() + self.session["AuditeeCertificationStep1Session"] = {"field": "value"} + self.session.save() + response = self.client.get(self.url) + self.assertRedirects( + response, f"/audit/submission-progress/{self.sac.report_id}" + ) + + @patch("audit.views.views.validate_auditee_certification_json") + @patch("audit.views.views.sac_transition") + def test_post_valid_form(self, mock_transition, mock_validate): + """ + Test that submitting a valid Auditee Certification Form + updates the SAC and redirects correctly + """ + mock_transition.return_value = True + mock_validate.return_value = {"auditee_certification": "validated_data"} + + self.session["AuditeeCertificationStep1Session"] = {"field": "value"} + self.session.save() + + self.sac.submission_status = STATUS.AUDITOR_CERTIFIED + self.sac.save() + + response = self.client.post( + self.url, + { + "auditee_certification_date_signed": "2024-01-01", + "auditee_name": "Test Auditee", + "auditee_title": "Auditor", + }, + ) + self.assertEqual(response.status_code, 302) + self.sac.refresh_from_db() + mock_transition.assert_called_once_with( + response.wsgi_request, self.sac, transition_to=STATUS.AUDITEE_CERTIFIED + ) + self.assertRedirects( + response, reverse("audit:SubmissionProgress", args=[self.sac.report_id]) + ) + + def test_post_invalid_form(self): + """ + Test that submitting an invalid Auditee Ceritifcation Form + renders an error template + """ + self.session["AuditeeCertificationStep1Session"] = None + self.session.save() + response = self.client.post(self.url, {"auditee_certification_date_signed": ""}) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "audit/auditee-certification-step-2.html") + self.assertIn("form", response.context) + self.assertIsInstance(response.context["form"], AuditeeCertificationStep2Form) + self.assertTrue(response.context["form"].errors) + + def test_post_redirects_if_status_not_auditor_certified(self): + """Test that POST will redirect if SAC submission status is not AUDITOR_CERTIFIED""" + self.sac.submission_status = STATUS.IN_PROGRESS + self.sac.save() + self.session["AuditeeCertificationStep1Session"] = {"field": "value"} + self.session.save() + response = self.client.post( + self.url, {"auditee_certification_date_signed": "2024-01-01"} + ) + self.assertRedirects( + response, f"/audit/submission-progress/{self.sac.report_id}" + ) + + def test_post_permission_denied_if_sac_not_found(self): + """Test that POST will result in permission error is SAC report_id not found""" + response = self.client.post( + reverse( + "audit:AuditeeCertificationConfirm", kwargs={"report_id": "INVALID"} + ) + ) + self.assertEqual(response.status_code, 403) + + def test_single_audit_checklist_does_not_exist_exception(self): + """Test that SAC does not exist renders a unique exception.""" + factory = RequestFactory() + request = factory.get(reverse("audit:AuditeeCertification", args=["12345"])) + + with patch("audit.models.SingleAuditChecklist.objects.get") as mock_get: + mock_get.side_effect = SingleAuditChecklist.DoesNotExist + + view = AuditeeCertificationStep2View.as_view() + + with self.assertRaises(PermissionDenied) as context: + view(request, report_id="12345") + + self.assertEqual( + str(context.exception), "You do not have access to this audit." + ) + + +class CrossValidationViewTests(TestCase): + def setUp(self): + """Setup client, user, SAC, and URL""" + self.client = Client() + self.user = baker.make(User) + self.sac = baker.make( + SingleAuditChecklist, + report_id="test-report-id", + submission_status=STATUS.IN_PROGRESS, + general_information={"auditee_fiscal_period_end": "2024-12-31"}, + ) + self.url = reverse( + "audit:CrossValidation", kwargs={"report_id": self.sac.report_id} + ) + self.client.force_login(self.user) + baker.make( + "audit.Access", + sac=self.sac, + user=self.user, + role="certifying_auditee_contact", + ) + + def test_get_view_renders_correct_template(self): + """Test that GET renders cross-validation template with correct context""" + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed( + response, "audit/cross-validation/cross-validation.html" + ) + self.assertEqual(response.context["report_id"], self.sac.report_id) + self.assertEqual( + response.context["submission_status"], self.sac.submission_status + ) + + def test_get_view_permission_denied(self): + """Test that GET returns 403 if SAC report_id does not exist""" + url = reverse("audit:CrossValidation", args=["non-existent-id"]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 403) # PermissionDenied results in 403 + + @patch("audit.models.SingleAuditChecklist.validate_full") + def test_post_view_renders_results_template(self, mock_validate_full): + """Test that POST with validation errors renders template with errors""" + mock_validate_full.return_value = ["Error 1", "Error 2"] + + response = self.client.post(self.url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed( + response, "audit/cross-validation/cross-validation-results.html" + ) + self.assertEqual(response.context["report_id"], self.sac.report_id) + self.assertEqual(response.context["errors"], ["Error 1", "Error 2"]) + mock_validate_full.assert_called_once() + + def test_post_view_permission_denied(self): + """Test that POST returns 403 if SAC report_id does not exist""" + url = reverse("audit:CrossValidation", args=["non-existent-id"]) + response = self.client.post(url) + + self.assertEqual(response.status_code, 403)