diff --git a/docs/ApiValidationRules.md b/docs/ApiValidationRules.md index 0e6b46f..64bf33a 100644 --- a/docs/ApiValidationRules.md +++ b/docs/ApiValidationRules.md @@ -147,6 +147,7 @@ The id field is required, missing fields will not be updated. | Field | Validation | |-------|------------| | eta_berth, etd_berth, lock_time, zone_entry, operations_start, operations_end | if set these values must be in the future| + | eta_interval_end, etd_interval_end | if set these values must be in the future. They must be larger than their ETA/ETD counterparts. | | remarks, berth_info | must be <= 512 chars | | participant_type | must not be BSMD | @@ -188,6 +189,7 @@ shipcall_id, participant_id, participant_type 1. A dataset may only be changed by a user belonging to the same participant as the times dataset is referring to. 2. See reference and value checking as specified in /times POST. +3. The shipcall type may not be changed. #### Required fields diff --git a/misc/Ampelfunktion.md b/misc/Ampelfunktion.md index 1274ba7..def1e03 100644 --- a/misc/Ampelfunktion.md +++ b/misc/Ampelfunktion.md @@ -32,7 +32,7 @@ ___ | 0002 | Zeiten für einen Eintrag weichen voneinander ab | Bedingungen:
- Header der Zeile ist zugeordnet (Agentur, Festmacher usw. - außer BSMD-Spalte)
- Zeiten ungleich (leere Einträge nicht berücksichtigen => 0001) | | | 0002 - A | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / einkommend | Schnittmenge aus:
times_agency:
- ETA Berth
____und____
times_mooring:
- ETA Berth
____und____
times_portauthority:
- ETA Berth
____und____
times_pilot:
- ETA Berth
____und____
times_tug:
- ETA Berth | rot | | 0002 - B | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / ausgehend | Schnittmenge aus:
times_agency:
- ETD Berth
____und____
times_mooring:
- ETD Berth
____und____
times_portauthority:
- ETD Berth
____und____
times_pilot:
- ETD Berth
____und____
times_tug:
- ETD Berth | rot | -| 0002 - C | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / Verholung | Schnittmenge aus:
times_agency:
- ETA Berth
- ETD Berth
____und____
times_mooring:
- ETA Berth
- ETD Berth
____und____
times_portauthority:
- ETA Berth
- ETD Berth
____und____
times_pilot:
- ETA Berth
- ETD Berth
____und____
times_tug:
- ETA Berth
- ETD Berth | rot | +| 0002 - C | Agentur + Festmacher + Hafenamt + Lotsen + Schlepper / Verholung | Schnittmenge aus:
times_agency:
- ETD Berth
____und____
times_mooring:
- ETD Berth
____und____
times_portauthority:
- ETD Berth
____und____
times_pilot:
- ETD Berth
____und____
times_tug:
- ETD Berth | rot | | 0003 | Arbeitszeit überschneidet sich mit Fahrtzeit | Bedingungen:
- Header der Zeile ist zugeordnet (Terminal)
- Zeiten passt nicht zu Ankunft / Abfahrt (leere Einträge nicht berücksichtigen => 0001) | | | 0003 - A | Terminal / einkommend | times_terminal:
- Operation Start
___vor (kleiner als)____
times_agency:
- ETA Berth | rot, aktuell __deaktiviert__! | | 0003 - B | Terminal / ausgehend + Verholung | times_terminal:
- Operation Ende
___nach (größer als)____
times_agency:
- ETD Berth | rot, aktuell __deaktiviert__! | 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/schemas/model.py b/src/server/BreCal/schemas/model.py index a21fb20..14bceda 100644 --- a/src/server/BreCal/schemas/model.py +++ b/src/server/BreCal/schemas/model.py @@ -436,6 +436,21 @@ class TimesSchema(Schema): # when 'value' is 'None', a ValidationError is not issued. valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) return + + @validates("eta_interval_end") + def validate_eta_interval_end(self, value): + # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future + # when 'value' is 'None', a ValidationError is not issued. + valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) + return + + @validates("etd_interval_end") + def validate_etd_interval_end(self, value): + # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future + # when 'value' is 'None', a ValidationError is not issued. + valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) + return + # deserialize PUT object target 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_shipcall.py b/src/server/BreCal/validators/input_validation_shipcall.py index 729a896..59b8b35 100644 --- a/src/server/BreCal/validators/input_validation_shipcall.py +++ b/src/server/BreCal/validators/input_validation_shipcall.py @@ -123,6 +123,9 @@ class InputValidationShipcall(): # time values must use future-dates InputValidationShipcall.check_times_are_in_future(loadedModel, content) + # the type of a shipcall may not be changed. It can only be set with the initial POST-request. + InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel) + # some arguments must not be provided InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys) return @@ -247,6 +250,16 @@ class InputValidationShipcall(): if not valid_participant_types: # #TODO: according to Daniel, there may eventually be multi-assignment of participants for the same role raise ValidationError(f"every participant id and type should be listed only once. Found multiple entries for one of the participants.") + @staticmethod + def check_shipcall_type_is_unchanged(loadedModel:dict): + # the type of a shipcall may only be set on POST requests. Afterwards, shipcall types may not be changed. + query = SQLQuery.get_shipcall_by_id() + shipcall = execute_sql_query_standalone(query=query, model=Shipcall, param={"id":loadedModel.get("id")}, command_type="single") + + if int(loadedModel["type"]) != int(shipcall.type): + raise ValidationError(f"The shipcall type may only be set in the initial POST-request. Afterwards, changing the shipcall type is not allowed.") # @pytest.raises + return + @staticmethod def check_forbidden_arguments(content:dict, forbidden_keys=["evaluation", "evaluation_message"]): """ diff --git a/src/server/BreCal/validators/input_validation_times.py b/src/server/BreCal/validators/input_validation_times.py index 0385220..688c651 100644 --- a/src/server/BreCal/validators/input_validation_times.py +++ b/src/server/BreCal/validators/input_validation_times.py @@ -29,32 +29,32 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic ShipcallType.arrival:{ ParticipantType.undefined:[], # should not be set in POST requests ParticipantType.BSMD:[], # should not be set in POST requests - ParticipantType.TERMINAL:["operations_start"], - ParticipantType.AGENCY:["eta_berth"], - ParticipantType.MOORING:["eta_berth"], - ParticipantType.PILOT:["eta_berth"], - ParticipantType.PORT_ADMINISTRATION:["eta_berth"], - ParticipantType.TUG:["eta_berth"], + ParticipantType.TERMINAL:[], + ParticipantType.AGENCY:[], + ParticipantType.MOORING:[], + ParticipantType.PILOT:[], + ParticipantType.PORT_ADMINISTRATION:[], + ParticipantType.TUG:[], }, ShipcallType.departure:{ ParticipantType.undefined:[], # should not be set in POST requests ParticipantType.BSMD:[], # should not be set in POST requests - ParticipantType.TERMINAL:["operations_end"], - ParticipantType.AGENCY:["etd_berth"], - ParticipantType.MOORING:["etd_berth"], - ParticipantType.PILOT:["etd_berth"], - ParticipantType.PORT_ADMINISTRATION:["etd_berth"], - ParticipantType.TUG:["etd_berth"], + ParticipantType.TERMINAL:[], + ParticipantType.AGENCY:[], + ParticipantType.MOORING:[], + ParticipantType.PILOT:[], + ParticipantType.PORT_ADMINISTRATION:[], + ParticipantType.TUG:[], }, 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:[], + ParticipantType.AGENCY:[], + ParticipantType.MOORING:[], + ParticipantType.PILOT:[], + ParticipantType.PORT_ADMINISTRATION:[], + ParticipantType.TUG:[], }, } return post_data_type_dependent_required_fields_dict @@ -167,6 +167,17 @@ class InputValidationTimes(): if ParticipantType.BSMD in loadedModel["participant_type"]: raise ValidationError(f"current user belongs to BSMD. Cannot post times datasets. Found user data: {user_data}") + + if (loadedModel["etd_interval_end"] is not None) and (loadedModel["etd_berth"] is not None): + time_end_after_time_start = loadedModel["etd_interval_end"] >= loadedModel["etd_berth"] + if not time_end_after_time_start: + raise ValidationError(f"The provided time interval for the estimated departure time is invalid. The interval end takes place before the interval start. Found interval data: {loadedModel['etd_berth']} to {loadedModel['etd_interval_end']}") + + + if (loadedModel["eta_interval_end"] is not None) and (loadedModel["eta_berth"] is not None): + time_end_after_time_start = loadedModel["eta_interval_end"] >= loadedModel["eta_berth"] + if not time_end_after_time_start: + raise ValidationError(f"The provided time interval for the estimated arrival time is invalid. The interval begin takes place after the interval end. Found interval data: {loadedModel['eta_berth']} to {loadedModel['eta_interval_end']}") return @staticmethod @@ -471,3 +482,44 @@ class InputValidationTimes(): has_bsmd_flag = ParticipantFlag.BSMD in [ParticipantFlag(participant.get("flags"))] return has_bsmd_flag + +def deprecated_build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dict[ParticipantType,typing.Optional[list[str]]]]: + """ + The required fields of a POST-request depend on ShipcallType and ParticipantType. This function creates + a dictionary, which maps those types to a list of required fields. + + The participant types 'undefined' and 'bsmd' should not be used in POST-requests. They return 'None'. + """ + post_data_type_dependent_required_fields_dict = { + ShipcallType.arrival:{ + ParticipantType.undefined:[], # should not be set in POST requests + ParticipantType.BSMD:[], # should not be set in POST requests + ParticipantType.TERMINAL:[], + ParticipantType.AGENCY:["eta_berth"], + ParticipantType.MOORING:["eta_berth"], + ParticipantType.PILOT:["eta_berth"], + ParticipantType.PORT_ADMINISTRATION:["eta_berth"], + ParticipantType.TUG:["eta_berth"], + }, + ShipcallType.departure:{ + ParticipantType.undefined:[], # should not be set in POST requests + ParticipantType.BSMD:[], # should not be set in POST requests + ParticipantType.TERMINAL:[], + ParticipantType.AGENCY:["etd_berth"], + ParticipantType.MOORING:["etd_berth"], + ParticipantType.PILOT:["etd_berth"], + ParticipantType.PORT_ADMINISTRATION:["etd_berth"], + ParticipantType.TUG:["etd_berth"], + }, + ShipcallType.shifting:{ + ParticipantType.undefined:[], # should not be set in POST requests + ParticipantType.BSMD:[], # should not be set in POST requests + ParticipantType.TERMINAL:[], + 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/BreCal/validators/validation_error.py b/src/server/BreCal/validators/validation_error.py index cdac103..65ee985 100644 --- a/src/server/BreCal/validators/validation_error.py +++ b/src/server/BreCal/validators/validation_error.py @@ -27,8 +27,15 @@ def create_validation_error_response(ex:ValidationError, status_code:int=400)->t "errors":errors, "valid_data":valid_data } - return (json.dumps(json_response), status_code) + + # json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not) + # natively serializable are properly serialized. + serialized_response = json.dumps(json_response, default=str) + return (serialized_response, status_code) def create_werkzeug_error_response(ex:Forbidden, status_code:int=403)->typing.Tuple[str,int]: - return json.dumps({"message":ex.description}), status_code + # json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not) + # natively serializable are properly serialized. + serialized_response = json.dumps({"message":ex.description}, default=str) + return serialized_response, status_code diff --git a/src/server/BreCal/validators/validation_rule_functions.py b/src/server/BreCal/validators/validation_rule_functions.py index ae3e3bc..b035345 100644 --- a/src/server/BreCal/validators/validation_rule_functions.py +++ b/src/server/BreCal/validators/validation_rule_functions.py @@ -27,7 +27,7 @@ error_message_dict = { # 0002 A+B+C "validation_rule_fct_shipcall_incoming_participants_disagree_on_eta":"There are deviating times between agency, mooring, port authority, pilot and tug for the estimated time of arrival (ETA) {Rule #0002A}", "validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd":"There are deviating times between agency, mooring, port authority, pilot and tug for the estimated time of departure (ETD) {Rule #0002B}", - "validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd":"There are deviating times between agency, mooring, port authority, pilot and tug for ETA and ETD {Rule #0002C}", + "validation_rule_fct_shipcall_shifting_participants_disagree_on_etd":"There are deviating times between agency, mooring, port authority, pilot and tug for the estimated time of departure (ETD) {Rule #0002C}", # 0003 A+B "validation_rule_fct_eta_time_not_in_operation_window":"The estimated time of arrival will be AFTER the planned start of operations. {Rule #0003A}", @@ -723,21 +723,13 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions): else: return self.get_no_violation_default_output() - def validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(self, shipcall, df_times, *args, **kwargs): + def validation_rule_fct_shipcall_shifting_participants_disagree_on_etd(self, shipcall, df_times, *args, **kwargs): """ Code: #0002-C Type: Local Rule - Description: this validation checks, whether the participants expect different ETA or ETD times + Description: this validation checks, whether the participants expect different ETD times Filter: only applies to shifting shipcalls """ - violation_state_eta = self.check_participants_agree_on_estimated_time( - shipcall = shipcall, - - query="eta_berth", - df_times=df_times, - applicable_shipcall_type=ShipcallType.SHIFTING - ) - violation_state_etd = self.check_participants_agree_on_estimated_time( shipcall = shipcall, @@ -746,16 +738,14 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions): applicable_shipcall_type=ShipcallType.SHIFTING ) - # apply 'eta_berth' check # apply 'etd_berth' - # violation: if either 'eta_berth' or 'etd_berth' is violated + # violation: if either 'etd_berth' is violated # functionally, this is the same as individually comparing all times for the participants - # times_agency.eta_berth==times_mooring.eta_berth==times_portadministration.eta_berth==times_pilot.eta_berth==times_tug.eta_berth # times_agency.etd_berth==times_mooring.etd_berth==times_portadministration.etd_berth==times_pilot.etd_berth==times_tug.etd_berth - violation_state = (violation_state_eta) or (violation_state_etd) + violation_state = (violation_state_etd) if violation_state: - validation_name = "validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd" + validation_name = "validation_rule_fct_shipcall_shifting_participants_disagree_on_etd" return (StatusFlags.RED, validation_name) else: return self.get_no_violation_default_output() 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_shipcall.py b/src/server/tests/validators/test_input_validation_shipcall.py index df41f1f..87b3b7f 100644 --- a/src/server/tests/validators/test_input_validation_shipcall.py +++ b/src/server/tests/validators/test_input_validation_shipcall.py @@ -901,3 +901,35 @@ def test_post_data_with_valid_data(get_stub_token): assert response.status_code==201 return +def test_input_validation_shipcall_put_fails_when_type_differs(): + from marshmallow import ValidationError + from BreCal.stubs.shipcall import get_stub_valid_shipcall_arrival + from BreCal.schemas import model + from BreCal.database.sql_queries import SQLQuery + from BreCal.database.sql_handler import execute_sql_query_standalone + + from BreCal.validators.input_validation_shipcall import InputValidationShipcall + + # stub data + put_data = get_stub_valid_shipcall_arrival() + put_data["id"] = 3 + loadedModel = model.ShipcallSchema().load(data=put_data, many=False, partial=True) + + # load data from DB + shipcall_id = loadedModel.get("id") + query = SQLQuery.get_shipcall_by_id() + shipcall = execute_sql_query_standalone(query=query, model=model.Shipcall, param={"id":shipcall_id}, command_type="single") + + # create failure case. Ensures that the type is not the same as in the loaded shipcall from the DB + failure_case = 1 if shipcall.type==2 else 2 + loadedModel["type"] = model.ShipcallType(failure_case) + + # should fail: different 'type' + with pytest.raises(ValidationError, match="The shipcall type may only be set in the initial POST-request. Afterwards, changing the shipcall type is not allowed"): + InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel) + + # should pass: same 'type' + loadedModel["type"] = shipcall.type + InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel) + return + 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 + diff --git a/src/server/tests/validators/test_validation_error.py b/src/server/tests/validators/test_validation_error.py new file mode 100644 index 0000000..a103919 --- /dev/null +++ b/src/server/tests/validators/test_validation_error.py @@ -0,0 +1,19 @@ +import pytest + +def test_create_validation_error_response_is_serializable(): + from BreCal.stubs.times_full import get_valid_stub_times + from BreCal.schemas import model + from BreCal.validators.validation_error import create_validation_error_response + + content = get_valid_stub_times() + + import datetime + content["operations_end"] = (datetime.datetime.now()-datetime.timedelta(minutes=14)).isoformat() + + content["id"] = 3 + try: + loadedModel = model.TimesSchema().load(data=content, many=False, partial=True) + except Exception as ex: + my_var = ex + create_validation_error_response(ex=ex, status_code=400) # this function initially created errors, as datetime objects were not serializable. Corrected now. + return