def test_extra_keys_disallowed(self): """ FilterMappers can be configured to treat any extra key as an invalid value. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, # Treat all extra keys as invalid values.s allow_extra_keys = False, ) self.assertFilterErrors( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'extra': [f.FilterMapper.CODE_EXTRA_KEY], }, # The valid fields were still included in the return value, # but the invalid field was removed. expected_value = { 'id': 42, 'subject': 'Hello, world!', } )
def test_fail_mapping(self): """ A FilterRepeater is applied to a dict containing invalid values. """ self.filter_type = lambda: f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.assertFilterErrors( { 'id': None, 'subject': 'Antidisestablishmentarianism', }, { 'id': [f.Required.CODE_EMPTY], 'subject': [f.MaxLength.CODE_TOO_LONG], }, expected_value = { 'id': None, 'subject': None, } )
def test_extra_keys_ordered(self): """ When the filter map is an OrderedDict, extra keys are alphabetized. """ # Note that we pass an OrderedDict to the filter initializer. self.filter_type = lambda: f.FilterMapper(OrderedDict(( ('subject', f.NotEmpty | f.MaxLength(16)), ('id', f.Required | f.Int | f.Min(1)), ))) filter_ = self._filter({ 'id': '42', 'subject': 'Hello, world!', 'cat': 'felix', 'bird': 'phoenix', 'fox': 'fennecs', }) self.assertFilterPasses( filter_, OrderedDict(( # The filtered keys are always listed first. ('subject', 'Hello, world!'), ('id', 42), # Extra keys are listed afterward, in alphabetical # order. ('bird', 'phoenix'), ('cat', 'felix'), ('fox', 'fennecs'), )), )
def test_missing_keys_specified(self): """ FilterMappers can be configured to allow some missing keys but not others. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, allow_missing_keys={'subject'}, ) # The FilterMapper is configured to treat missing 'subject' as # if it were set to `None`. self.assertFilterPasses( {'id': '42'}, { 'id': 42, 'subject': None, }, ) # However, 'id' is still required. self.assertFilterErrors({ 'subject': 'Hello, world!', }, { 'id': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value={ 'id': None, 'subject': 'Hello, world!', })
def test_missing_keys_allowed(self): """ By default, FilterMappers treat missing keys as `None`. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) # 'subject' allows null values, so no errors are generated. self.assertFilterPasses( { 'id': '42', }, { 'id': 42, 'subject': None, }, ) # However, 'id' has Required in its FilterChain, so a missing # 'id' is still an error. self.assertFilterErrors( { 'subject': 'Hello, world!', }, { 'id': [f.Required.CODE_EMPTY], }, expected_value={ 'id': None, 'subject': 'Hello, world!', }, )
def test_pass_mapping(self): """ A FilterRepeater is applied to a dict containing valid values. """ self.filter_type = lambda: f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) filter_ = self._filter({ 'id': '42', 'subject': 'Hello, world!', }) self.assertFilterPasses( filter_, { 'id': 42, 'subject': 'Hello, world!', }, ) # The result is a dict, to match the type of the filter map. self.assertIs(type(filter_.cleaned_data), dict)
def test_missing_keys_disallowed(self): """ FilterMappers can be configured to treat missing keys as invalid values. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, # Treat missing keys as invalid values. allow_missing_keys = False, ) self.assertFilterErrors( {}, { 'id': [f.FilterMapper.CODE_MISSING_KEY], 'subject': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value = { 'id': None, 'subject': None, }, )
def test_pass_ordered_mapping(self): """ Configuring the FilterRepeater to return an OrderedDict. """ # Note that we pass an OrderedDict to the filter initializer. self.filter_type = lambda: f.FilterMapper(OrderedDict(( ('subject', f.NotEmpty | f.MaxLength(16)), ('id', f.Required | f.Int | f.Min(1)), ))) filter_ = self._filter({ 'id': '42', 'subject': 'Hello, world!', }) self.assertFilterPasses( filter_, OrderedDict(( ('subject', 'Hello, world!'), ('id', 42), )), ) # The result is an OrderedDict, to match the type of the filter # map. self.assertIs(type(filter_.cleaned_data), OrderedDict)
def test_mapper_chained_with_mapper(self): """ Chaining two FilterMappers together has basically the same effect as combining their Filters. Generally, combining two FilterMappers into a single instance is much easier to read/maintain than chaining them, but in a few cases it may be unavoidable (for example, if you need each FilterMapper to handle extra and/or missing keys differently). """ fm1 = f.FilterMapper( { 'id': f.Int | f.Min(1), }, allow_missing_keys=True, allow_extra_keys=True, ) fm2 = f.FilterMapper( { 'id': f.Required | f.Max(256), 'subject': f.NotEmpty | f.MaxLength(16), }, allow_missing_keys=False, allow_extra_keys=False, ) self.filter_type = lambda: fm1 | fm2 self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', }, { 'id': 42, 'subject': 'Hello, world!', }, ) self.assertFilterErrors( {}, { # ``fm1`` allows missing keys, so it sets 'id' to # ``None``. # However, ``fm2`` does not allow ``None`` for 'id' # (because of the ``Required`` filter). 'id': [f.Required.CODE_EMPTY], # `fm1` does not care about `subject`, but `fm2` # expects it to be there. 'subject': [f.FilterMapper.CODE_MISSING_KEY], }, expected_value={ 'id': None, 'subject': None, }, )
def test_fail_non_mapping(self): """The incoming value is not a mapping.""" self.filter_type = lambda: f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.assertFilterErrors( # Nope; it's gotta be an explicit mapping. (('id', '42'), ('subject', 'Hello, world!')), [f.Type.CODE_WRONG_TYPE], )
def test_extra_keys_specified(self): """ FilterMappers can be configured only to allow certain extra keys. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }, allow_extra_keys = {'message', 'extra'}, ) # As long as the extra keys are in the FilterMapper's # ``allow_extra_keys`` setting, everything is fine. self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'id': 42, 'subject': 'Hello, world!', 'extra': 'ignored', }, ) # But, add a key that isn't in ``allow_extra_keys``, and you've # got a problem. self.assertFilterErrors( { 'id': '42', 'subject': 'Hello, world!', 'attachment': { 'type': 'image/jpeg', 'data': '...', }, }, { 'attachment': [f.FilterMapper.CODE_EXTRA_KEY], }, expected_value = { 'id': 42, 'subject': 'Hello, world!', } )
def test_filter_mapper_chained_with_filter(self): """ Chaining a Filter with a FilterMapper causes the chained Filter to operate on the entire mapping. """ fm = f.FilterMapper({ 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.filter_type = lambda: fm | f.MaxLength(3) self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'id': 42, 'subject': 'Hello, world!', 'extra': 'ignored', }, ) self.assertFilterErrors( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', 'attachment': None, }, # The incoming value has 4 items, which fails the MaxLength # filter. [f.MaxLength.CODE_TOO_LONG], )
def test_stop_after_invalid_value(self): """ A FilterChain stops processing the incoming value after any filter fails. """ # This FilterChain will pretty much reject anything that you # throw at it. self.filter_type =\ lambda: f.MaxLength(3) | f.MinLength(8) | f.Required # Note that the value 'foobar' fails both the MaxLength and the # MinLength filters, but the FilterChain stops processing # after MaxLength fails. self.assertFilterErrors('foobar', [f.MaxLength.CODE_TOO_LONG])
def test_extra_keys_allowed(self): """ By default, FilterMappers passthru extra keys. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), }) self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'extra': 'ignored', }, { 'id': 42, 'subject': 'Hello, world!', 'extra': 'ignored', })
def test_repeater_chained_with_filter(self): """ Chaining a Filter with a FilterRepeater causes the chained Filter to operate on the entire collection. """ # This chain will apply NotEmpty to every item in the # collection, and then apply MaxLength to the collection as a # whole. self.filter_type =\ lambda: f.FilterRepeater(f.NotEmpty) | f.MaxLength(2) # The collection has a length of 2, so it passes the MaxLength # filter. self.assertFilterPasses(['foo', 'bar']) # The collection has a length of 3, so it fails the MaxLength # filter. self.assertFilterErrors( ['a', 'b', 'c'], [f.MaxLength.CODE_TOO_LONG], )
def test_mapperception(self): """ Want to filter dicts that contain other dicts? We need to go deeper. """ self.filter_type = lambda: f.FilterMapper( { 'id': f.Required | f.Int | f.Min(1), 'subject': f.NotEmpty | f.MaxLength(16), 'attachment': f.FilterMapper( { 'type': f.Required | f.Choice(choices={'image/jpeg', 'image/png'}), 'data': f.Required | f.Base64Decode, }, allow_extra_keys = False, allow_missing_keys = False, ) }, allow_extra_keys = False, allow_missing_keys = False, ) # Valid mapping is valid. self.assertFilterPasses( { 'id': '42', 'subject': 'Hello, world!', 'attachment': { 'type': 'image/jpeg', 'data': b'R0lGODlhDwAPAKECAAAAzMzM/////wAAACwAAAAAD' b'wAPAAACIISPeQHsrZ5ModrLlN48CXF8m2iQ3YmmKq' b'VlRtW4MLwWACH+EVRIRSBDQUtFIElTIEEgTElFOw==', }, }, { 'id': 42, 'subject': 'Hello, world!', 'attachment': { 'type': 'image/jpeg', 'data': b'GIF89a\x0f\x00\x0f\x00\xa1\x02\x00\x00\x00' b'\xcc\xcc\xcc\xff\xff\xff\xff\x00\x00\x00,\x00' b'\x00\x00\x00\x0f\x00\x0f\x00\x00\x02 \x84\x8f' b'y\x01\xec\xad\x9eL\xa1\xda\xcb\x94\xde<\tq|' b'\x9bh\x90\xdd\x89\xa6*\xa5eF\xd5\xb80\xbc\x16' b'\x00!\xfe\x11THE CAKE IS A LIE;' }, }, ) # Invalid mapping... not so much. self.assertFilterErrors( { 'id': 'NaN', 'attachment': { 'type': 'foo', 'data': False, }, }, { # The error keys are the dotted paths to the invalid # values. # This way, we don't have to deal with nested dicts # when processing error codes. 'id': [f.Decimal.CODE_NON_FINITE], 'subject': [f.FilterMapper.CODE_MISSING_KEY], 'attachment.type': [f.Choice.CODE_INVALID], 'attachment.data': [f.Type.CODE_WRONG_TYPE], }, # The resulting value has the expected structure, but it's # a ghost town. expected_value = { 'id': None, 'subject': None, 'attachment': { 'type': None, 'data': None, }, }, )
def test_repeaterception(self): """ FilterRepeaters can contain other FilterRepeaters. """ self.filter_type = lambda: ( # Apply the following filters to each item in the incoming # value: f.FilterRepeater( # 1. It must be a list. f.Type(list) # 2. Apply the Int filter to each of its items. | f.FilterRepeater(f.Int) # 3. It must have a length <= 3. | f.MaxLength(3) ) ) self.assertFilterPasses( # # Note that the INCOMING VALUE ITSELF does not have to be a # list, nor does it have to have a max length <= 3. # # These Filters are applied to the items INSIDE THE # INCOMING VALUE (because of the outer FilterRepeater). # { 'foo': ['1', '2', '3'], 'bar': [-20, 20], 'baz': ['486'], 'luhrmann': [None, None, None], }, { 'foo': [1, 2, 3], 'bar': [-20, 20], 'baz': [486], 'luhrmann': [None, None, None], }, ) # The 1st item in this value is not a list, so it fails. self.assertFilterErrors( [ [42], {'arch': 486}, ], { '1': [f.Type.CODE_WRONG_TYPE], }, expected_value = [[42], None], ) # The 1st item in this value contains invalid ints. self.assertFilterErrors( [ [42], ['NaN', 3.14, 'FOO'], ], { # # The error keys are the dotted paths to the invalid # values (in this case, they are numeric because we # are working with lists). # # This way, we don't have to deal with nested dicts # when processing error codes. # '1.0': [f.Decimal.CODE_NON_FINITE], '1.1': [f.Int.CODE_DECIMAL], '1.2': [f.Decimal.CODE_INVALID], }, expected_value = [[42], [None, None, None]], ) # The 1st item in this value is too long. self.assertFilterErrors( [ [42], [1, 2, 3, 4] ], { '1': [f.MaxLength.CODE_TOO_LONG], }, expected_value = [[42], None] )