import pytest import os import random import datetime from marshmallow import ValidationError from BreCal import local_db from BreCal.schemas import model from BreCal.schemas.model import ParticipantType from BreCal.validators.input_validation_times import InputValidationTimes from BreCal.stubs.times_full import get_valid_stub_times, get_valid_stub_for_pytests instance_path = os.path.join(os.path.expanduser('~'), "brecal", "src", "server", "instance") local_db.initPool(os.path.dirname(instance_path), connection_filename="connection_data_local.json") def test_input_validation_times_fails_when_berth_info_exceeds_length_limit(): # success post_data = get_valid_stub_times() post_data["berth_info"] = "a"*512 # 512 characters model.TimesSchema().load(data=post_data, many=False, partial=True) post_data["berth_info"] = "" # 0 characters model.TimesSchema().load(data=post_data, many=False, partial=True) # failure with pytest.raises(ValidationError, match="Longer than maximum length 512."): post_data["berth_info"] = "a"*513 # 513 characters model.TimesSchema().load(data=post_data, many=False, partial=True) return def test_input_validation_times_fails_when_remarks_exceeds_length_limit(): # success post_data = get_valid_stub_times() post_data["remarks"] = "a"*512 # 512 characters model.TimesSchema().load(data=post_data, many=False, partial=True) post_data["remarks"] = "" # 0 characters model.TimesSchema().load(data=post_data, many=False, partial=True) # failure with pytest.raises(ValidationError, match="Longer than maximum length 512."): post_data["remarks"] = "a"*513 # 513 characters model.TimesSchema().load(data=post_data, many=False, partial=True) return def test_input_validation_times_fails_when_participant_type_is_bsmd(): # BSMD -> Failure post_data = get_valid_stub_times() post_data["participant_type"] = int(ParticipantType.BSMD) with pytest.raises(ValidationError, match="the participant_type must not be .BSMD"): model.TimesSchema().load(data=post_data, many=False, partial=True) # IntFlag property: BSMD & AGENCY -> Failure post_data = get_valid_stub_times() post_data["participant_type"] = int(ParticipantType(ParticipantType.BSMD+ParticipantType.AGENCY)) with pytest.raises(ValidationError, match="the participant_type must not be .BSMD"): model.TimesSchema().load(data=post_data, many=False, partial=True) return def test_input_validation_times_fails_when_time_key_is_not_reasonable(): """ every time key (e.g., 'eta_berth' or 'zone_entry') must be reasonable. The validation expects these values to be 'in the future' (larger than datetime.datetime.now()) and not 'in the too distant future' (e.g., more than one year from now.) """ for time_key in ["eta_berth", "etd_berth", "lock_time", "zone_entry", "operations_start", "operations_end"]: post_data = get_valid_stub_times() # success post_data[time_key] = (datetime.datetime.now() + datetime.timedelta(minutes=11)).isoformat() model.TimesSchema().load(data=post_data, many=False, partial=True) # fails with pytest.raises(ValidationError, match="The provided value must be in the future."): post_data[time_key] = (datetime.datetime.now() - datetime.timedelta(minutes=11)).isoformat() model.TimesSchema().load(data=post_data, many=False, partial=True) # fails with pytest.raises(ValidationError, match="The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries."): post_data[time_key] = (datetime.datetime.now() + datetime.timedelta(days=367)).isoformat() model.TimesSchema().load(data=post_data, many=False, partial=True) return def test_input_validation_times_fails_when_user_is_bsmd_user(): # create stub-data for a POST request from BreCal.services.jwt_handler import decode_jwt from BreCal.database.sql_utils import get_user_data_for_id import re # user 4 is a BSMD user -> fails user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=4) with pytest.raises(ValidationError, match=re.escape("current user belongs to BSMD. Cannot post 'times' datasets.")): InputValidationTimes.check_user_is_not_bsmd_type(user_data) # user 13 is not a BSMD user -> passes user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=13) # success InputValidationTimes.check_user_is_not_bsmd_type(user_data) return def test_input_validation_times_fails_when_participant_type_entry_already_exists(): # the participant type already has an entry -> fails with pytest.raises(ValidationError, match="A dataset for the participant type is already present. Participant Type:"): user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["participant_type"] = int(ParticipantType.AGENCY) # 2.) datasets may only be created, if the respective participant type did not already create one. InputValidationTimes.check_if_entry_already_exists_for_participant_type(user_data, loadedModel, content) return def test_input_validation_times_fails_when_participant_type_deviates_from_shipcall_participant_map(): # success # user id 3 is assigned as participant_type=4, but the stub assigns participant_type=4 user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) InputValidationTimes.check_if_user_fits_shipcall_participant_map(user_data, loadedModel, content) # fails # user id 4 is assigned as participant_type=1, but the stub assigns participant_type=4 with pytest.raises(ValidationError, match="is not assigned to the shipcall"): user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=4) InputValidationTimes.check_if_user_fits_shipcall_participant_map(user_data, loadedModel, content) return def test_input_validation_times_fails_when_id_references_do_not_exist(): # success: all IDs exist user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) InputValidationTimes.check_dataset_references(content) # fails: IDs do not exist # iterates once for each, berth_id, shipcall_id, participant_id and generates an artificial, non-existing ID for key in ["berth_id", "shipcall_id", "participant_id"]: user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) content[key] = loadedModel[key] = 9912737 with pytest.raises(ValidationError, match=f"The referenced {key} '{content[key]}' does not exist in the database."): InputValidationTimes.check_dataset_references(content) return from BreCal.schemas.model import ParticipantType def test_input_validation_times_fails_when_missing_required_fields_arrival(): """ evaluates every individual combination of arriving shipcalls, where one of the required values is arbitrarily missing randomly selects one of the non-terminal ParticipantTypes, which are reasonable (not .BSMD), and validates. This makes sure, that over time, every possible combination has been tested. """ # arrival + not-terminal non_terminal_list = [ParticipantType.AGENCY, ParticipantType.MOORING, ParticipantType.PILOT, ParticipantType.PORT_ADMINISTRATION, ParticipantType.TUG] for key in ["eta_berth"]+InputValidationTimes.get_post_data_type_independent_fields(): random_participant_type_for_unit_test = random.sample(non_terminal_list,k=1)[0] # pass: all required fields exist for the current shipcall type (arrival/incoming) user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 222 loadedModel["participant_type"] = random_participant_type_for_unit_test content["participant_type"] = int(random_participant_type_for_unit_test) InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # fails: iteratively creates stubs, where one of the required keys is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 222 loadedModel["participant_type"] = random_participant_type_for_unit_test content["participant_type"] = int(random_participant_type_for_unit_test) with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"): loadedModel[key] = content[key] = None InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # arrival + terminal for key in ["operations_start"]+InputValidationTimes.get_post_data_type_independent_fields(): # pass: all required fields exist for the current shipcall type (arrival/incoming) user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 222 loadedModel["participant_type"] = ParticipantType.TERMINAL content["participant_type"] = int(ParticipantType.TERMINAL) InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # fails: iteratively creates stubs, where one of the required keys is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 222 loadedModel["participant_type"] = ParticipantType.TERMINAL content["participant_type"] = int(ParticipantType.TERMINAL) with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"): loadedModel[key] = content[key] = None InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) return def test_input_validation_times_fails_when_missing_required_fields_departure(): """ evaluates every individual combination of departing shipcalls, where one of the required values is arbitrarily missing randomly selects one of the non-terminal ParticipantTypes, which are reasonable (not .BSMD), and validates. This makes sure, that over time, every possible combination has been tested. """ # departure + not-terminal non_terminal_list = [ParticipantType.AGENCY, ParticipantType.MOORING, ParticipantType.PILOT, ParticipantType.PORT_ADMINISTRATION, ParticipantType.TUG] for key in ["etd_berth"]+InputValidationTimes.get_post_data_type_independent_fields(): # select a *random* particiipant type, which is reasonable and *not* TERMINAL, and validate the function. random_participant_type_for_unit_test = random.sample(non_terminal_list,k=1)[0] # pass user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 241 loadedModel["participant_type"] = random_participant_type_for_unit_test content["participant_type"] = int(random_participant_type_for_unit_test) InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # fails: iteratively creates stubs, where one of the required keys is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 241 loadedModel["participant_type"] = random_participant_type_for_unit_test content["participant_type"] = int(random_participant_type_for_unit_test) with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"): loadedModel[key] = content[key] = None InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # departure + terminal for key in ["operations_end"]+InputValidationTimes.get_post_data_type_independent_fields(): # pass user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 241 loadedModel["participant_type"] = ParticipantType.TERMINAL content["participant_type"] = int(ParticipantType.TERMINAL) InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # fails: iteratively creates stubs, where one of the required keys is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 241 loadedModel["participant_type"] = ParticipantType.TERMINAL content["participant_type"] = int(ParticipantType.TERMINAL) with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"): loadedModel[key] = content[key] = None InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) return def test_input_validation_times_fails_when_missing_required_fields_shifting(): """ evaluates every individual combination of shifting shipcalls, where one of the required values is arbitrarily missing randomly selects one of the non-terminal ParticipantTypes, which are reasonable (not .BSMD), and validates. This makes sure, that over time, every possible combination has been tested. """ # shifting + not-terminal non_terminal_list = [ParticipantType.AGENCY, ParticipantType.MOORING, ParticipantType.PILOT, ParticipantType.PORT_ADMINISTRATION, ParticipantType.TUG] for key in ["eta_berth", "etd_berth"]+InputValidationTimes.get_post_data_type_independent_fields(): # select a *random* particiipant type, which is reasonable and *not* TERMINAL, and validate the function. random_participant_type_for_unit_test = random.sample(non_terminal_list,k=1)[0] # pass: all required fields exist for the current shipcall type (arrival/incoming) user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 189 loadedModel["participant_type"] =random_participant_type_for_unit_test content["participant_type"] = int(random_participant_type_for_unit_test) InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # fails: iteratively creates stubs, where one of the required keys is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 189 loadedModel["participant_type"] = random_participant_type_for_unit_test content["participant_type"] = int(random_participant_type_for_unit_test) with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"): loadedModel[key] = content[key] = None InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # shifting + terminal for key in ["operations_start", "operations_end"]+InputValidationTimes.get_post_data_type_independent_fields(): # pass: all required fields exist for the current shipcall type (arrival/incoming) user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 189 loadedModel["participant_type"] = ParticipantType.TERMINAL content["participant_type"] = int(ParticipantType.TERMINAL) InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) # fails: iteratively creates stubs, where one of the required keys is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) loadedModel["shipcall_id"] = content["shipcall_id"] = 189 loadedModel["participant_type"] = ParticipantType.TERMINAL content["participant_type"] = int(ParticipantType.TERMINAL) with pytest.raises(ValidationError, match="At least one of the required fields is missing. Missing:"): loadedModel[key] = content[key] = None InputValidationTimes.check_times_required_fields_post_data(loadedModel, content) return def test_input_validation_times_fails_when_participant_type_is_not_assigned__or__user_does_not_belong_to_the_same_participant_id(): """ There are two failure cases in InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines 1.) when the participant type is simply not assigned 2.) when the participant type matches to the user, but the participant_id is not assigned Test case: shipcall_id 234 is assigned to the participants DELETE# {"participant_id": 136, "type":2} and {"participant_id": 136, "type":8} {"participant_id": 2, "type":4} {"participant_id": 3, "type":1} {"participant_id": 4, "type":2} {"participant_id": 5, "type":8} Case 1: When user_id 27 should be set as participant_type 16, the call fails, because type 16 is not assigned Case 2: When user_id 2 (participant_id 1) should be set as participant_type 2, the call fails even though type 2 exists, because participant_id 4 is assigned Case 3: When user_id 9 (participant_id 4) is set as participant_type 2, the call passes. """ # fails: participant type 16 does not exist user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=27) participant_type = 16 loadedModel["shipcall_id"] = content["shipcall_id"] = 234 loadedModel["participant_id"] = content["participant_id"] = 16 loadedModel["participant_type"] = content["participant_type"] = participant_type with pytest.raises(ValidationError, match=f"Could not find a matching time dataset for the provided participant_type: {participant_type} at shipcall with id"): InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None) # fails: participant type 2 exists, but user_id 2 is part of the wrong participant_id group user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=2) loadedModel["shipcall_id"] = content["shipcall_id"] = 234 participant_type = 2 loadedModel["participant_type"] = content["participant_type"] = participant_type with pytest.raises(ValidationError, match="The dataset may only be changed by a user belonging to the same participant group as the times dataset is referring to. User participant_id:"): InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None) # pass: participant type 2 exists & user_id is part of participant_id group 4, which is correct user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=9) loadedModel["shipcall_id"] = content["shipcall_id"] = 234 participant_type = 2 loadedModel["participant_type"] = content["participant_type"] = participant_type InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=loadedModel, times_id=None) return def test_input_validation_times_put_request_fails_when_id_field_is_missing(): """used within PUT-requests. When 'id' is missing, a ValidationError is issued""" # passes: as an 'id' is provided user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) content["id"] = 379 InputValidationTimes.check_times_required_fields_put_data(content) # fails: 'id' field is missing user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) content.pop("id",None) with pytest.raises(ValidationError, match="A PUT-request requires an 'id' reference, which was not found."): InputValidationTimes.check_times_required_fields_put_data(content) return def test_input_validation_times_delete_request_fails_when_times_id_is_deleted_already(): # passes: id exists times_id = 379 InputValidationTimes.check_if_entry_is_already_deleted(times_id) # passes: id exists times_id = 391 InputValidationTimes.check_if_entry_is_already_deleted(times_id) # fails times_id = 11 with pytest.raises(ValidationError, match=f"The selected time entry is already deleted. ID: {times_id}"): InputValidationTimes.check_if_entry_is_already_deleted(times_id) # fails times_id = 4 with pytest.raises(ValidationError, match=f"The selected time entry is already deleted. ID: {times_id}"): InputValidationTimes.check_if_entry_is_already_deleted(times_id) return def test_input_validation_times_delete_request_fails_when_times_id_does_not_exist_(): # passes: times_id exists user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28) times_id = 392 pdata = [{'participant_id': 136, 'participant_type': 8, 'shipcall_id': 154}] InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id, pdata=pdata) # fails: times_id does not exist user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28) times_id = 4 with pytest.raises(ValidationError, match=f"Unknown times_id. Could not find a matching entry for ID: {times_id}"): InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id) return def test_input_validation_times_delete_request_fails_when_user_belongs_to_wrong_participant_id(): # fails: participant_id should be 136, but user_id=3 belongs to participant_id=2 user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3) times_id = 392 with pytest.raises(ValidationError, match=f"The dataset may only be changed by a user belonging to the same participant group as the times dataset is referring to. User participant_id:"): InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id) # success: the participant_id within the times entry is 136. user_id=28 belongs to participant_id=136, so it matches user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28) times_id = 392 InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id) # success: creates an artificial SPM, where the participant_id is '136', so it matches the user_id's (28) participant_id (136) user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28) times_id = 392 pdata = [{'participant_id': 136, 'participant_type': 8, 'shipcall_id': 154}] InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id, pdata=pdata) return