adapting shipcall, times and user to include ValidationError (marshmallow). Adjusting the Schemas for User, Times and Shipcall to be validated with additional input validators. Creating a set of tests for the input validations.
This commit is contained in:
parent
39fbe67938
commit
3f08c342c7
8
brecal.code-workspace
Normal file
8
brecal.code-workspace
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from webargs.flaskparser import parser
|
from webargs.flaskparser import parser
|
||||||
from marshmallow import Schema, fields
|
from marshmallow import Schema, fields, ValidationError
|
||||||
from ..schemas import model
|
from ..schemas import model
|
||||||
from .. import impl
|
from .. import impl
|
||||||
from ..services.auth_guard import auth_guard
|
from ..services.auth_guard import auth_guard
|
||||||
@ -40,6 +40,12 @@ def PostShipcalls():
|
|||||||
try:
|
try:
|
||||||
content = request.get_json(force=True)
|
content = request.get_json(force=True)
|
||||||
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
||||||
|
|
||||||
|
except ValidationError as ex:
|
||||||
|
logging.error(ex)
|
||||||
|
print(ex)
|
||||||
|
return json.dumps(f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"), 400
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
print(ex)
|
print(ex)
|
||||||
@ -56,6 +62,12 @@ def PutShipcalls():
|
|||||||
content = request.get_json(force=True)
|
content = request.get_json(force=True)
|
||||||
logging.info(content)
|
logging.info(content)
|
||||||
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
||||||
|
|
||||||
|
except ValidationError as ex:
|
||||||
|
logging.error(ex)
|
||||||
|
print(ex)
|
||||||
|
return json.dumps(f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"), 400
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
print(ex)
|
print(ex)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from .. import impl
|
|||||||
from ..services.auth_guard import auth_guard
|
from ..services.auth_guard import auth_guard
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
bp = Blueprint('times', __name__)
|
bp = Blueprint('times', __name__)
|
||||||
|
|
||||||
@ -29,6 +30,11 @@ def PostTimes():
|
|||||||
# print (content)
|
# print (content)
|
||||||
# body = parser.parse(schema, request, location='json')
|
# body = parser.parse(schema, request, location='json')
|
||||||
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
||||||
|
|
||||||
|
except ValidationError as ex:
|
||||||
|
logging.error(ex)
|
||||||
|
print(ex)
|
||||||
|
return json.dumps(f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"), 400
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
@ -45,6 +51,11 @@ def PutTimes():
|
|||||||
try:
|
try:
|
||||||
content = request.get_json(force=True)
|
content = request.get_json(force=True)
|
||||||
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
||||||
|
|
||||||
|
except ValidationError as ex:
|
||||||
|
logging.error(ex)
|
||||||
|
print(ex)
|
||||||
|
return json.dumps(f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"), 400
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from .. import impl
|
|||||||
from ..services.auth_guard import auth_guard
|
from ..services.auth_guard import auth_guard
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
bp = Blueprint('user', __name__)
|
bp = Blueprint('user', __name__)
|
||||||
|
|
||||||
@ -14,6 +15,11 @@ def PutUser():
|
|||||||
try:
|
try:
|
||||||
content = request.get_json(force=True)
|
content = request.get_json(force=True)
|
||||||
loadedModel = model.UserSchema().load(data=content, many=False, partial=True)
|
loadedModel = model.UserSchema().load(data=content, many=False, partial=True)
|
||||||
|
|
||||||
|
except ValidationError as ex:
|
||||||
|
logging.error(ex)
|
||||||
|
print(ex)
|
||||||
|
return json.dumps(f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"), 400
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from dataclasses import field, dataclass
|
from dataclasses import field, dataclass
|
||||||
from marshmallow import Schema, fields, post_load, INCLUDE, ValidationError
|
from marshmallow import Schema, fields, INCLUDE, ValidationError, validate, validates
|
||||||
from marshmallow.fields import Field
|
from marshmallow.fields import Field
|
||||||
from marshmallow_enum import EnumField
|
from marshmallow_enum import EnumField
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
@ -9,6 +9,7 @@ from typing import List
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
|
from BreCal.validators.time_logic import validate_time_exceeds_threshold
|
||||||
|
|
||||||
def obj_dict(obj):
|
def obj_dict(obj):
|
||||||
if isinstance(obj, datetime.datetime):
|
if isinstance(obj, datetime.datetime):
|
||||||
@ -152,34 +153,34 @@ class ParticipantList(Participant):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class ParticipantAssignmentSchema(Schema):
|
class ParticipantAssignmentSchema(Schema):
|
||||||
participant_id = fields.Int()
|
participant_id = fields.Integer()
|
||||||
type = fields.Int()
|
type = fields.Integer()
|
||||||
|
|
||||||
class ShipcallSchema(Schema):
|
class ShipcallSchema(Schema):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(unknown=None)
|
super().__init__(unknown=None)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
id = fields.Int()
|
id = fields.Integer()
|
||||||
ship_id = fields.Int()
|
ship_id = fields.Integer()
|
||||||
type = fields.Enum(ShipcallType, required=True)
|
type = fields.Integer()
|
||||||
eta = fields.DateTime(Required = False, allow_none=True)
|
eta = fields.DateTime(Required = False, allow_none=True)
|
||||||
voyage = fields.Str(allow_none=True, metadata={'Required':False}) # Solving: RemovedInMarshmallow4Warning: Passing field metadata as keyword arguments is deprecated. Use the explicit `metadata=...` argument instead. Additional metadata: {'Required': False}
|
voyage = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(max=16)]) # Solving: RemovedInMarshmallow4Warning: Passing field metadata as keyword arguments is deprecated. Use the explicit `metadata=...` argument instead. Additional metadata: {'Required': False}
|
||||||
etd = fields.DateTime(Required = False, allow_none=True)
|
etd = fields.DateTime(Required = False, allow_none=True)
|
||||||
arrival_berth_id = fields.Int(Required = False, allow_none=True)
|
arrival_berth_id = fields.Integer(Required = False, allow_none=True)
|
||||||
departure_berth_id = fields.Int(Required = False, allow_none=True)
|
departure_berth_id = fields.Integer(Required = False, allow_none=True)
|
||||||
tug_required = fields.Bool(Required = False, allow_none=True)
|
tug_required = fields.Bool(Required = False, allow_none=True)
|
||||||
pilot_required = fields.Bool(Required = False, allow_none=True)
|
pilot_required = fields.Bool(Required = False, allow_none=True)
|
||||||
flags = fields.Int(Required = False, allow_none=True)
|
flags = fields.Integer(Required = False, allow_none=True)
|
||||||
pier_side = fields.Bool(Required = False, allow_none=True)
|
pier_side = fields.Bool(Required = False, allow_none=True)
|
||||||
bunkering = fields.Bool(Required = False, allow_none=True)
|
bunkering = fields.Bool(Required = False, allow_none=True)
|
||||||
replenishing_terminal = fields.Bool(Required = False, allow_none=True)
|
replenishing_terminal = fields.Bool(Required = False, allow_none=True)
|
||||||
replenishing_lock = fields.Bool(Required = False, allow_none=True)
|
replenishing_lock = fields.Bool(Required = False, allow_none=True)
|
||||||
draft = fields.Float(Required = False, allow_none=True)
|
draft = fields.Float(Required = False, allow_none=True, validate=[validate.Range(min=0, max=20, min_inclusive=False, max_inclusive=True)])
|
||||||
tidal_window_from = fields.DateTime(Required = False, allow_none=True)
|
tidal_window_from = fields.DateTime(Required = False, allow_none=True)
|
||||||
tidal_window_to = fields.DateTime(Required = False, allow_none=True)
|
tidal_window_to = fields.DateTime(Required = False, allow_none=True)
|
||||||
rain_sensitive_cargo = fields.Bool(Required = False, allow_none=True)
|
rain_sensitive_cargo = fields.Bool(Required = False, allow_none=True)
|
||||||
recommended_tugs = fields.Int(Required = False, allow_none=True)
|
recommended_tugs = fields.Integer(Required = False, allow_none=True)
|
||||||
anchored = fields.Bool(Required = False, allow_none=True)
|
anchored = fields.Bool(Required = False, allow_none=True)
|
||||||
moored_lock = fields.Bool(Required = False, allow_none=True)
|
moored_lock = fields.Bool(Required = False, allow_none=True)
|
||||||
canceled = fields.Bool(Required = False, allow_none=True)
|
canceled = fields.Bool(Required = False, allow_none=True)
|
||||||
@ -297,12 +298,13 @@ class ShipcallId(Schema):
|
|||||||
|
|
||||||
# this is the way!
|
# this is the way!
|
||||||
|
|
||||||
|
|
||||||
class TimesSchema(Schema):
|
class TimesSchema(Schema):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(unknown=None)
|
super().__init__(unknown=None)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
id = fields.Int(Required=False)
|
id = fields.Integer(Required=False)
|
||||||
eta_berth = fields.DateTime(Required = False, allow_none=True)
|
eta_berth = fields.DateTime(Required = False, allow_none=True)
|
||||||
eta_berth_fixed = fields.Bool(Required = False, allow_none=True)
|
eta_berth_fixed = fields.Bool(Required = False, allow_none=True)
|
||||||
etd_berth = fields.DateTime(Required = False, allow_none=True)
|
etd_berth = fields.DateTime(Required = False, allow_none=True)
|
||||||
@ -313,13 +315,13 @@ class TimesSchema(Schema):
|
|||||||
zone_entry_fixed = fields.Bool(Required = False, allow_none=True)
|
zone_entry_fixed = fields.Bool(Required = False, allow_none=True)
|
||||||
operations_start = fields.DateTime(Required = False, allow_none=True)
|
operations_start = fields.DateTime(Required = False, allow_none=True)
|
||||||
operations_end = fields.DateTime(Required = False, allow_none=True)
|
operations_end = fields.DateTime(Required = False, allow_none=True)
|
||||||
remarks = fields.String(Required = False, allow_none=True)
|
remarks = fields.String(Required = False, allow_none=True, validate=[validate.Length(max=256)])
|
||||||
participant_id = fields.Int(Required = True)
|
participant_id = fields.Integer(Required = True)
|
||||||
berth_id = fields.Int(Required = False, allow_none = True)
|
berth_id = fields.Integer(Required = False, allow_none = True)
|
||||||
berth_info = fields.String(Required = False, allow_none=True)
|
berth_info = fields.String(Required = False, allow_none=True, validate=[validate.Length(max=256)])
|
||||||
pier_side = fields.Bool(Required = False, allow_none = True)
|
pier_side = fields.Bool(Required = False, allow_none = True)
|
||||||
shipcall_id = fields.Int(Required = True)
|
shipcall_id = fields.Integer(Required = True)
|
||||||
participant_type = fields.Int(Required = False, allow_none=True)
|
participant_type = fields.Integer(Required = False, allow_none=True)
|
||||||
ata = fields.DateTime(Required = False, allow_none=True)
|
ata = fields.DateTime(Required = False, allow_none=True)
|
||||||
atd = fields.DateTime(Required = False, allow_none=True)
|
atd = fields.DateTime(Required = False, allow_none=True)
|
||||||
eta_interval_end = fields.DateTime(Required = False, allow_none=True)
|
eta_interval_end = fields.DateTime(Required = False, allow_none=True)
|
||||||
@ -327,19 +329,38 @@ class TimesSchema(Schema):
|
|||||||
created = fields.DateTime(Required = False, allow_none=True)
|
created = fields.DateTime(Required = False, allow_none=True)
|
||||||
modified = fields.DateTime(Required = False, allow_none=True)
|
modified = fields.DateTime(Required = False, allow_none=True)
|
||||||
|
|
||||||
|
@validates("eta_berth")
|
||||||
|
def validate_eta_berth(self, value):
|
||||||
|
threshold_exceeded = validate_time_exceeds_threshold(value, months=12)
|
||||||
|
print(threshold_exceeded, value)
|
||||||
|
if threshold_exceeded:
|
||||||
|
raise ValidationError(f"the provided time exceeds the twelve month threshold.")
|
||||||
|
|
||||||
# deserialize PUT object target
|
# deserialize PUT object target
|
||||||
|
|
||||||
class UserSchema(Schema):
|
class UserSchema(Schema):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(unknown=None)
|
super().__init__(unknown=None)
|
||||||
pass
|
pass
|
||||||
id = fields.Int(required=True)
|
id = fields.Integer(required=True)
|
||||||
first_name = fields.Str(allow_none=True, metadata={'Required':False})
|
first_name = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(max=64)])
|
||||||
last_name = fields.Str(allow_none=True, metadata={'Required':False})
|
last_name = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(max=64)])
|
||||||
user_phone = fields.Str(allow_none=True, metadata={'Required':False})
|
user_phone = fields.String(allow_none=True, metadata={'Required':False})
|
||||||
user_email = fields.Str(allow_none=True, metadata={'Required':False})
|
user_email = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(max=64)])
|
||||||
old_password = fields.Str(allow_none=True, metadata={'Required':False})
|
old_password = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(max=128)])
|
||||||
new_password = fields.Str(allow_none=True, metadata={'Required':False})
|
new_password = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(min=6, max=128)])
|
||||||
|
|
||||||
|
@validates("user_phone")
|
||||||
|
def validate_user_phone(self, value):
|
||||||
|
valid_characters = list(map(str,range(0,10)))+["+", " "]
|
||||||
|
if not all([v in valid_characters for v in value]):
|
||||||
|
raise ValidationError(f"one of the phone number values is not valid.")
|
||||||
|
|
||||||
|
@validates("user_email")
|
||||||
|
def validate_user_email(self, value):
|
||||||
|
if not "@" in value:
|
||||||
|
raise ValidationError(f"invalid email address")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Times:
|
class Times:
|
||||||
|
|||||||
80
src/server/tests/schemas/test_model.py
Normal file
80
src/server/tests/schemas/test_model.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from marshmallow import ValidationError
|
||||||
|
import pytest
|
||||||
|
from BreCal.schemas.model import ShipcallSchema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function") # function: destroy fixture at the end of each test
|
||||||
|
def prepare_shipcall_content():
|
||||||
|
import datetime
|
||||||
|
from BreCal.stubs.shipcall import get_shipcall_simple
|
||||||
|
shipcall_stub = get_shipcall_simple()
|
||||||
|
content = shipcall_stub.__dict__
|
||||||
|
content["participants"] = []
|
||||||
|
content = {k:v.isoformat() if isinstance(v, datetime.datetime) else v for k,v in content.items()}
|
||||||
|
return locals()
|
||||||
|
|
||||||
|
def test_shipcall_input_validation_draft(prepare_shipcall_content):
|
||||||
|
content = prepare_shipcall_content["content"]
|
||||||
|
content["draft"] = 24.11
|
||||||
|
|
||||||
|
schemaModel = ShipcallSchema()
|
||||||
|
with pytest.raises(ValidationError, match="Must be greater than 0 and less than or equal to 20."):
|
||||||
|
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_shipcall_input_validation_voyage(prepare_shipcall_content):
|
||||||
|
content = prepare_shipcall_content["content"]
|
||||||
|
content["voyage"] = "".join(list(map(str,list(range(0,24))))) # 38 characters
|
||||||
|
|
||||||
|
schemaModel = ShipcallSchema()
|
||||||
|
with pytest.raises(ValidationError, match="Longer than maximum length "):
|
||||||
|
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function") # function: destroy fixture at the end of each test
|
||||||
|
def prepare_user_content():
|
||||||
|
import datetime
|
||||||
|
from BreCal.stubs.user import get_user_simple
|
||||||
|
from BreCal.schemas.model import UserSchema
|
||||||
|
schemaModel = UserSchema()
|
||||||
|
|
||||||
|
user_stub = get_user_simple()
|
||||||
|
content = user_stub.__dict__
|
||||||
|
content = {k:v.isoformat() if isinstance(v, datetime.datetime) else v for k,v in content.items()}
|
||||||
|
content = {k:v for k,v in content.items() if k in list(schemaModel.fields.keys())}
|
||||||
|
content["old_password"] = "myfavoritedog123"
|
||||||
|
content["new_password"] = "SecuRepassW0rd!"
|
||||||
|
return locals()
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_validation_berth_phone_number_is_valid(prepare_user_content):
|
||||||
|
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||||
|
content["user_phone"] = "+49123 45678912" # whitespace and + are valid
|
||||||
|
|
||||||
|
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_input_validation_berth_phone_number_is_invalid(prepare_user_content):
|
||||||
|
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||||
|
content["user_phone"] = "+49123 45678912!" # ! is invalid
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="one of the phone number values is not valid."):
|
||||||
|
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_input_validation_new_password_too_short(prepare_user_content):
|
||||||
|
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||||
|
content["new_password"] = "1234" # must have between 6 and 128 characters
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Length must be between 6 and 128."):
|
||||||
|
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def test_input_validation_user_email_invalid(prepare_user_content):
|
||||||
|
content, schemaModel = prepare_user_content["content"], prepare_user_content["schemaModel"]
|
||||||
|
content["user_email"] = "userbrecal.com" # forgot @ -> invalid
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="invalid email address"):
|
||||||
|
loadedModel = schemaModel.load(data=content, many=False, partial=True)
|
||||||
|
return
|
||||||
Reference in New Issue
Block a user