From 5a7ea0b6c2c1b615f1336b6d3dc63b42cc8f5e72 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 27 Jan 2022 17:09:38 +0100 Subject: [PATCH] #31 API Tests * adds creation tests with minimum data for intervention, compensation, ema and ecoaccount * fixes bug where empty geometry would not be created properly using the API * reworks key fetching from POST data, so inproperly stated keys will lead to an error for the API user, instead of silently working and use default data * adds some logical checks for deductable_surface of eco account creation using api * fixes bug that would have occured on creating compensations via api --- api/tests/v1/create/__init__.py | 7 ++ .../create/compensation_create_post_body.json | 19 ++++ .../create/ecoaccount_create_post_body.json | 25 +++++ api/tests/v1/create/ema_create_post_body.json | 21 ++++ .../create/intervention_create_post_body.json | 21 ++++ api/tests/v1/create/test_api_create.py | 105 ++++++++++++++++++ api/tests/v1/test_api_create.py | 31 ------ api/tests/v1/test_api_sharing.py | 18 ++- api/utils/serializer/serializer.py | 2 + api/utils/serializer/v1/compensation.py | 2 +- api/utils/serializer/v1/ecoaccount.py | 30 ++++- api/utils/serializer/v1/ema.py | 33 +++++- api/utils/serializer/v1/serializer.py | 10 +- intervention/models/intervention.py | 2 +- 14 files changed, 280 insertions(+), 46 deletions(-) create mode 100644 api/tests/v1/create/__init__.py create mode 100644 api/tests/v1/create/compensation_create_post_body.json create mode 100644 api/tests/v1/create/ecoaccount_create_post_body.json create mode 100644 api/tests/v1/create/ema_create_post_body.json create mode 100644 api/tests/v1/create/intervention_create_post_body.json create mode 100644 api/tests/v1/create/test_api_create.py delete mode 100644 api/tests/v1/test_api_create.py diff --git a/api/tests/v1/create/__init__.py b/api/tests/v1/create/__init__.py new file mode 100644 index 00000000..c5acabae --- /dev/null +++ b/api/tests/v1/create/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 27.01.22 + +""" diff --git a/api/tests/v1/create/compensation_create_post_body.json b/api/tests/v1/create/compensation_create_post_body.json new file mode 100644 index 00000000..b0d21e83 --- /dev/null +++ b/api/tests/v1/create/compensation_create_post_body.json @@ -0,0 +1,19 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + ], + "properties": { + "title": "Test_compensation", + "is_cef": false, + "is_coherence_keeping": false, + "intervention": "MUST_BE_SET_IN_TEST", + "before_states": [ + ], + "after_states": [ + ], + "actions": [ + ], + "deadlines": [ + ] + } +} \ No newline at end of file diff --git a/api/tests/v1/create/ecoaccount_create_post_body.json b/api/tests/v1/create/ecoaccount_create_post_body.json new file mode 100644 index 00000000..742c0fb3 --- /dev/null +++ b/api/tests/v1/create/ecoaccount_create_post_body.json @@ -0,0 +1,25 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + ], + "properties": { + "title": "Test_ecoaccount", + "deductable_surface": 10000.0, + "responsible": { + "conservation_office": null, + "conservation_file_number": null, + "handler": null + }, + "legal": { + "agreement_date": null + }, + "before_states": [ + ], + "after_states": [ + ], + "actions": [ + ], + "deadlines": [ + ] + } +} \ No newline at end of file diff --git a/api/tests/v1/create/ema_create_post_body.json b/api/tests/v1/create/ema_create_post_body.json new file mode 100644 index 00000000..d9081fb8 --- /dev/null +++ b/api/tests/v1/create/ema_create_post_body.json @@ -0,0 +1,21 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + ], + "properties": { + "title": "Test_ema", + "responsible": { + "conservation_office": null, + "conservation_file_number": null, + "handler": null + }, + "before_states": [ + ], + "after_states": [ + ], + "actions": [ + ], + "deadlines": [ + ] + } +} \ No newline at end of file diff --git a/api/tests/v1/create/intervention_create_post_body.json b/api/tests/v1/create/intervention_create_post_body.json new file mode 100644 index 00000000..2d39919a --- /dev/null +++ b/api/tests/v1/create/intervention_create_post_body.json @@ -0,0 +1,21 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + ], + "properties": { + "title": "Test_intervention", + "responsible": { + "registration_office": null, + "registration_file_number": null, + "conservation_office": null, + "conservation_file_number": null, + "handler": null + }, + "legal": { + "registration_date": null, + "binding_date": null, + "process_type": null, + "laws": [] + } + } +} \ No newline at end of file diff --git a/api/tests/v1/create/test_api_create.py b/api/tests/v1/create/test_api_create.py new file mode 100644 index 00000000..5e7134a1 --- /dev/null +++ b/api/tests/v1/create/test_api_create.py @@ -0,0 +1,105 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 27.01.22 + +""" +import json + +from django.urls import reverse + +from api.tests.v1.test_api_sharing import BaseAPIV1TestCase + + +class APIV1CreateTestCase(BaseAPIV1TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + def _run_create_request(self, url, data): + data = json.dumps(data) + response = self.client.post( + url, + data=data, + content_type="application/json", + **self.header_data + ) + return response + + def _test_create_object(self, url, post_body): + """ Tests the API creation of a new data object. + + Post body data stored in a local json file + + Args: + url (str): The api creation url + post_body (dict): The post body content as dict + + Returns: + + """ + response = self._run_create_request(url, post_body) + self.assertEqual(response.status_code, 200, msg=response.content) + content = json.loads(response.content) + self.assertIsNotNone(content.get("id", None), msg=response.content) + + def test_create_intervention(self): + """ Tests api creation of bare minimum interventions + + Returns: + + """ + url = reverse("api:v1:intervention") + json_file_path = "api/tests/v1/create/intervention_create_post_body.json" + with open(json_file_path) as json_file: + post_body = json.load(fp=json_file) + self._test_create_object(url, post_body) + + def test_create_compensation(self): + """ Tests api creation of bare minimum interventions + + Returns: + + """ + url = reverse("api:v1:compensation") + json_file_path = "api/tests/v1/create/compensation_create_post_body.json" + with open(json_file_path) as json_file: + post_body = json.load(fp=json_file) + post_body["properties"]["intervention"] = str(self.intervention.id) + + # Expect this first request to fail, since user has no shared access on the intervention, we want to create + # a compensation for + response = self._run_create_request(url, post_body) + self.assertEqual(response.status_code, 500, msg=response.content) + content = json.loads(response.content) + self.assertGreater(len(content.get("errors", [])), 0, msg=response.content) + + # Add the user to the shared users of the intervention and try again! Now everything should work as expected. + self.intervention.users.add(self.superuser) + self._test_create_object(url, post_body) + + def test_create_eco_account(self): + """ Tests api creation of bare minimum interventions + + Returns: + + """ + url = reverse("api:v1:ecoaccount") + json_file_path = "api/tests/v1/create/ecoaccount_create_post_body.json" + with open(json_file_path) as json_file: + post_body = json.load(fp=json_file) + self._test_create_object(url, post_body) + + def test_create_ema(self): + """ Tests api creation of bare minimum interventions + + Returns: + + """ + url = reverse("api:v1:ema") + json_file_path = "api/tests/v1/create/ema_create_post_body.json" + with open(json_file_path) as json_file: + post_body = json.load(fp=json_file) + self._test_create_object(url, post_body) + diff --git a/api/tests/v1/test_api_create.py b/api/tests/v1/test_api_create.py deleted file mode 100644 index 175bc7fc..00000000 --- a/api/tests/v1/test_api_create.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 27.01.22 - -""" -from konova.settings import DEFAULT_GROUP -from konova.tests.test_views import BaseTestCase - - -class BaseAPIV1TestCase(BaseTestCase): - @classmethod - def setUpTestData(cls): - super().setUpTestData() - - cls.superuser.get_API_token() - cls.superuser.api_token.is_active = True - cls.superuser.api_token.save() - default_group = cls.groups.get(name=DEFAULT_GROUP) - cls.superuser.groups.add(default_group) - - cls.header_data = { - "HTTP_ksptoken": cls.superuser.api_token.token, - } - - -class APIV1CreateTestCase(BaseAPIV1TestCase): - @classmethod - def setUpTestData(cls): - super().setUpTestData() diff --git a/api/tests/v1/test_api_sharing.py b/api/tests/v1/test_api_sharing.py index 289a16b0..d5c8b057 100644 --- a/api/tests/v1/test_api_sharing.py +++ b/api/tests/v1/test_api_sharing.py @@ -2,11 +2,27 @@ import json from django.urls import reverse -from api.tests.v1.test_api_create import BaseAPIV1TestCase from konova.settings import DEFAULT_GROUP +from konova.tests.test_views import BaseTestCase from konova.utils.user_checks import is_default_group_only +class BaseAPIV1TestCase(BaseTestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.superuser.get_API_token() + cls.superuser.api_token.is_active = True + cls.superuser.api_token.save() + default_group = cls.groups.get(name=DEFAULT_GROUP) + cls.superuser.groups.add(default_group) + + cls.header_data = { + "HTTP_ksptoken": cls.superuser.api_token.token, + } + + class APIV1SharingTestCase(BaseAPIV1TestCase): @classmethod diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index 2a7639bb..d0a01b95 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -123,6 +123,8 @@ class AbstractModelAPISerializer: if isinstance(geojson, dict): geojson = json.dumps(geojson) geometry = geos.fromstr(geojson) + if geometry.empty: + geometry = None return geometry def get_obj_from_db(self, id, user): diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index cc3428b2..2010ed02 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -79,7 +79,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa Returns: obj (Compensation) """ - if obj.intervention.id == intervention_id: + if obj.intervention is not None and obj.intervention.id == intervention_id: # Nothing to do here return obj diff --git a/api/utils/serializer/v1/ecoaccount.py b/api/utils/serializer/v1/ecoaccount.py index 9fc1e63e..59aa5f76 100644 --- a/api/utils/serializer/v1/ecoaccount.py +++ b/api/utils/serializer/v1/ecoaccount.py @@ -9,6 +9,7 @@ from django.db import transaction from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \ LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from compensation.models import EcoAccount from intervention.models import Legal, Responsibility from konova.models import Geometry @@ -46,6 +47,26 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1, "handler": responsible.handler, } + def set_responsibility(self, obj, responsibility_data: dict): + """ Sets the responsible data contents to the provided responsibility_data dict + + Args: + obj (Intervention): The intervention object + responsibility_data (dict): The new data + + Returns: + obj + """ + if responsibility_data is None: + return obj + obj.responsible.conservation_office = self.konova_code_from_json( + responsibility_data["conservation_office"], + CODELIST_CONSERVATION_OFFICE_ID, + ) + obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] + obj.responsible.handler = responsibility_data["handler"] + return obj + def set_legal(self, obj, legal_data): obj.legal.registration_date = legal_data.get("agreement_date", None) return obj @@ -95,7 +116,14 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1, properties = json_model["properties"] obj.identifier = obj.generate_new_identifier() obj.title = properties["title"] - obj.deductable_surface = float(properties["deductable_surface"]) + + try: + obj.deductable_surface = float(properties["deductable_surface"]) + except TypeError: + raise ValueError("Deductable surface (m²) must be a number >= 0") + if obj.deductable_surface < 0: + raise ValueError("Deductable surface (m²) must be greater or equal 0") + obj = self.set_responsibility(obj, properties["responsible"]) obj = self.set_legal(obj, properties["legal"]) diff --git a/api/utils/serializer/v1/ema.py b/api/utils/serializer/v1/ema.py index 8b1f358a..728faa84 100644 --- a/api/utils/serializer/v1/ema.py +++ b/api/utils/serializer/v1/ema.py @@ -9,6 +9,7 @@ from django.db import transaction from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \ ResponsibilityAPISerializerV1Mixin +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from ema.models import Ema from intervention.models import Responsibility from konova.models import Geometry @@ -19,6 +20,13 @@ from user.models import UserActionLogEntry class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin): model = Ema + def extend_properties_data(self, entry): + self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) + self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) + self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) + self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + def responsible_to_json(self, responsible: Responsibility): return { "conservation_office": self.konova_code_to_json(responsible.conservation_office), @@ -26,12 +34,25 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe "handler": responsible.handler, } - def extend_properties_data(self, entry): - self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) - self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) - self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) - self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) - self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + def set_responsibility(self, obj, responsibility_data: dict): + """ Sets the responsible data contents to the provided responsibility_data dict + + Args: + obj (Intervention): The intervention object + responsibility_data (dict): The new data + + Returns: + obj + """ + if responsibility_data is None: + return obj + obj.responsible.conservation_office = self.konova_code_from_json( + responsibility_data["conservation_office"], + CODELIST_CONSERVATION_OFFICE_ID, + ) + obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] + obj.responsible.handler = responsibility_data["handler"] + return obj def initialize_objects(self, json_model, user): """ Initializes all needed objects from the json_model data diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index ace721ef..3d23eff1 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -168,16 +168,16 @@ class ResponsibilityAPISerializerV1Mixin: if responsibility_data is None: return obj obj.responsible.registration_office = self.konova_code_from_json( - responsibility_data.get("registration_office", None), + responsibility_data["registration_office"], CODELIST_REGISTRATION_OFFICE_ID ) - obj.responsible.registration_file_number = responsibility_data.get("registration_file_number", None) + obj.responsible.registration_file_number = responsibility_data["registration_file_number"] obj.responsible.conservation_office = self.konova_code_from_json( - responsibility_data.get("conservation_office", None), + responsibility_data["conservation_office"], CODELIST_CONSERVATION_OFFICE_ID, ) - obj.responsible.conservation_file_number = responsibility_data.get("conservation_file_number", None) - obj.responsible.handler = responsibility_data.get("handler", None) + obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] + obj.responsible.handler = responsibility_data["handler"] return obj diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 5e2aca0d..d1a11e40 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -116,7 +116,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec z_l = v_zoom break zoom_lvl = z_l - except AttributeError: + except (AttributeError, IndexError) as e: # If no geometry has been added, yet. x = 1 y = 1