""" Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de Created on: 10.11.21 """ import datetime from django.core.exceptions import ObjectDoesNotExist from django.urls import reverse from compensation.models import Payment, EcoAccountDeduction from intervention.models import Intervention from konova.settings import ETS_GROUP, ZB_GROUP from konova.tests.test_views import BaseWorkflowTestCase from user.models import UserActionLogEntry, UserAction class InterventionWorkflowTestCase(BaseWorkflowTestCase): """ This test case adds workflow tests """ @classmethod def setUpTestData(cls): super().setUpTestData() def setUp(self) -> None: super().setUp() # Recreate a new (bare minimum) intervention before each test self.intervention = self.create_dummy_intervention() self.intervention.share_with(self.superuser) def test_new(self): """ Checks a 'normal' case of creating a new intervention. We expect the user to be redirected as expected right away to the detail page of the new intervention. We expect the user to be directly added to the shared user of the intervention We expect that a minimum of data (identifier, title, (empty) geometry) can be used to create an intervention Returns: """ # Define the intervention identifier for easier handling on the next lines test_id = self.create_dummy_string() test_title = self.create_dummy_string() test_geom = self.create_dummy_geometry() new_url = reverse("intervention:new", args=()) # Expect the new intervention does not exist yet obj_exists = Intervention.objects.filter( identifier=test_id ).exists() self.assertFalse(obj_exists) # User creates a new intervention with bare minimum content, using the proper url and post data post_data = { "identifier": test_id, "title": test_title, "geom": test_geom.geojson, } response = self.client_user.post( new_url, post_data ) # Now expect the new intervention to exist in the db try: obj = Intervention.objects.get( identifier=test_id ) self.assertEqual(obj.identifier, test_id) self.assertEqual(obj.title, test_title) self.assert_equal_geometries(obj.geometry.geom, test_geom) except ObjectDoesNotExist: # Fail if there is no such object self.fail() expected_redirect = reverse("intervention:detail", args=(obj.id,)) # Expect redirect to the detail view of the new intervention self.assertRedirects(response, expected_redirect) # Expect user to be first and only user with shared access self.assertIn(self.superuser, obj.users.all()) self.assertEqual(1, obj.users.count()) def test_checkability(self): """ Tests that the intervention can only be checked if all required data has been added Returns: """ check_url = reverse("intervention:check", args=(self.intervention.id,)) post_data = { "checked_intervention": True, "checked_comps": True, } # First of all, the intervention should not be checked, yet if self.intervention.checked: self.intervention.checked.delete() self.intervention.refresh_from_db() # Make sure the dummy compensation is currently not linked to the intervention, # since the system would check on it's quality as well (and it would fail) self.intervention.compensations.set([]) # Run request with an incomplete intervention and missing user privileges --> expect to fail self.client_user.post(check_url, post_data) # We expect that the intervention is still not checked now self.intervention.refresh_from_db() self.assertIsNone(self.intervention.checked) # Now give the user the required privileges by adding to the registration office group group = self.groups.get(name=ZB_GROUP) self.superuser.groups.add(group) # Now fill in the missing data, so the intervention is 'valid' for checking self.intervention = self.fill_out_intervention(self.intervention) # Then add a dummy payment, so we pass the quality check (Checks whether any kind of valid compensation exists) payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test") self.intervention.payments.add(payment) # Run request again self.client_user.post(check_url, post_data) # Update intervention from db self.intervention.refresh_from_db() # We expect the intervention to be checked now and contains the proper data # Attention: We check the timestamp only on the date, not the time, since the microseconds delay would result # in an unwanted assertion error checked = self.intervention.checked self.assertIsNotNone(checked) self.assertEqual(self.superuser, checked.user) self.assertEqual(datetime.date.today(), checked.timestamp.date()) self.assertEqual(UserAction.CHECKED, checked.action) # Expect the user action now to live in the log self.assertIn(checked, self.intervention.log.all()) def test_recordability(self): """ Tests that the intervention can only be recorded if all required data has been added Returns: """ record_url = reverse("intervention:record", args=(self.intervention.id,)) post_data = { "confirm": True, } # Make sure the dummy compensation is currently not linked to the intervention, # since we would check on it's quality as well then self.intervention.compensations.set([]) # First of all, the intervention should not be recorded, yet if self.intervention.recorded: self.intervention.recorded.delete() self.intervention.refresh_from_db() # Run request with an incomplete intervention and missing user privileges --> expect to fail self.client_user.post(record_url, post_data) # We expect that the intervention is still not recorded now self.intervention.refresh_from_db() self.assertIsNone(self.intervention.recorded) # Now give the user the required privileges by adding to the ETS group group = self.groups.get(name=ETS_GROUP) self.superuser.groups.add(group) # Now fill in the missing data, so the intervention is 'valid' for recording self.intervention = self.fill_out_intervention(self.intervention) # Then add a dummy payment, so we pass the quality check (Checks whether any kind of valid compensation exists) payment = Payment.objects.create(amount=10.00, due_on=None, comment="No due date because test") self.intervention.payments.add(payment) # Run request again self.client_user.post(record_url, post_data) # Update intervention from db self.intervention.refresh_from_db() # We expect the intervention to be recorded now and contains the proper data # Attention: We check the timestamp only on the date, not the time, since the microseconds delay would result # in an unwanted assertion error self.assertIsNotNone(self.intervention.recorded) self.assertEqual(self.superuser, self.intervention.recorded.user) self.assertEqual(datetime.date.today(), self.intervention.recorded.timestamp.date()) self.assertEqual(UserAction.RECORDED, self.intervention.recorded.action) # Expect the user action now to live in the log self.assertIn(self.intervention.recorded, self.intervention.log.all()) def subtest_add_payment(self): """ Subroutine for 'normal' payment tests Checks a 'normal' case of adding a payment. We expect a new payment to be addable to an existing intervention Returns: """ # Attention: Despite the fact, this url refers to a compensation app route, we test it here for the interventions. # Reason: A payment is some kind of compensation for an intervention. Therefore it lives inside the compensation app. # BUT: Payments are added on the intervention detail page. Therefore it's part of a regular intervention workflow. new_payment_url = reverse("compensation:pay-new", args=(self.intervention.id,)) # Make sure there are no payments on the intervention, yet self.assertEqual(0, self.intervention.payments.count()) # Create form data to be sent to the url test_amount = 10.00 test_due = "2021-01-01" test_comment = self.create_dummy_string() post_data = { "amount": test_amount, "due": test_due, "comment": test_comment } self.client_user.post( new_payment_url, post_data, ) # We do not test for any redirects in here, since the new payment url is realized using a modal, which does not # perform any direct redirects but instead reloads the page after finisihing. # Make sure there is a new payment on the intervention now self.assertEqual(1, self.intervention.payments.count()) # Make sure the payment contains our data payment = self.intervention.payments.all()[0] self.assertEqual(payment.amount, test_amount) self.assertEqual(payment.due_on, datetime.date.fromisoformat(test_due)) self.assertEqual(payment.comment, test_comment) return payment def subtest_delete_payment(self, payment: Payment): """ Subroutine for 'normal' payment tests Checks a 'normal' case of adding a payment. We expect a payment to be deletable to an existing intervention Returns: """ # Create removing url for the payment remove_url = reverse("compensation:pay-remove", args=(payment.id,)) post_data = { "confirm": True, } self.client_user.post( remove_url, post_data ) # Expect the payment to be gone from the db and therefore from the intervention as well self.assert_object_is_deleted(payment) # Now make sure the intervention has no payments anymore self.assertEqual(0, self.intervention.payments.count()) def test_payments(self): """ Checks a 'normal' case of adding a payment. We expect a new payment to be addable to an existing intervention We expect a payment to be deletable from an existing intervention Returns: """ # Create new payment for the default intervention payment = self.subtest_add_payment() # Now remove the payment again self.subtest_delete_payment(payment) def subtest_add_deduction_fail_positive(self, new_url: str, post_data: dict, test_surface: float): """ Holds tests for postivie fails of new deduction creation Reasons for failing are: * EcoAccount does not provide enough 'deductable_surface' * EcoAccount is not recorded (not "approved"), yet * EcoAccount is not shared with performing user Args: new_url (str): The url to send the post data to post_data (dict): The form post data to be sent Returns: """ # Before running fail positive tests, we need to have an account in a (normally) fine working state self.assertIsNotNone(self.eco_account.recorded) # -> is recorded self.assertGreater(self.eco_account.deductable_surface, test_surface) # -> has more deductable surface than we need self.assertIn(self.superuser, self.eco_account.users.all()) # -> is shared with the performing user # Count the number of already existing deductions in total and for the account for later comparison num_deductions = self.eco_account.deductions.count() num_deductions_total = EcoAccountDeduction.objects.count() # First test that a deduction can not be created, if the account does not provide # enough surface for the deduction. So we modify the deductable surface of the account self.eco_account.deductable_surface = 0 self.eco_account.save() # Now perform the (expected) failing request self.client_user.post(new_url, post_data) # Expect no changes at all, since the deduction should not have been created self.assertEqual(num_deductions, self.eco_account.deductions.count()) self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count()) # Now restore the deductable surface to a valid size back again but remove the user from the shared list self.eco_account.deductable_surface = test_surface + 100.00 self.eco_account.share_with_list([]) self.eco_account.save() # Now perform the (expected) failing request (again) self.client_user.post(new_url, post_data) # Expect no changes at all, since the account is not shared self.assertEqual(num_deductions, self.eco_account.deductions.count()) self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count()) # Restore the sharing but remove the recording state self.eco_account.share_with_list([self.superuser]) self.eco_account.recorded.delete() self.eco_account.refresh_from_db() self.eco_account.save() # Now perform the (expected) failing request (again) self.client_user.post(new_url, post_data) # Expect no changes at all, since the account is no shared with the user, yet self.assertEqual(num_deductions, self.eco_account.deductions.count()) self.assertEqual(num_deductions_total, EcoAccountDeduction.objects.count()) def subtest_add_deduction_normal(self, new_url: str, post_data: dict, test_surface: float): """ Holds tests on working ("normal") deduction creation Args: new_url (str): The url to send the post data to post_data (dict): The form post data to be sent test_surface (float): The expected surface of the deduction Returns: """ # Prepare the account for a working situation (enough deductable surface, recorded and shared) self.eco_account.deductable_surface = 10000.00 if self.eco_account.recorded is None: rec_action = UserActionLogEntry.objects.create( user=self.superuser, action=UserAction.RECORDED ) self.eco_account.recorded = rec_action self.eco_account.share_with_list([self.superuser]) self.eco_account.save() # Run the request self.client_user.post(new_url, post_data) # Expect the deduction to be created, since all constraints are fulfilled self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, EcoAccountDeduction.objects.count()) # Make sure the deduction contains the expected data deduction = EcoAccountDeduction.objects.first() self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.intervention, self.intervention) self.assertEqual(deduction.account, self.eco_account) # Return deduction for further usage in tests return deduction def subtest_add_deduction(self): """ Holds test for adding a new deduction Contains tests for * positive fails (as expected) * normal cases Returns: """ # Create the url for creating a new deduction new_url = reverse("compensation:acc-new-deduction", args=(self.eco_account.id,)) # Prepare the form data test_surface = 100.00 post_data = { "surface": test_surface, "account": self.eco_account.id, "intervention": self.intervention.id, } # Run some tests for regular, working cases deduction = self.subtest_add_deduction_normal(new_url, post_data, test_surface) # Run some tests where we expect the creation of a deduction to fail (as expected) self.subtest_add_deduction_fail_positive(new_url, post_data, test_surface) # Return deduction for further usage in tests return deduction def subtest_delete_deduction(self, deduction: EcoAccountDeduction): """ Holds test for deleting a deduction Returns: """ # Prepare url for deleting of this deduction delete_url = reverse("compensation:acc-remove-deduction", args=(self.eco_account.id, deduction.id,)) post_data = { "confirm": True } # Save number of current deductions for later comparison num_deductions = self.eco_account.deductions.count() num_deductions_total = EcoAccountDeduction.objects.count() # Run request self.client_user.post(delete_url, post_data) # Expect the deduction to be gone from the db and relations self.assertEqual(num_deductions - 1, self.eco_account.deductions.count()) self.assertEqual(num_deductions_total - 1, EcoAccountDeduction.objects.count()) # Expect the deduction to be totally gone self.assert_object_is_deleted(deduction) def test_deduction(self): """ Checks a 'normal case of adding a deduction. We expect a new deduction to be addable to an existing intervention We expect a deduction to be deletable Returns: """ # Create a new deduction for the default intervention deduction = self.subtest_add_deduction() # Now remove the deduction again self.subtest_delete_deduction(deduction)