""" Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de Created on: 26.10.21 """ import datetime import json from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID from ema.models import Ema from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from user.models import User, Team from django.contrib.auth.models import Group from django.contrib.gis.geos import MultiPolygon, Polygon from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase, Client from django.urls import reverse from codelist.models import KonovaCode, KonovaCodeList from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction from intervention.models import Legal, Responsibility, Intervention, Handler from konova.management.commands.setup_data import GROUPS_DATA from konova.models import Geometry, Deadline, DeadlineType from konova.settings import DEFAULT_GROUP from konova.utils.generators import generate_random_string from user.models import UserActionLogEntry class BaseTestCase(TestCase): """ Provides reusable functionality for specialized test cases """ users = None groups = None superuser = None user = None intervention = None compensation = None eco_account = None comp_state = None comp_action = None finished_deadline = None codes = None superuser_pw = "root" user_pw = "root" class Meta: abstract = True def setUp(self) -> None: """ Setup data before each test run Returns: """ super().setUp() self.create_users() self.create_groups() self.handler = self.create_dummy_handler() self.intervention = self.create_dummy_intervention() self.compensation = self.create_dummy_compensation() self.eco_account = self.create_dummy_eco_account() self.ema = self.create_dummy_ema() self.deduction = self.create_dummy_deduction() self.create_dummy_states() self.create_dummy_action() self.codes = self.create_dummy_codes() self.team = self.create_dummy_team() self.finished_deadline = self.create_dummy_deadline() # Set the default group as only group for the user default_group = self.groups.get(name=DEFAULT_GROUP) self.superuser.groups.set([default_group]) # Create fresh logged in client and a non-logged in client (anon) for each test self.client_user = Client() self.client_user.login(username=self.superuser.username, password=self.superuser_pw) self.client_anon = Client() def create_users(self): # Create superuser and regular user self.superuser = User.objects.create_superuser( username="root", email="root@root.com", password=self.superuser_pw, ) self.user = User.objects.create_user( username="user1", email="user@root.com", password=self.user_pw ) self.users = User.objects.all() def create_groups(self): # Create groups for group_data in GROUPS_DATA: name = group_data.get("name") Group.objects.get_or_create( name=name, ) self.groups = Group.objects.all() @staticmethod def create_dummy_string(prefix: str = ""): """ Create Returns: """ return f"{prefix}{generate_random_string(3, True)}" def create_dummy_document(self, DocumentModel, instance): """ Creates a document db entry which can be used for tests """ doc = DocumentModel.objects.create( title="TEST_doc", comment="", file=None, date_of_creation="1970-01-01", instance=instance, ) return doc def create_dummy_intervention(self): """ Creates an intervention which can be used for tests Returns: """ # Create dummy data # Create log entry action = UserActionLogEntry.get_created_action(self.superuser) # Create legal data object (without M2M laws first) legal_data = Legal.objects.create() # Create responsible data object responsibility_data = Responsibility.objects.create( handler=self.handler ) geometry = Geometry.objects.create() # Finally create main object, holding the other objects intervention = Intervention.objects.create( title="Test_title", responsible=responsibility_data, legal=legal_data, created=action, geometry=geometry, comment="Test", ) intervention.generate_access_token(make_unique=True) return intervention def create_dummy_compensation(self, interv: Intervention=None): """ Creates a compensation which can be used for tests Returns: """ if not interv: if self.intervention is None: interv = self.create_dummy_intervention() else: interv = self.intervention # Create dummy data # Create log entry action = UserActionLogEntry.get_created_action(self.superuser) geometry = Geometry.objects.create() # Finally create main object, holding the other objects compensation = Compensation.objects.create( title="Test_title", intervention=interv, created=action, geometry=geometry, comment="Test", ) return compensation def create_dummy_eco_account(self): """ Creates an eco account which can be used for tests Returns: """ # Create dummy data # Create log entry action = UserActionLogEntry.get_created_action(self.superuser) geometry = Geometry.objects.create() # Create responsible data object lega_data = Legal.objects.create() responsible_data = Responsibility.objects.create() handler = self.handler responsible_data.handler = handler responsible_data.save() # Finally create main object, holding the other objects eco_account = EcoAccount.objects.create( title="Test_title", deductable_surface=500, legal=lega_data, responsible=responsible_data, created=action, geometry=geometry, comment="Test", ) return eco_account def create_dummy_ema(self): """ Creates an ema which can be used for tests Returns: """ # Create dummy data # Create log entry action = UserActionLogEntry.get_created_action(self.superuser) geometry = Geometry.objects.create() # Create responsible data object responsible_data = Responsibility.objects.create() responsible_data.handler = self.handler responsible_data.save() # Finally create main object, holding the other objects ema = Ema.objects.create( title="Test_title", responsible=responsible_data, created=action, geometry=geometry, comment="Test", ) return ema def create_dummy_deduction(self, acc: EcoAccount = None, interv: Intervention = None): if not acc: acc = self.create_dummy_eco_account() if not interv: interv = self.create_dummy_intervention() return EcoAccountDeduction.objects.create( account=acc, intervention=interv, surface=100, ) def create_dummy_states(self): """ Creates an intervention which can be used for tests Returns: """ self.comp_state = CompensationState.objects.create( surface=10.00, biotope_type=None, ) return self.comp_state def create_dummy_action(self): """ Creates an intervention which can be used for tests Returns: """ self.comp_action = CompensationAction.objects.create( amount=10 ) return self.comp_action def create_dummy_codes(self): """ Creates some dummy KonovaCodes which can be used for testing Returns: """ codes = KonovaCode.objects.all() if codes.count() == 0: codes = KonovaCode.objects.bulk_create([ KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"), KonovaCode(id=2, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test2"), KonovaCode(id=3, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test3"), KonovaCode(id=4, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test4"), ]) return codes def create_dummy_team(self, name: str = None): """ Creates a dummy team Returns: """ if self.superuser is None: self.create_users() if not name: name = "Testteam" team = Team.objects.get_or_create( name=name, description="Testdescription", )[0] team.users.add(self.superuser) return team def create_dummy_deadline(self, type: DeadlineType = DeadlineType.FINISHED): """ Creates a dummy deadline. If type is not specified, it defaults to DeadlineType.FINISHED Returns: deadline (Deadline): A deadline """ deadline = Deadline.objects.create( type=type, date="1970-01-01" ) return deadline @staticmethod def create_dummy_geometry() -> MultiPolygon: """ Creates some geometry Returns: """ polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874)) polygon.srid = 4326 polygon.transform(DEFAULT_SRID_RLP) return MultiPolygon(polygon, srid=DEFAULT_SRID_RLP) def create_geojson(self, geometry): """ Creates a default structure including geojson from a geometry Args: geometry (): Returns: """ geom_json = { "features": [ { "type": "Feature", "geometry": json.loads(geometry.geojson), } ] } geom_json = json.dumps(geom_json) return geom_json def create_dummy_handler(self) -> Handler: """ Creates a Handler Returns: """ handler = Handler.objects.get_or_create( type=KonovaCode.objects.all().first(), detail="Test handler" )[0] return handler def fill_out_intervention(self, intervention: Intervention) -> Intervention: """ Adds all required (dummy) data to an intervention Args: intervention (Intervention): The intervention which shall be filled out Returns: intervention (Intervention): The modified intervention """ intervention.responsible.registration_office = KonovaCode.objects.get(id=1) intervention.responsible.conservation_office = KonovaCode.objects.get(id=2) intervention.responsible.registration_file_number = "test" intervention.responsible.conservation_file_number = "test" intervention.responsible.handler = self.handler intervention.responsible.save() intervention.legal.registration_date = datetime.date.fromisoformat("1970-01-01") intervention.legal.binding_date = datetime.date.fromisoformat("1970-01-01") intervention.legal.process_type = KonovaCode.objects.get(id=3) intervention.legal.save() intervention.legal.laws.set([KonovaCode.objects.get(id=(4))]) intervention.geometry.geom = self.create_dummy_geometry() intervention.geometry.save() intervention.save() return intervention def fill_out_compensation(self, compensation: Compensation) -> Compensation: """ Adds all required (dummy) data to a compensation Args: compensation (Compensation): The compensation which shall be filled out Returns: compensation (Compensation): The modified compensation """ compensation.after_states.add(self.comp_state) compensation.before_states.add(self.comp_state) compensation.actions.add(self.comp_action) compensation.geometry.geom = self.create_dummy_geometry() compensation.deadlines.add(self.finished_deadline) compensation.geometry.save() return compensation def get_conservation_office_code(self): """ Returns a dummy KonovaCode as conservation office code Returns: """ codelist = KonovaCodeList.objects.get_or_create( id=CODELIST_CONSERVATION_OFFICE_ID )[0] code = KonovaCode.objects.get(id=2) codelist.codes.add(code) return code def get_registration_office_code(self): """ Returns a dummy KonovaCode as conservation office code Returns: """ codelist = KonovaCodeList.objects.get_or_create( id=CODELIST_REGISTRATION_OFFICE_ID )[0] code = KonovaCode.objects.get(id=3) codelist.codes.add(code) return code def fill_out_ema(self, ema): """ Adds all required (dummy) data to an Ema Returns: """ ema.responsible.conservation_office = self.get_conservation_office_code() ema.responsible.conservation_file_number = "test" ema.responsible.handler = self.handler ema.responsible.save() ema.after_states.add(self.comp_state) ema.before_states.add(self.comp_state) ema.actions.add(self.comp_action) ema.geometry.geom = self.create_dummy_geometry() ema.deadlines.add(self.finished_deadline) ema.geometry.save() return ema def fill_out_eco_account(self, eco_account): """ Adds all required (dummy) data to an EcoAccount Returns: """ eco_account.legal.registration_date = "2022-01-01" eco_account.legal.save() eco_account.responsible.conservation_office = self.get_conservation_office_code() eco_account.responsible.conservation_file_number = "test" eco_account.responsible.handler = self.handler eco_account.responsible.save() eco_account.after_states.add(self.comp_state) eco_account.before_states.add(self.comp_state) eco_account.actions.add(self.comp_action) eco_account.geometry.geom = self.create_dummy_geometry() eco_account.geometry.save() eco_account.deductable_surface = eco_account.get_surface_after_states() eco_account.deadlines.add(self.finished_deadline) eco_account.save() return eco_account def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001): """ Assert for geometries to be equal Transforms the geometries to matching srids before checking Args: geom1 (MultiPolygon): A geometry geom2 (MultiPolygon): A geometry Returns: """ # Two empty geometries are basically identical - no further testing if geom1.empty and geom2.empty: self.assertTrue(True) return if geom1.srid != geom2.srid: # Due to prior possible transformation of any of these geometries, we need to make sure there exists a # transformation from one coordinate system into the other, which is valid geom1.transform(geom2.srid) geom2.transform(geom1.srid) self.assertTrue(geom1.equals_exact(geom2, tolerance) or geom2.equals_exact(geom1, tolerance)) class BaseViewTestCase(BaseTestCase): """ Wraps basic test functionality, reusable for every specialized ViewTestCase """ login_url = None class Meta: abstract = True @classmethod def setUpTestData(cls) -> None: super().setUpTestData() def setUp(self) -> None: super().setUp() self.login_url = reverse("oauth-login") def assert_url_success(self, client: Client, urls: list): """ Assert for all given urls a direct 200 response Args: client (Client): The performing client urls (list): An iterable list of urls to be checked Returns: """ for url in urls: response = client.get(url) self.assertEqual(response.status_code, 200, msg=f"Failed for {url}") def assert_url_success_redirect(self, client: Client, urls: dict): """ Assert for all given urls a 302 response to a certain location. Assert the redirect being the expected behaviour. Args: client (Client): The performing client urls (dict): An iterable dict of (urls, redirect_to_url) pairs to be checked Returns: """ for url, redirect_to in urls.items(): response = client.get(url, follow=True) # Expect redirects to the landing page self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}") def assert_url_fail(self, client: Client, urls: list): """ Assert for all given urls a direct 302 response Args: client (Client): The performing client urls (list): An iterable list of urls to be checked Returns: """ for url in urls: response = client.get(url) self.assertEqual(response.status_code, 302, msg=f"Failed for {url}") class KonovaViewTestCase(BaseViewTestCase): """ Holds tests for all regular views, which are not app specific """ def setUp(self) -> None: super().setUp() geom = self.create_dummy_geometry() self.geom_1 = Geometry.objects.create( geom=geom, ) self.home_url = reverse("home") def test_views_logged_in_no_groups(self): """ Check correct status code for all requests Assumption: User logged in but has no groups Returns: """ # User logged in client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) success_urls = [ self.home_url ] self.assert_url_success(client, success_urls) def test_views_anonymous_user(self): """ Check correct status code for all requests Assumption: User logged in but has no groups Returns: """ # User not logged in client = Client() urls = [ self.home_url ] self.assert_url_fail(client, urls) def test_htmx_parcel_fetch(self): """ Tests that the htmx geometry-parcel fetch returns a proper status code and content Returns: """ client_user = Client() client_user.login(username=self.superuser.username, password=self.superuser_pw) has_parcels = self.geom_1.parcels.all().exists() if not has_parcels: self.geom_1.update_parcels() htmx_url = reverse("geometry-parcels", args=(self.geom_1.id,)) response = client_user.get(htmx_url) self.assertEqual(response.status_code, 286, "Unexpected status code for HTMX fetch") self.assertGreater(len(response.content), 0) class AutocompleteTestCase(BaseViewTestCase): @classmethod def setUpTestData(cls) -> None: super().setUpTestData() cls.atcmplt_accs = reverse("compensation:acc:autocomplete") cls.atcmplt_interventions = reverse("intervention:autocomplete") cls.atcmplt_code_comp_action = reverse("codelist:compensation-action-autocomplete") cls.atcmplt_code_comp_biotope = reverse("codelist:biotope-autocomplete") cls.atcmplt_code_comp_law = reverse("codelist:law-autocomplete") cls.atcmplt_code_comp_process = reverse("codelist:process-type-autocomplete") cls.atcmplt_code_comp_reg_off = reverse("codelist:registration-office-autocomplete") cls.atcmplt_code_comp_cons_off = reverse("codelist:conservation-office-autocomplete") cls.atcmplt_code_share_user = reverse("user:share-user-autocomplete") def _test_views_anonymous_user(self): # ATTENTION: As of the current state of django-autocomplete-light, there is no way to check on authenticated # users in a way like @loing_required or anything else. The documentation considers to check on the user's # authentication state during get_queryset() of the call. Therefore this test method here will stay here # for future clarification but won't be run due to the prefix '_' # User not logged in client = Client() urls = [ self.atcmplt_accs, self.atcmplt_interventions, self.atcmplt_code_comp_action, self.atcmplt_code_comp_biotope, self.atcmplt_code_comp_law, self.atcmplt_code_comp_process, self.atcmplt_code_comp_reg_off, self.atcmplt_code_comp_cons_off, self.atcmplt_code_share_user, ] self.assert_url_fail(client, urls) def test_views_logged_in_no_groups(self): # User logged in client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) self.superuser.groups.set([]) urls = [ self.atcmplt_accs, self.atcmplt_interventions, self.atcmplt_code_comp_action, self.atcmplt_code_comp_biotope, self.atcmplt_code_comp_law, self.atcmplt_code_comp_process, self.atcmplt_code_comp_reg_off, self.atcmplt_code_comp_cons_off, ] self.assert_url_success(client, urls) class BaseWorkflowTestCase(BaseTestCase): """ Holds base methods and attributes for workflow testing """ client_user = None client_anon = None class Meta: abstract = True @classmethod def setUpTestData(cls): super().setUpTestData() def assert_object_is_deleted(self, obj): """ Provides a quick check whether an object has been removed from the database or not Args: obj (): Returns: """ # Expect the object to be gone from the db try: obj.refresh_from_db() # Well, we should not reach this next line of code, since the object should be gone, therefore not # refreshable -> fail! self.fail() except ObjectDoesNotExist: # If we get in here, the test was fine pass