399 lines
22 KiB
Python
399 lines
22 KiB
Python
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", "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 assigned to the shipcall in a different role."):
|
|
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 222 is assigned to the participants {"participant_id": 136, "type":2} and {"participant_id": 136, "type":8}
|
|
|
|
Case 1:
|
|
When user_id 3 should be set as participant_type 4, the call fails, because type 4 is not assigned
|
|
|
|
Case 2:
|
|
When user_id 2 (participant_id 2) should be set as participant_type 2, the call fails even though type 2 exists,
|
|
because participant_id 136 is assigned
|
|
|
|
Case 3:
|
|
When user_id 28 (participant_id 136) is set as participant_type 2, the call passes.
|
|
"""
|
|
# fails: participant type 4 does not exist
|
|
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=3)
|
|
participant_type = 4
|
|
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
|
loadedModel["participant_id"] = content["participant_id"] = 2
|
|
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}. Found Time Datasets:"):
|
|
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_id 28 or 29 would be)
|
|
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=2)
|
|
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
|
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 136, which is correct
|
|
user_data, loadedModel, content = get_valid_stub_for_pytests(user_id=28)
|
|
loadedModel["shipcall_id"] = content["shipcall_id"] = 222
|
|
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
|
|
InputValidationTimes.check_user_belongs_to_same_group_as_dataset_determines(user_data, loadedModel=None, times_id=times_id)
|
|
|
|
# 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)
|
|
|
|
# passes: participant_id should be 136, and user_id=28 belongs to participant_id=2
|
|
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)
|
|
return
|
|
|
|
|