Source code for subui.validators
from __future__ import print_function, unicode_literals
import inspect
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import Resolver404, resolve
from django.template.response import SimpleTemplateResponse
from six.moves.urllib_parse import urlsplit, urlunsplit
[docs]class BaseValidator(object):
"""
Base validator which should be sub-classed to create
custom validators.
:var tuple BaseValidator.expected_attrs: Required attributes for the
validator. :py:meth:`_check_improper_configuration`
will verify that all of these attributes are defined.
.. note:: Each validator should only define required
attributes for itself. :py:meth:`_get_expected_attrs`
will automatically return required attributes
from all current validator and base classes.
:param test_step: :py:class:`subui.step.TestStep`
instance which will be used to make assertions on.
.. note:: This parameter is not really required in the
``__init__`` because the same step will be passed
in :py:meth:`test` however is useful in ``__init__``
in case subclass validator needs to apply custom
login according to values from the ``step``.
"""
expected_attrs = None
def __init__(self, test_step=None, **kwargs):
self.step = test_step
self.__dict__.update(**kwargs)
def _get_expected_attrs(self):
"""
Get all required/expected attributes as defined by
:py:attr:`expected_attrs` from all current and base
classes.
For example::
class Validator1(BaseValidator):
expected_attr = ('foo', 'bar')
class Validator2(Validator1):
expected_attr = ('hello', 'world')
> validator = Validator2()
> validator._get_expected_attrs()
['foo', 'bar', 'hello', 'world']
:rtype: list
"""
bases = inspect.getmro(self.__class__)
attrs = set()
for base in bases:
attrs |= set(getattr(base, 'expected_attrs', None) or set())
return list(sorted(attrs))
def _check_improper_configuration(self):
"""
Check that the validator is configured correctly
by verifying that all attributes as returned by
:py:meth:`_get_expected_attrs` are defined.
:raises ImproperlyConfigured: If any of the required
attributes are not defined.
"""
for attr in self._get_expected_attrs():
if not getattr(self, attr, None):
msg = '{} requires to define {}'
raise ImproperlyConfigured(
msg.format(self.__class__.__name__, attr)
)
def _get_base_error_message(self):
"""
Get base error message which will be used in assertions
"""
msg = 'Response for {{{key}:{step}}} requesting "{url}"'
msg = msg.format(
key=self.step.step_key,
step=self.step.__class__.__name__,
url=self.step.get_url(),
)
return msg
[docs] def test(self, test_step):
"""
Test the step's server response by making
all the necessary assertions. This method
by default saves the ``test_step`` parameter
into :py:attr:`step` and validates the validator
by using :py:meth:`_check_improper_configuration`.
All subclass validators should actually implement
assertions in this method.
:param test_step: Test step
"""
self.step = test_step
self._check_improper_configuration()
[docs]class HeaderValidator(BaseValidator):
"""
Validator which can check that a particular header
is returned and that it is of particular value.
:var str HeaderValidator.header_name: Name of the header which
must be returned
:var str expected_header: Expected header value
to be returned
:var bool HeaderValidator.test_header_value: Whether to test the
header value or simply check its existence
:var test_contains_value: Whether to test if the
header value contains another value, or simply
check equality to that value
"""
expected_attrs = ('header_name', 'expected_header',)
header_name = None
expected_header = None
test_header_value = True
test_contains_value = False
def _get_expected_attrs(self):
"""
Get all expected attributes except remove "expected_header"
if :py:attr:`test_header_value` is ``False``.
"""
attrs = super(HeaderValidator, self)._get_expected_attrs()
if not self.test_header_value:
attrs.remove('expected_header')
return attrs
[docs] def test(self, test_step):
"""
Test the response returned with
:py:attr:header_name header and that its value
is equal to :py:attr:expected_header if
:py:attr:test_header_value is True.
If :py:attr:test_contains_value is True,
header value will be tested to contain expected value.
"""
super(HeaderValidator, self).test(test_step)
if self.test_contains_value:
self.test_header_value = False
self.step.test.assertIn(
self.header_name,
self.step.response,
'{} must contain {} header'
''.format(self._get_base_error_message(),
self.header_name)
)
if self.test_header_value:
self.test_contains_value = False
self.step.test.assertEqual(
self.step.response[self.header_name],
self.expected_header,
'{} returned header {} with value {} != {}'
''.format(self._get_base_error_message(),
self.header_name,
self.step.response[self.header_name],
self.expected_header)
)
if self.test_contains_value:
self.step.test.assertIn(
self.expected_header,
self.step.response[self.header_name],
'{} returned header {} with value {} which doesnt contain {}'
''.format(self._get_base_error_message(),
self.header_name,
self.step.response[self.header_name],
self.expected_header)
)
[docs]class HeaderContentTypeValidator(HeaderValidator):
"""
Validator to check that the expected
"Content-Type" header is returned.
"""
header_name = 'Content-Type'
[docs]class HeaderLocationValidator(HeaderValidator):
"""
Validator to check that the redirect
"Location" header is returned
"""
header_name = 'Location'
[docs]class StatusCodeValidator(BaseValidator):
"""
Validator which allows to verify the returned
server status code such as "OK-200" or "Redirect-302", etc.
:var int StatusCodeValidator.expected_status_code: Expected status code to be
returned by the server
"""
expected_attrs = ('expected_status_code',)
expected_status_code = None
[docs] def test(self, test_step):
"""
Test that the response status code
matched expected status code.
"""
super(StatusCodeValidator, self).test(test_step)
self.step.test.assertEqual(
self.step.response.status_code,
self.expected_status_code,
'{} returned with status code {} != {}'
''.format(self._get_base_error_message(),
self.step.response.status_code,
self.expected_status_code)
)
[docs]class StatusCodeCreatedValidator(StatusCodeValidator):
"""
Validator to check that the returned status
code is created - 201
"""
expected_status_code = 201
[docs]class StatusCodeOkValidator(StatusCodeValidator):
"""
Validator to check that the returned status
code is OK - 200
"""
expected_status_code = 200
[docs]class StatusCodeRedirectValidator(HeaderLocationValidator, StatusCodeValidator):
"""
Validator to check that the server returns a redirect
with the "Location" header defined.
"""
expected_status_code = 302
test_header_value = False
[docs]class RedirectToRouteValidator(StatusCodeRedirectValidator):
"""
Validator which also checks that the server returns
a redirect to an expected Django route.
:var str expected_route_name: Route name to which
the server should redirect to
"""
expected_attrs = ('expected_route_name',)
expected_route_name = None
[docs] def test(self, test_step):
"""
Test the response by additionally testing
that the response redirects to an expected
route as defined by :py:attr:`expected_route_name`.
"""
super(RedirectToRouteValidator, self).test(test_step)
location = self.step.response['Location']
# remove schema, query string and host from URL. Since query string need to be removed to properly resolve that url
location = urlunsplit(('', '', urlsplit(location).path, '', ''))
try:
redirected_to_route = resolve(location).view_name
except Resolver404:
msg = '{} returned a redirect to "{}" which cannot be resolved'
self.step.test.fail(msg.format(self._get_base_error_message(),
location))
self.step.test.assertEqual(
redirected_to_route,
self.expected_route_name,
'{} returned redirect to route {} != {}'
''.format(self._get_base_error_message(),
redirected_to_route,
self.expected_route_name)
)
[docs]class ResponseContentContainsValidator(StatusCodeOkValidator):
"""
Validator which also checks that returned response
content contains expect string.
:var str expected_content: Expected string in the
server response
"""
expected_attrs = ('expected_content',)
expected_content = None
[docs] def test(self, test_step):
"""
Test the response by additionally testing
that the response context contains expected
string as defined by :py:attr:`expected_content`.
"""
super(ResponseContentContainsValidator, self).test(test_step)
self.step.test.assertIn(
self.expected_content,
# need to decode since content is binary
self.step.response.content.decode('utf-8'),
'{} does not contain {!r} in its content'
''.format(self._get_base_error_message(),
self.expected_content)
)
[docs]class ResponseContentNotContainsValidator(StatusCodeOkValidator):
"""
Validator checks that returned response
content does not contain unexpected string.
:var str unexpected_content: Unexpected string in the
server response
"""
expected_attrs = ('unexpected_content',)
unexpected_content = None
[docs] def test(self, test_step):
"""
Test the response by additionally testing
that the response context does not contain the unexpected
string as defined by :py:attr:`unexpected_content`.
"""
super(ResponseContentNotContainsValidator, self).test(test_step)
self.step.test.assertNotIn(
self.unexpected_content,
# need to decode since content is binary
self.step.response.content.decode('utf-8'),
'UnexpectedContentFound: {} contains {!r} in its content. '
''.format(self._get_base_error_message(),
self.unexpected_content)
)
[docs]class SessionDataValidator(BaseValidator):
"""
Validator which allows to verify the data in the session based on
session key.
:var str expected_session_key: Expected session key to be present in session
:var list expected_session_secondary_keys: List of Expected session key to be present in session
"""
expected_attrs = ('expected_session_key',)
expected_session_key = None
expected_session_secondary_keys = []
[docs] def test(self, test_step):
"""
Test that expected session key is present. If expected session data provided,
ensure the expected session key data matches what is there currently.
"""
super(SessionDataValidator, self).test(test_step)
self.step.test.assertIn(
self.expected_session_key,
self.step.response.wsgi_request.session.keys(),
'{} does not contain session[{!r}].'
''.format(self._get_base_error_message(),
self.expected_session_key)
)
if self.expected_session_secondary_keys:
self.step.test.assertIsInstance(
self.step.response.wsgi_request.session[self.expected_session_key],
dict,
'{} session[{!r}] is not a dictionary hence cannot contain secondary keys.'
''.format(self._get_base_error_message(), self.expected_session_key)
)
# Make sure secondary keys are not empty.
for secondary_key in self.expected_session_secondary_keys:
self.step.test.assertIn(
secondary_key,
self.step.response.wsgi_request.session[self.expected_session_key].keys(),
'{} does not contain session[{!r}][{!r}].'
''.format(self._get_base_error_message(), self.expected_session_key, secondary_key)
)
self.step.test.assertIsNotNone(
self.step.response.wsgi_request.session[self.expected_session_key][secondary_key],
'{} contains session[{!r}][{!r}] but is empty.'
''.format(self._get_base_error_message(), self.expected_session_key, secondary_key)
)
[docs]class FormInitialDataValidator(BaseValidator):
"""
Validator checks that form in response has expected data in initial data.
:var str initial_data_key: Expected initial data key to be present in form initial data
:var expected_initial_data_value: Expected value initial value should be set to
:var str context_data_form_name: Template context data key for form data
:var bool test_initial_value: Test if the initial value matched expected value
:var bool test_initial_value_present: Test if the initial value key is present in initial data
:var bool test_initial_value_not_none: Test if the initial value is not ``None``
"""
expected_attrs = ('initial_data_key',)
initial_data_key = None
expected_initial_data_value = None
context_data_form_name = 'form'
test_initial_value = False
test_initial_value_present = True
test_initial_value_not_none = True
[docs] def test(self, test_step):
super(FormInitialDataValidator, self).test(test_step)
self.step.test.assertIsInstance(
self.step.response,
SimpleTemplateResponse,
'{} did not return SimpleTemplateResponse '
'and as such response.context_data is not accessible. '
'It returned {!r}'
''.format(self._get_base_error_message(),
type(self.step.response))
)
self.step.test.assertIn(
self.context_data_form_name,
self.step.response.context_data,
'{} did not render with {!r} in its context_data'
''.format(self._get_base_error_message(),
self.context_data_form_name)
)
self.step.test.assertIsInstance(
self.step.response.context_data[self.context_data_form_name].initial,
dict,
'{} did not render with the context_data[{!r}].initial being a dictionary'
''.format(self._get_base_error_message(),
self.context_data_form_name)
)
if self.test_initial_value_present:
self.step.test.assertIn(
self.initial_data_key,
self.step.response.context_data[self.context_data_form_name].initial.keys(),
'{} did not render with {!r} in the context_data[{!r}].initial. '
'Provided keys - {!r}'
''.format(self._get_base_error_message(),
self.initial_data_key,
self.context_data_form_name,
self.step.response.context_data[self.context_data_form_name].initial.keys())
)
if self.test_initial_value_not_none:
self.step.test.assertIsNotNone(
self.step.response.context_data['form'].initial[self.initial_data_key],
'{} rendered with {!r} for context_data[{!r}].initial[{!r}]'
''.format(self._get_base_error_message(),
None,
self.context_data_form_name,
self.initial_data_key)
)
if self.test_initial_value:
self.step.test.assertEqual(
self.step.response.context_data['form'].initial[self.initial_data_key],
self.expected_initial_data_value,
'{} rendered with context_data[{!r}].initial[{!r}] = {} != {}'
''.format(self._get_base_error_message(),
self.context_data_form_name,
self.initial_data_key,
self.step.response.context_data['form'].initial[self.initial_data_key],
self.expected_initial_data_value)
)