"""Tests for custom validations.""" import datetime from abacus_common_logic.constants import error from abacus_common_logic.marshalling.custom_fields import ma OPTIONS = ('Option A', 'Option B', 'Option C') VALID_DATA = { 'test_id': 1, 'money_amount': 100, 'percentage': 99, 'date': '2020-05-20', 'date_time': '2020-05-20T05:10:20.123456-0000', 'nonempty_string': 'valid string', 'enum': 'Option B', 'file_path': 's3://bucket/file.txt', } class MockSchema(ma.Schema): """Ad-hoc schema for testing.""" test_id = ma.IntegerId(required=True) money_amount = ma.MoneyAmount(required=True) percentage = ma.Percentage(required=True) date = ma.FormattedDate(required=True) date_time = ma.FormattedDateTime(required=True) nonempty_string = ma.NonemptyString(required=True) enum = ma.Enum(options=OPTIONS, required=True) file_path = ma.FilePath(required=True) def errors(input_data): """Load errors.""" return MockSchema().validate(input_data) def test_valid_data(): """Test VALID_DATA dict to be valid input.""" assert errors(VALID_DATA) == {} def test_file_path_required(): """Requires a value when required=True.""" assert error.ERROR_FIELD_MISSING in errors({})['file_path'] def test_file_path_must_be_valid(): """The value must be a valid file path.""" assert error.ERROR_INVALID_FILE_PATH in errors({'file_path': ''})['file_path'] assert ( error.ERROR_INVALID_FILE_PATH in errors({'file_path': 's3://bucket'})['file_path'] ) assert ( error.ERROR_INVALID_FILE_PATH in errors({'file_path': 's3://bucket/file'})['file_path'] ) assert ( error.ERROR_INVALID_FILE_PATH in errors({'file_path': 'bucket.txt'})['file_path'] ) def test_integer_id_required(): """Requires a value when required=True.""" assert error.ERROR_FIELD_MISSING in errors({})['test_id'] def test_integer_id_must_be_valid(): """The value must be an integer.""" assert error.ERROR_MUST_BE_INT in errors({'test_id': 'asdf'})['test_id'] def test_percentage_required(): """Requires a value when required=True.""" assert error.ERROR_FIELD_MISSING in errors({})['percentage'] def test_percentage_must_be_in_range(): """Percentages must be between 0 and 100.""" assert ( error.ERROR_PERCENTAGE_NOT_IN_RANGE in errors({'percentage': -12.45})['percentage'] ) def test_percentage_must_be_valid(): """Percentages must be numbers.""" assert error.ERROR_INVALID_DECIMAL in errors({'percentage': 'asdf'})['percentage'] def test_percentage_precision_is_max_2_digits(): """Percentages cannot have more than 2 fractional digits.""" assert error.ERROR_INVALID_DECIMAL in errors({'percentage': 12.455})['percentage'] def test_date_required(): """Requires a value when required=True.""" assert error.ERROR_FIELD_MISSING in errors({})['date'] def test_date_must_be_valid(): """Dates must be dates.""" assert error.ERROR_INVALID_DATE in errors({'date': 'asdf'})['date'] def test_date_must_be_in_correct_format(): """Dates must be in the right format.""" assert error.ERROR_INVALID_DATE in errors({'date': '01-01-2018'})['date'] def test_datetime_must_be_valid(): """Datetime should be valid value error.""" assert ( error.ERROR_INVALID_DATETIME in errors(({'date_time': '2020-05-04 05:05:05'}))['date_time'] ) def test_datetime_loads(): """Datetime should be load properly.""" str_datetime = '2020-05-20T05:10:20.123456+00:00' input_data = {**VALID_DATA, 'date_time': str_datetime} loaded = MockSchema().load(input_data) assert isinstance(loaded['date_time'], datetime.datetime) dumped = MockSchema().dump(loaded) assert dumped['date_time'] == '2020-05-20T05:10:20.123456+0000' str_datetime = '2020-05-20T05:10:20.123456' input_data = {**VALID_DATA, 'date_time': str_datetime} loaded = MockSchema().load(input_data) assert isinstance(loaded['date_time'], datetime.datetime) dumped = MockSchema().dump(loaded) assert dumped['date_time'] == '2020-05-20T05:10:20.123456+0000' str_datetime = '2020-05-20T05:10:20.123456+0400' input_data = {**VALID_DATA, 'date_time': str_datetime} loaded = MockSchema().load(input_data) assert isinstance(loaded['date_time'], datetime.datetime) dumped = MockSchema().dump(loaded) assert dumped['date_time'] == '2020-05-20T05:10:20.123456+0400' def test_invalid_datetime(): """Datetime should return a null for invalid date time.""" str_datetime = '0000-00-00T00:00:00.000000+0000' input_data = {**VALID_DATA, 'date_time': str_datetime} dumped = MockSchema().dump(input_data) assert dumped['date_time'] is None def test_money_amount_precision_is_max_2_digits(): """Money amounts cannot have more than 2 fractional digits.""" assert ( error.ERROR_INVALID_DECIMAL in errors({'money_amount': 12.233})['money_amount'] ) def test_money_amount_can_be_negative(): """Money amounts can be negative.""" assert 'money_amount' not in errors({'money_amount': -12.23}) def test_money_amount_when_required(): """Money amount when required must be present.""" assert error.ERROR_FIELD_MISSING in errors({})['money_amount'] def test_money_amount_must_be_valid(): """Money amount must be a number.""" assert ( error.ERROR_INVALID_DECIMAL in errors({'money_amount': 'asdf'})['money_amount'] ) def test_nonempty_string_required(): """Money amount must be a number.""" assert error.ERROR_FIELD_MISSING in errors({})['nonempty_string'] def test_nonempty_string_cannot_by_empty(): """Money amount must be a number.""" assert ( error.ERROR_FIELD_MISSING in errors({'nonempty_string': ''})['nonempty_string'] ) def test_enum_required(): """Money amount must be a number.""" assert error.ERROR_FIELD_MISSING in errors({})['enum'] def test_enum_invalid_value(): """Money amount must be a number.""" assert ( error.ERROR_INVALID_OPTION.format(choices=', '.join(OPTIONS)) in errors({'enum': 'Option D'})['enum'] ) class MockTruncatedStringSchema(ma.Schema): """Ad-hoc schema for testing TruncatedString field.""" truncated_string = ma.TruncatedString(required=True, metadata={'truncate': 4}) truncated_string_negative = ma.TruncatedString(metadata={'truncate': -1}) truncated_string_zero = ma.TruncatedString(metadata={'truncate': 0}) truncated_string_empty = ma.TruncatedString() VALID_TRUNCATED_STRING_DATA = { 'truncated_string': 'skittles is a dog', 'truncated_string_negative': 'skittles is a dog', 'truncated_string_zero': 'skittles is a dog', 'truncated_string_empty': 'skittles is a dog', } def truncated_string_errors(input_data): """Load errors.""" return MockTruncatedStringSchema().validate(input_data) def test_valid_truncated_string_data(): """Test VALID_TRUNCATED_STRING_DATA dict to be valid input.""" assert truncated_string_errors(VALID_TRUNCATED_STRING_DATA) == {} def test_required_truncated_string(): """Test required param is used by TruncatedString.""" assert truncated_string_errors({}) == { 'truncated_string': [error.ERROR_FIELD_MISSING] } def test_truncated_string_serializes(): """Test serializes truncated string.""" result = MockTruncatedStringSchema().dump(VALID_TRUNCATED_STRING_DATA) assert result == { 'truncated_string': 'skit', 'truncated_string_negative': 'skittles is a dog', 'truncated_string_zero': 'skittles is a dog', 'truncated_string_empty': 'skittles is a dog', }