From 3d2405e8fbbfcb585f4bbc0e29db771cb3a79735 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Wed, 4 Sep 2024 09:41:58 +0200 Subject: [PATCH] maintenance of API Input Validation (ship & times) --- src/server/BreCal/database/sql_queries.py | 5 ++ src/server/BreCal/stubs/times.py | 12 ++++ .../validators/input_validation_ship.py | 22 ++++--- .../validators/input_validation_times.py | 12 ++-- src/server/tests/database/test_sql_queries.py | 7 +++ .../validators/test_input_validation_times.py | 57 +++++++++++++++++++ 6 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 src/server/BreCal/stubs/times.py diff --git a/src/server/BreCal/database/sql_queries.py b/src/server/BreCal/database/sql_queries.py index 9888ada..e0e9b7e 100644 --- a/src/server/BreCal/database/sql_queries.py +++ b/src/server/BreCal/database/sql_queries.py @@ -266,6 +266,11 @@ class SQLQuery(): query = "SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship ORDER BY name" return query + @staticmethod + def get_ship_by_id()->str: + query = "SELECT * FROM ship where id = ?id?" + return query + @staticmethod def get_times()->str: query = "SELECT id, eta_berth, eta_berth_fixed, etd_berth, etd_berth_fixed, lock_time, lock_time_fixed, " + \ diff --git a/src/server/BreCal/stubs/times.py b/src/server/BreCal/stubs/times.py new file mode 100644 index 0000000..a26b659 --- /dev/null +++ b/src/server/BreCal/stubs/times.py @@ -0,0 +1,12 @@ +import datetime +from BreCal.schemas import model +from BreCal.schemas.model import ParticipantType + +def get_schema_model_stub_departure(): + schemaModel = {'id': 0, 'eta_berth': None, 'eta_berth_fixed': None, 'etd_berth': datetime.datetime(2024, 9, 7, 15, 12, 58), 'etd_berth_fixed': None, 'lock_time': None, 'lock_time_fixed': None, 'zone_entry': None, 'zone_entry_fixed': None, 'operations_start': None, 'operations_end': None, 'remarks': 'test', 'participant_id': 10, 'berth_id': 146, 'berth_info': '', 'pier_side': None, 'shipcall_id': 115, 'participant_type': ParticipantType.AGENCY, 'ata': None, 'atd': None, 'eta_interval_end': None, 'etd_interval_end': None, 'created': None, 'modified': None} + return schemaModel + +def get_schema_model_stub_arrival(): + schemaModel = {'id': 0, 'eta_berth': datetime.datetime(2024, 9, 7, 15, 12, 58), 'eta_berth_fixed': None, 'etd_berth': None, 'etd_berth_fixed': None, 'lock_time': None, 'lock_time_fixed': None, 'zone_entry': None, 'zone_entry_fixed': None, 'operations_start': None, 'operations_end': None, 'remarks': 'test', 'participant_id': 10, 'berth_id': 146, 'berth_info': '', 'pier_side': None, 'shipcall_id': 115, 'participant_type': ParticipantType.AGENCY, 'ata': None, 'atd': None, 'eta_interval_end': None, 'etd_interval_end': None, 'created': None, 'modified': None} + return schemaModel + diff --git a/src/server/BreCal/validators/input_validation_ship.py b/src/server/BreCal/validators/input_validation_ship.py index 47fa918..3a47b2c 100644 --- a/src/server/BreCal/validators/input_validation_ship.py +++ b/src/server/BreCal/validators/input_validation_ship.py @@ -6,6 +6,8 @@ from marshmallow import ValidationError from string import ascii_letters, digits from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant, ShipcallType +from BreCal.database.sql_handler import execute_sql_query_standalone +from BreCal.database.sql_queries import SQLQuery from BreCal.impl.participant import GetParticipant from BreCal.impl.ships import GetShips from BreCal.impl.berths import GetBerths @@ -47,14 +49,14 @@ class InputValidationShip(): # 1.) Only users of type BSMD are allowed to PUT InputValidationShip.check_user_is_bsmd_type(user_data) - # 2.) The IMO number field may not be changed + # 2.) ID field is mandatory + InputValidationShip.content_contains_ship_id(content) + + # 3.) The IMO number field may not be changed InputValidationShip.put_content_may_not_contain_imo_number(content) - # 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema) + # 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema) InputValidationShip.optionally_evaluate_bollard_pull_value(content) - - # 4.) ID field is mandatory - InputValidationShip.content_contains_ship_id(content) return @staticmethod @@ -101,11 +103,15 @@ class InputValidationShip(): @staticmethod def put_content_may_not_contain_imo_number(content:dict): + # IMO is a required field, so it will never be None outside of tests. If so, there is no violation put_data_ship_imo = content.get("imo",None) + if put_data_ship_imo is None: + return + + # grab the ship by its ID and compare, whether the IMO is unchanged + ship = execute_sql_query_standalone(SQLQuery.get_ship_by_id(), param={"id":content.get("id")}, command_type="single", model=Ship) - # #TODO: Aktuelle IMO abfragen und nach Änderung suchen, bevor eine Fehlermeldung erstellt wird - - if put_data_ship_imo is not None: + if put_data_ship_imo != ship.imo: raise ValidationError(f"The IMO number field may not be changed since it serves the purpose of a primary (matching) key.") return diff --git a/src/server/BreCal/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py index 37b70b8..dbe4b3b 100644 --- a/src/server/BreCal/validators/input_validation_times.py +++ b/src/server/BreCal/validators/input_validation_times.py @@ -49,12 +49,12 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic ShipcallType.shifting:{ ParticipantType.undefined:[], # should not be set in POST requests ParticipantType.BSMD:[], # should not be set in POST requests - ParticipantType.TERMINAL:["operations_start", "operations_end"], - ParticipantType.AGENCY:["eta_berth", "etd_berth"], - ParticipantType.MOORING:["eta_berth", "etd_berth"], - ParticipantType.PILOT:["eta_berth", "etd_berth"], - ParticipantType.PORT_ADMINISTRATION:["eta_berth", "etd_berth"], - ParticipantType.TUG:["eta_berth", "etd_berth"], + ParticipantType.TERMINAL:["operations_start"], + ParticipantType.AGENCY:["etd_berth"], + ParticipantType.MOORING:["etd_berth"], + ParticipantType.PILOT:["etd_berth"], + ParticipantType.PORT_ADMINISTRATION:["etd_berth"], + ParticipantType.TUG:["etd_berth"], }, } return post_data_type_dependent_required_fields_dict diff --git a/src/server/tests/database/test_sql_queries.py b/src/server/tests/database/test_sql_queries.py index fc4267a..9db3d75 100644 --- a/src/server/tests/database/test_sql_queries.py +++ b/src/server/tests/database/test_sql_queries.py @@ -87,6 +87,13 @@ def test_sql_get_ships(): assert all([isinstance(ship, model.Ship) for ship in ships]) return +def test_sql_get_ship_by_id(): + query = #"SELECT * FROM ship where id = ?id?" # #TODO_refactor: put into the SQLQuery object + ship = execute_sql_query_standalone(SQLQuery.get_ship_by_id(), param={"id":1}, command_type="single", model=model.Ship) + assert isinstance(ship, model.Ship) + return + + def test_sql_get_times(): options = {'shipcall_id':153} times = execute_sql_query_standalone(query=SQLQuery.get_times(), model=model.Times, param={"scid" : options["shipcall_id"]}) diff --git a/src/server/tests/validators/test_input_validation_times.py b/src/server/tests/validators/test_input_validation_times.py index 550387a..7a9b7d5 100644 --- a/src/server/tests/validators/test_input_validation_times.py +++ b/src/server/tests/validators/test_input_validation_times.py @@ -409,3 +409,60 @@ def test_input_validation_times_delete_request_fails_when_user_belongs_to_wrong_ InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id, pdata=pdata) return +def test_input_validation_times_check_dataset_values_for_time_intervals(): + import datetime + from BreCal.schemas import model + from BreCal.validators.input_validation_times import InputValidationTimes + from BreCal.stubs.times import get_schema_model_stub_arrival, get_schema_model_stub_departure + from marshmallow import ValidationError + + ### ETD (departure) + # expected to pass + schemaModel = get_schema_model_stub_departure() + schemaModel["etd_interval_end"] = schemaModel["etd_berth"] + datetime.timedelta(minutes=2) + + schemaModel["etd_berth"] = schemaModel["etd_berth"].isoformat() + schemaModel["etd_interval_end"] = schemaModel["etd_interval_end"].isoformat() + content = schemaModel + loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True) + + InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content) + + # expected to fail: the from-to-interval is incorrectly set. + schemaModel = get_schema_model_stub_departure() + schemaModel["etd_interval_end"] = schemaModel["etd_berth"] - datetime.timedelta(minutes=2) + + schemaModel["etd_berth"] = schemaModel["etd_berth"].isoformat() + schemaModel["etd_interval_end"] = schemaModel["etd_interval_end"].isoformat() + content = schemaModel + loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True) + + with pytest.raises(ValidationError, match="The provided time interval for the estimated departure time is invalid"): + InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content) + + + ### ETA (arrival) + # expected to pass + schemaModel = get_schema_model_stub_arrival() + schemaModel["eta_interval_end"] = schemaModel["eta_berth"] + datetime.timedelta(minutes=2) + + schemaModel["eta_berth"] = schemaModel["eta_berth"].isoformat() + schemaModel["eta_interval_end"] = schemaModel["eta_interval_end"].isoformat() + content = schemaModel + loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True) + + InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content) + + # expected to fail: the from-to-interval is incorrectly set. + schemaModel = get_schema_model_stub_arrival() + schemaModel["eta_interval_end"] = schemaModel["eta_berth"] - datetime.timedelta(minutes=2) + + schemaModel["eta_berth"] = schemaModel["eta_berth"].isoformat() + schemaModel["eta_interval_end"] = schemaModel["eta_interval_end"].isoformat() + content = schemaModel + loadedModel = model.TimesSchema().load(data=schemaModel, many=False, partial=True) + + with pytest.raises(ValidationError, match="The provided time interval for the estimated arrival time is invalid"): + InputValidationTimes.check_dataset_values(user_data={}, loadedModel=loadedModel, content=content) + return +