Skip to content

Commit

Permalink
Merge pull request #119 from sigmavirus24/stronger-validation
Browse files Browse the repository at this point in the history
Stronger validation
  • Loading branch information
sigmavirus24 committed Aug 12, 2016
2 parents 5172166 + 55d0dc0 commit 7a3af9a
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 16 deletions.
10 changes: 10 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
History
=======

Unreleased
----------

- Add ``ValidationError`` and a set of subclasses for each possible validation
error.
- Raise ``InvalidOption`` on unknown cassette options rather than silently
ignoring extra options.
- Raise a subclass of ``ValidationError`` when a particular cassette option is
invalid, rather than silently ignoring the validation failure.

0.7.2 - 2016-08-04
------------------

Expand Down
48 changes: 48 additions & 0 deletions betamax/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,51 @@ def __repr__(self):

class MissingDirectoryError(BetamaxError):
pass


class ValidationError(BetamaxError):
pass


class InvalidOption(ValidationError):
pass


class BodyBytesValidationError(ValidationError):
pass


class MatchersValidationError(ValidationError):
pass


class RecordValidationError(ValidationError):
pass


class RecordIntervalValidationError(ValidationError):
pass


class PlaceholdersValidationError(ValidationError):
pass


class PlaybackRepeatsValidationError(ValidationError):
pass


class SerializerValidationError(ValidationError):
pass


validation_error_map = {
'allow_playback_repeats': PlaybackRepeatsValidationError,
'match_requests_on': MatchersValidationError,
'record': RecordValidationError,
'placeholders': PlaceholdersValidationError,
'preserve_exact_body_bytes': BodyBytesValidationError,
're_record_interval': RecordIntervalValidationError,
'serialize': SerializerValidationError, # TODO: Remove this
'serialize_with': SerializerValidationError
}
13 changes: 8 additions & 5 deletions betamax/options.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .cassette import Cassette
from .exceptions import InvalidOption, validation_error_map


def validate_record(record):
Expand All @@ -19,9 +20,10 @@ def validate_serializer(serializer):
def validate_placeholders(placeholders):
"""Validate placeholders is a dict-like structure"""
keys = ['placeholder', 'replace']
return all(
sorted(list(p.keys())) == keys for p in placeholders
)
try:
return all(sorted(list(p.keys())) == keys for p in placeholders)
except TypeError:
return False


def translate_cassette_options():
Expand Down Expand Up @@ -84,8 +86,9 @@ def items(self):
def validate(self):
for key, value in list(self.data.items()):
if key not in Options.valid_options:
del self[key]
raise InvalidOption('{0} is not a valid option'.format(key))
else:
is_valid = Options.valid_options[key]
if not is_valid(value):
del self[key]
raise validation_error_map[key]('{0!r} is not valid'
.format(value))
8 changes: 8 additions & 0 deletions tests/unit/test_cassette.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import unittest
from datetime import datetime

import pytest

from betamax import __version__
from betamax.cassette import cassette
from betamax import mock_response
Expand Down Expand Up @@ -310,6 +312,12 @@ def test_find_match(self):
assert i is not None
assert self.interaction is i

def test_find_match__missing_matcher(self):
self.cassette.match_options = set(['uri', 'method', 'invalid'])
self.cassette.record_mode = 'none'
with pytest.raises(KeyError):
self.cassette.find_match(self.response.request)

def test_eject(self):
serializer = self.test_serializer
self.cassette.eject()
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import unittest
import inspect

from betamax import exceptions


def exception_classes():
for _, module_object in inspect.getmembers(exceptions):
if inspect.isclass(module_object):
yield module_object


class TestExceptions(unittest.TestCase):
def test_all_exceptions_are_betamax_errors(self):
for exception_class in exception_classes():
assert isinstance(exception_class('msg'), exceptions.BetamaxError)

def test_all_validation_errors_are_in_validation_error_map(self):
validation_error_map_values = exceptions.validation_error_map.values()
for exception_class in exception_classes():
if exception_class.__name__ == 'ValidationError' or \
not exception_class.__name__.endswith('ValidationError'):
continue
assert exception_class in validation_error_map_values

def test_all_validation_errors_are_validation_errors(self):
for exception_class in exception_classes():
if not exception_class.__name__.endswith('ValidationError'):
continue
assert isinstance(exception_class('msg'),
exceptions.ValidationError)

def test_invalid_option_is_validation_error(self):
assert isinstance(exceptions.InvalidOption('msg'),
exceptions.ValidationError)
55 changes: 44 additions & 11 deletions tests/unit/test_options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import unittest
from itertools import permutations

import pytest

from betamax import exceptions
from betamax.options import Options, validate_record, validate_matchers


Expand Down Expand Up @@ -31,21 +35,50 @@ def test_data_is_valid(self):
for key in self.data:
assert key in self.options

def test_invalid_data_is_removed(self):
def test_raise_on_unknown_option(self):
data = self.data.copy()
data['fake'] = 'value'
options = Options(data)
with pytest.raises(exceptions.InvalidOption):
Options(data)

for key in self.data:
assert key in options
def test_raise_on_invalid_body_bytes(self):
data = self.data.copy()
data['preserve_exact_body_bytes'] = None
with pytest.raises(exceptions.BodyBytesValidationError):
Options(data)

def test_raise_on_invalid_matchers(self):
data = self.data.copy()
data['match_requests_on'] = ['foo', 'bar', 'bogus']
with pytest.raises(exceptions.MatchersValidationError):
Options(data)

assert 'fake' not in options
def test_raise_on_invalid_placeholders(self):
data = self.data.copy()
data['placeholders'] = None
with pytest.raises(exceptions.PlaceholdersValidationError):
Options(data)

def test_values_are_validated(self):
assert self.options['re_record_interval'] == 10000
assert self.options['match_requests_on'] == ['method']
def test_raise_on_invalid_playback_repeats(self):
data = self.data.copy()
data['allow_playback_repeats'] = None
with pytest.raises(exceptions.PlaybackRepeatsValidationError):
Options(data)

def test_raise_on_invalid_record(self):
data = self.data.copy()
data['match_requests_on'] = ['foo', 'bar', 'bogus']
options = Options(data)
assert options['match_requests_on'] == ['method', 'uri']
data['record'] = None
with pytest.raises(exceptions.RecordValidationError):
Options(data)

def test_raise_on_invalid_record_interval(self):
data = self.data.copy()
data['re_record_interval'] = -1
with pytest.raises(exceptions.RecordIntervalValidationError):
Options(data)

def test_raise_on_invalid_serializer(self):
data = self.data.copy()
data['serialize_with'] = None
with pytest.raises(exceptions.SerializerValidationError):
Options(data)

0 comments on commit 7a3af9a

Please sign in to comment.