From 30e5239c49d1fb913db5a8df9e931dd4cd57abba Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 27 Oct 2021 14:44:49 +0200 Subject: [PATCH] #19 Tests * adds tests for compensations (WIP) * refactors some dummy data generating into base test class * fixes bugs detected by testing * adds important requirements.txt change for itsdangerous package (<1.0.0 for compatibility to django-simple-sso) --- compensation/models.py | 13 ++ compensation/tests.py | 3 - compensation/tests/__init__.py | 0 compensation/tests/test_views.py | 202 +++++++++++++++++++++++ compensation/views/compensation_views.py | 17 ++ intervention/tests/test_views.py | 167 ++++++++++++++----- intervention/views.py | 9 + konova/models.py | 2 +- konova/tests/test_views.py | 63 +++++++ requirements.txt | 2 +- 10 files changed, 435 insertions(+), 43 deletions(-) delete mode 100644 compensation/tests.py create mode 100644 compensation/tests/__init__.py create mode 100644 compensation/tests/test_views.py diff --git a/compensation/models.py b/compensation/models.py index b355a428..e7d62dde 100644 --- a/compensation/models.py +++ b/compensation/models.py @@ -229,6 +229,19 @@ class Compensation(AbstractCompensation): self.identifier = self.generate_new_identifier() super().save(*args, **kwargs) + def is_shared_with(self, user: User): + """ Access check + + Checks whether a given user has access to this object + + Args: + user (): + + Returns: + + """ + return self.intervention.users.filter(id=user.id) + def get_LANIS_link(self) -> str: """ Generates a link for LANIS depending on the geometry diff --git a/compensation/tests.py b/compensation/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/compensation/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/compensation/tests/__init__.py b/compensation/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/compensation/tests/test_views.py b/compensation/tests/test_views.py new file mode 100644 index 00000000..92840281 --- /dev/null +++ b/compensation/tests/test_views.py @@ -0,0 +1,202 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 27.10.21 + +""" +from django.urls import reverse +from django.test import Client + +from compensation.models import CompensationState, CompensationAction +from konova.settings import DEFAULT_GROUP +from konova.tests.test_views import BaseViewTestCase + + +class ViewTestCase(BaseViewTestCase): + comp_state = None + comp_action = None + + def setUp(self) -> None: + super().setUp() + self.create_dummy_states() + self.create_dummy_action() + + # Prepare urls + self.index_url = reverse("compensation:index", args=()) + self.new_url = reverse("compensation:new", args=(self.intervention.id,)) + self.new_id_url = reverse("compensation:new-id", args=()) + self.detail_url = reverse("compensation:detail", args=(self.compensation.id,)) + self.log_url = reverse("compensation:log", args=(self.compensation.id,)) + self.edit_url = reverse("compensation:edit", args=(self.compensation.id,)) + self.remove_url = reverse("compensation:remove", args=(self.compensation.id,)) + self.report_url = reverse("compensation:report", args=(self.compensation.id,)) + self.state_new_url = reverse("compensation:new-state", args=(self.compensation.id,)) + self.action_new_url = reverse("compensation:new-action", args=(self.compensation.id,)) + self.deadline_new_url = reverse("compensation:new-deadline", args=(self.compensation.id,)) + self.new_doc_url = reverse("compensation:new-doc", args=(self.compensation.id,)) + + self.state_remove_url = reverse("compensation:state-remove", args=(self.comp_state.id,)) + self.action_remove_url = reverse("compensation:action-remove", args=(self.comp_action.id,)) + + 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, + ) + self.compensation.before_states.set([self.comp_state]) + self.compensation.after_states.set([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 + ) + self.compensation.actions.set([self.comp_action]) + + def test_logged_in_no_groups_shared(self): + """ Check correct status code for all requests + + Assumption: User logged in and has no groups and data is shared + + Returns: + + """ + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + self.superuser.groups.set([]) + self.intervention.users.set([self.superuser]) + + # Since the user has no groups, it does not matter that data has been shared. There SHOULD not be any difference + # to a user without access, since the important permissions are missing + success_urls = [ + self.index_url, + self.detail_url, + self.report_url, + ] + fail_urls = [ + self.new_url, + self.new_id_url, + self.log_url, + self.edit_url, + self.remove_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + ] + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + + def test_logged_in_no_groups_unshared(self): + """ Check correct status code for all requests + + Assumption: User logged in and has no groups and data is shared + + Returns: + + """ + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + self.superuser.groups.set([]) + self.intervention.users.set([]) + + # Since the user has no groups, it does not matter that data is unshared. There SHOULD not be any difference + # to a user having shared access, since all important permissions are missing + success_urls = [ + self.index_url, + self.detail_url, + self.report_url, + ] + fail_urls = [ + self.new_url, + self.new_id_url, + self.log_url, + self.edit_url, + self.remove_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + ] + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + + def test_logged_in_default_group_shared(self): + """ Check correct status code for all requests + + Assumption: User logged in and has no groups and data is shared + + Returns: + + """ + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + group = self.groups.get(name=DEFAULT_GROUP) + self.superuser.groups.set([group]) + self.intervention.users.set([self.superuser]) + + success_urls = [ + self.index_url, + self.detail_url, + self.report_url, + self.new_url, + self.new_id_url, + self.edit_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + self.log_url, + self.remove_url, + ] + self.assert_url_success(client, success_urls) + + def test_anonymous_user(self): + """ Check correct status code for all requests + + Assumption: User not logged in + + Returns: + + """ + client = Client() + + success_urls = [ + self.report_url, + ] + fail_urls = [ + self.index_url, + self.detail_url, + self.new_url, + self.new_id_url, + self.log_url, + self.edit_url, + self.remove_url, + self.state_new_url, + self.action_new_url, + self.deadline_new_url, + self.state_remove_url, + self.action_remove_url, + self.new_doc_url, + ] + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation_views.py index c7221570..3175c38a 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation_views.py @@ -90,6 +90,7 @@ def new_view(request: HttpRequest, intervention_id: str = None): @login_required +@default_group_required def new_id_view(request: HttpRequest): """ JSON endpoint @@ -196,6 +197,8 @@ def detail_view(request: HttpRequest, id: str): @login_required +@default_group_required +@shared_access_required(Compensation, "id") def log_view(request: HttpRequest, id: str): """ Renders a log view using modal @@ -220,6 +223,8 @@ def log_view(request: HttpRequest, id: str): @login_required +@default_group_required +@shared_access_required(Compensation, "id") def remove_view(request: HttpRequest, id: str): """ Renders a modal view for removing the compensation @@ -240,6 +245,8 @@ def remove_view(request: HttpRequest, id: str): @login_required +@default_group_required +@shared_access_required(Compensation, "id") def new_document_view(request: HttpRequest, id: str): """ Renders a form for uploading new documents @@ -258,6 +265,7 @@ def new_document_view(request: HttpRequest, id: str): @login_required +@default_group_required def get_document_view(request: HttpRequest, doc_id: str): """ Returns the document as downloadable file @@ -284,6 +292,7 @@ def get_document_view(request: HttpRequest, doc_id: str): @login_required +@default_group_required def remove_document_view(request: HttpRequest, doc_id: str): """ Removes the document from the database and file system @@ -304,6 +313,8 @@ def remove_document_view(request: HttpRequest, doc_id: str): @login_required +@default_group_required +@shared_access_required(Compensation, "id") def state_new_view(request: HttpRequest, id: str): """ Renders a form for adding new states for a compensation @@ -323,6 +334,8 @@ def state_new_view(request: HttpRequest, id: str): @login_required +@default_group_required +@shared_access_required(Compensation, "id") def action_new_view(request: HttpRequest, id: str): """ Renders a form for adding new actions for a compensation @@ -342,6 +355,8 @@ def action_new_view(request: HttpRequest, id: str): @login_required +@default_group_required +@shared_access_required(Compensation, "id") def deadline_new_view(request: HttpRequest, id: str): """ Renders a form for adding new states for a compensation @@ -361,6 +376,7 @@ def deadline_new_view(request: HttpRequest, id: str): @login_required +@default_group_required def state_remove_view(request: HttpRequest, id: str): """ Renders a form for removing a compensation state @@ -380,6 +396,7 @@ def state_remove_view(request: HttpRequest, id: str): @login_required +@default_group_required def action_remove_view(request: HttpRequest, id: str): """ Renders a form for removing a compensation action diff --git a/intervention/tests/test_views.py b/intervention/tests/test_views.py index 111b2f4c..ff231e1e 100644 --- a/intervention/tests/test_views.py +++ b/intervention/tests/test_views.py @@ -18,11 +18,8 @@ from user.models import UserActionLogEntry, UserAction class ViewTestCase(BaseViewTestCase): - intervention = None - def setUp(self) -> None: super().setUp() - self.create_dummy_data() # Prepare urls self.index_url = reverse("intervention:index", args=()) @@ -37,35 +34,6 @@ class ViewTestCase(BaseViewTestCase): self.run_check_url = reverse("intervention:run-check", args=(self.intervention.id,)) self.record_url = reverse("intervention:record", args=(self.intervention.id,)) self.report_url = reverse("intervention:report", args=(self.intervention.id,)) - - def create_dummy_data(self): - """ Creates an intervention which can be used for tests - - Returns: - - """ - # Create dummy data - # Create log entry - action = UserActionLogEntry.objects.create( - user=self.superuser, - action=UserAction.CREATED, - ) - # Create legal data object (without M2M laws first) - legal_data = LegalData.objects.create() - # Create responsible data object - responsibility_data = ResponsibilityData.objects.create() - geometry = Geometry.objects.create() - # Finally create main object, holding the other objects - self.intervention = Intervention.objects.create( - identifier="TEST", - title="Test_title", - responsible=responsibility_data, - legal=legal_data, - created=action, - geometry=geometry, - comment="Test", - ) - self.intervention.generate_access_token(make_unique=True) def test_views_logged_in_no_groups(self): """ Check correct status code for all requests @@ -78,7 +46,7 @@ class ViewTestCase(BaseViewTestCase): # Login client client = Client() client.login(username=self.superuser.username, password=self.superuser_pw) - + self.superuser.groups.set([]) success_urls = [ self.index_url, self.report_url, @@ -134,7 +102,7 @@ class ViewTestCase(BaseViewTestCase): response = client.get(url, follow=True) self.assertEqual(response.redirect_chain[0], (f"{self.login_url}?next={url}", 302), msg=f"Failed for {url}. Redirect chain is {response.redirect_chain}") - def test_views_logged_in_default_group(self): + def test_views_logged_in_default_group_shared(self): """ Check correct status code for all requests Assumption: User logged in and is default group member @@ -149,6 +117,7 @@ class ViewTestCase(BaseViewTestCase): # Add user to default group default_group = Group.objects.get(name=DEFAULT_GROUP) self.superuser.groups.set([default_group]) + self.intervention.users.set([self.superuser]) success_urls = [ self.index_url, @@ -173,10 +142,50 @@ class ViewTestCase(BaseViewTestCase): self.assert_url_fail(client, fail_urls) self.assert_url_success_redirect(client, success_urls_redirect) - def test_views_logged_in_zb_group(self): + def test_views_logged_in_default_group_unshared(self): """ Check correct status code for all requests - Assumption: User logged in and is registration office member + Assumption: User logged in and is default group member but data is not shared with + + Returns: + + """ + # Login client + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + + # Add user to default group + default_group = Group.objects.get(name=DEFAULT_GROUP) + self.superuser.groups.set([default_group]) + self.intervention.users.set([]) + + success_urls = [ + self.index_url, + self.report_url, + self.detail_url, + self.new_id_url, + self.new_url, + ] + fail_urls = [ + self.run_check_url, + self.record_url, + self.edit_url, + self.remove_url, + self.share_create_url, + self.log_url, + ] + success_urls_redirect = { + self.share_url: self.detail_url + } + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + self.assert_url_success_redirect(client, success_urls_redirect) + + def test_views_logged_in_zb_group_shared(self): + """ Check correct status code for all requests + + Assumption: User logged in and is registration office member and data is shared Returns: @@ -188,6 +197,7 @@ class ViewTestCase(BaseViewTestCase): # Add user to default group zb_group = self.groups.get(name=ZB_GROUP) self.superuser.groups.set([zb_group]) + self.intervention.users.set([self.superuser]) success_urls = [ self.index_url, @@ -212,10 +222,50 @@ class ViewTestCase(BaseViewTestCase): self.assert_url_fail(client, fail_urls) self.assert_url_success_redirect(client, success_urls_redirect) - def test_views_logged_in_ets_group(self): + def test_views_logged_in_zb_group_unshared(self): """ Check correct status code for all requests - Assumption: User logged in and is registration office member + Assumption: User logged in and is registration office member but data is not shared + + Returns: + + """ + # Login client + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + + # Add user to default group + zb_group = self.groups.get(name=ZB_GROUP) + self.superuser.groups.set([zb_group]) + self.intervention.users.set([]) + + success_urls = [ + self.index_url, + self.report_url, + self.detail_url, + ] + fail_urls = [ + self.log_url, + self.new_id_url, + self.new_url, + self.edit_url, + self.remove_url, + self.share_create_url, + self.record_url, + self.run_check_url, + ] + success_urls_redirect = { + self.share_url: self.detail_url + } + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + self.assert_url_success_redirect(client, success_urls_redirect) + + def test_views_logged_in_ets_group_shared(self): + """ Check correct status code for all requests + + Assumption: User logged in and is registration office member and data is shared with Returns: @@ -227,6 +277,7 @@ class ViewTestCase(BaseViewTestCase): # Add user to default group ets_group = Group.objects.get(name=ETS_GROUP) self.superuser.groups.set([ets_group]) + self.intervention.users.set([self.superuser]) success_urls = [ self.index_url, @@ -250,3 +301,43 @@ class ViewTestCase(BaseViewTestCase): self.assert_url_success(client, success_urls) self.assert_url_fail(client, fail_urls) self.assert_url_success_redirect(client, success_urls_redirect) + + def test_views_logged_in_ets_group_unshared(self): + """ Check correct status code for all requests + + Assumption: User logged in and is registration office member and data is not shared with + + Returns: + + """ + # Login client + client = Client() + client.login(username=self.superuser.username, password=self.superuser_pw) + + # Add user to default group + ets_group = Group.objects.get(name=ETS_GROUP) + self.superuser.groups.set([ets_group]) + self.intervention.users.set([]) + + success_urls = [ + self.index_url, + self.report_url, + self.detail_url, + ] + fail_urls = [ + self.record_url, + self.log_url, + self.new_id_url, + self.new_url, + self.edit_url, + self.remove_url, + self.share_create_url, + self.run_check_url, + ] + success_urls_redirect = { + self.share_url: self.detail_url + } + + self.assert_url_success(client, success_urls) + self.assert_url_fail(client, fail_urls) + self.assert_url_success_redirect(client, success_urls_redirect) diff --git a/intervention/views.py b/intervention/views.py index 667c4289..c04594bd 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -113,6 +113,7 @@ def new_id_view(request: HttpRequest): @login_required @default_group_required +@shared_access_required(Intervention, "id") def new_document_view(request: HttpRequest, id: str): """ Renders a form for uploading new documents @@ -264,6 +265,7 @@ def detail_view(request: HttpRequest, id: str): @login_required @default_group_required +@shared_access_required(Intervention, "id") def edit_view(request: HttpRequest, id: str): """ Renders a view for editing interventions @@ -306,6 +308,7 @@ def edit_view(request: HttpRequest, id: str): @login_required @default_group_required +@shared_access_required(Intervention, "id") def remove_view(request: HttpRequest, id: str): """ Renders a remove view for this intervention @@ -388,6 +391,7 @@ def share_view(request: HttpRequest, id: str, token: str): @login_required @default_group_required +@shared_access_required(Intervention, "id") def create_share_view(request: HttpRequest, id: str): """ Renders sharing form for an intervention @@ -408,6 +412,7 @@ def create_share_view(request: HttpRequest, id: str): @login_required @registration_office_group_required +@shared_access_required(Intervention, "id") def run_check_view(request: HttpRequest, id: str): """ Renders check form for an intervention @@ -429,6 +434,7 @@ def run_check_view(request: HttpRequest, id: str): @login_required @default_group_required +@shared_access_required(Intervention, "id") def new_revocation_view(request: HttpRequest, id: str): """ Renders sharing form for an intervention @@ -449,6 +455,7 @@ def new_revocation_view(request: HttpRequest, id: str): @login_required @default_group_required +@shared_access_required(Intervention, "id") def log_view(request: HttpRequest, id: str): """ Renders a log view using modal @@ -474,6 +481,7 @@ def log_view(request: HttpRequest, id: str): @login_required @default_group_required +@shared_access_required(Intervention, "id") def new_deduction_view(request: HttpRequest, id: str): """ Renders a modal form view for creating deductions @@ -494,6 +502,7 @@ def new_deduction_view(request: HttpRequest, id: str): @login_required @conservation_office_group_required +@shared_access_required(Intervention, "id") def record_view(request: HttpRequest, id: str): """ Renders a modal form for recording an intervention diff --git a/konova/models.py b/konova/models.py index 91ae0358..1c1fee57 100644 --- a/konova/models.py +++ b/konova/models.py @@ -149,7 +149,7 @@ class BaseObject(BaseResource): Returns: """ - if hasattr(self, "users"): + if isinstance(self, ShareableObject): return self.users.filter(id=user.id) else: return User.objects.none() diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index bdcfc5ad..62c576ed 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -11,7 +11,11 @@ from django.contrib.auth.models import User, Group from django.test import TestCase, Client from django.urls import reverse +from compensation.models import Compensation +from intervention.models import LegalData, ResponsibilityData, Intervention from konova.management.commands.setup_data import GROUPS_DATA +from konova.models import Geometry +from user.models import UserActionLogEntry, UserAction class BaseTestCase(TestCase): @@ -63,10 +67,14 @@ class BaseViewTestCase(BaseTestCase): """ login_url = None + intervention = None + compensation = None def setUp(self) -> None: self.create_users() self.create_groups() + self.create_dummy_intervention() + self.create_dummy_compensation() self.login_url = reverse("simple-sso-login") def assert_url_success(self, client: Client, urls: list): @@ -114,6 +122,61 @@ class BaseViewTestCase(BaseTestCase): response = client.get(url) self.assertEqual(response.status_code, 302, msg=f"Failed for {url}") + def create_dummy_intervention(self): + """ Creates an intervention which can be used for tests + + Returns: + + """ + # Create dummy data + # Create log entry + action = UserActionLogEntry.objects.create( + user=self.superuser, + action=UserAction.CREATED, + ) + # Create legal data object (without M2M laws first) + legal_data = LegalData.objects.create() + # Create responsible data object + responsibility_data = ResponsibilityData.objects.create() + geometry = Geometry.objects.create() + # Finally create main object, holding the other objects + self.intervention = Intervention.objects.create( + identifier="TEST", + title="Test_title", + responsible=responsibility_data, + legal=legal_data, + created=action, + geometry=geometry, + comment="Test", + ) + self.intervention.generate_access_token(make_unique=True) + + def create_dummy_compensation(self): + """ Creates an intervention which can be used for tests + + Returns: + + """ + if self.intervention is None: + self.create_dummy_intervention() + # Create dummy data + # Create log entry + action = UserActionLogEntry.objects.create( + user=self.superuser, + action=UserAction.CREATED, + ) + geometry = Geometry.objects.create() + # Finally create main object, holding the other objects + self.compensation = Compensation.objects.create( + identifier="TEST", + title="Test_title", + intervention=self.intervention, + created=action, + geometry=geometry, + comment="Test", + ) + self.intervention.generate_access_token(make_unique=True) + class KonovaViewTestCase(BaseViewTestCase): """ Holds tests for all regular views, which are not app specific diff --git a/requirements.txt b/requirements.txt index ac893917..c29cb37f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ django-tables2==2.3.4 et-xmlfile==1.1.0 idna==2.10 importlib-metadata==2.1.1 -itsdangerous==0.24 +itsdangerous<1.0.0 openpyxl==3.0.9 psycopg2-binary==2.9.1 pytz==2020.4