From e91232f722966bf0b116606d67daac29375b529a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 8 Nov 2019 00:14:39 -0400 Subject: [PATCH] Add TestServerTestCaseMixin Allows performing mocked HTTP requests. Signed-off-by: Roberto Rosario --- mayan/apps/common/tests/http_adapters.py | 73 +++++++++++++++++++++++ mayan/apps/common/tests/literals.py | 3 + mayan/apps/common/tests/mixins.py | 75 +++++++++++++++++++++--- mayan/apps/common/tests/mocks.py | 17 ++++++ 4 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 mayan/apps/common/tests/http_adapters.py create mode 100644 mayan/apps/common/tests/mocks.py diff --git a/mayan/apps/common/tests/http_adapters.py b/mayan/apps/common/tests/http_adapters.py new file mode 100644 index 0000000000..59b38928b6 --- /dev/null +++ b/mayan/apps/common/tests/http_adapters.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import, unicode_literals + +import requests + +from django.utils.six import BytesIO + + +class TestClientAdapter(requests.adapters.BaseAdapter): + def __init__(self, test_case): + self.test_case = test_case + + def build_response(self, request, django_response): + """ + Build a requests response from a Django response + """ + response = requests.Response() + + # Fallback to None if there's no status_code, for whatever reason. + response.status_code = getattr( + django_response, 'status_code', None + ) + + # Make headers case-insensitive. + response.headers = requests.structures.CaseInsensitiveDict( + getattr(django_response, 'headers', {}) + ) + + # Set encoding. + response.encoding = requests.utils.get_encoding_from_headers( + headers=response.headers + ) + response.raw = BytesIO(django_response.getvalue()) + + response.reason = django_response.reason_phrase + + if isinstance(request.url, bytes): + response.url = request.url.decode('utf-8') + else: + response.url = request.url + + # Add new cookies from the server. + requests.cookies.extract_cookies_to_jar( + jar=response.cookies, request=request, + response=django_response + ) + + # Give the Response some context. + response.request = django_response + response.connection = self + + return response + + def close(self): + """ + No connection needs to be closed, but the method must exists + or a NotImplementedError is raised. + """ + + def send( + self, request=None, stream=False, timeout=None, verify=True, + cert=None, proxies=None + ): + """Craft a Django request based on the attribute of + requests' request object. + """ + return self.build_response( + request=request, django_response=self.test_case.generic( + data=request.body, + headers=request.headers, + method=request.method, + viewname=self.test_case.test_view_name, + ) + ) diff --git a/mayan/apps/common/tests/literals.py b/mayan/apps/common/tests/literals.py index 3cfb7ca5fd..0f96e2cf0a 100644 --- a/mayan/apps/common/tests/literals.py +++ b/mayan/apps/common/tests/literals.py @@ -43,3 +43,6 @@ TEST_TAR_GZ_FILE_PATH = os.path.join( TEST_ZIP_FILE_PATH = os.path.join( settings.BASE_DIR, 'apps', 'common', 'tests', 'contrib', TEST_ZIP_FILE ) + +TEST_SERVER_HOST = 'testserver' +TEST_SERVER_SCHEME = 'http' diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index afb9501fde..e6ee2027d3 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -23,7 +23,9 @@ from django.utils.six import PY3 from mayan.apps.storage.settings import setting_temporary_directory -from .literals import TEST_VIEW_NAME, TEST_VIEW_URL +from .literals import ( + TEST_SERVER_HOST, TEST_SERVER_SCHEME, TEST_VIEW_NAME, TEST_VIEW_URL +) if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False): @@ -35,6 +37,7 @@ class ClientMethodsTestCaseMixin(object): data = kwargs.pop('data', {}) follow = kwargs.pop('follow', False) query = kwargs.pop('query', {}) + headers = kwargs.pop('headers', {}) if viewname: path = reverse(viewname=viewname, *args, **kwargs) @@ -42,7 +45,9 @@ class ClientMethodsTestCaseMixin(object): path = furl(url=path) path.args.update(query) - return {'follow': follow, 'data': data, 'path': path.tostr()} + result = {'follow': follow, 'data': data, 'path': path.tostr()} + result.update(headers) + return result def delete(self, viewname=None, path=None, *args, **kwargs): return self.client.delete( @@ -51,6 +56,13 @@ class ClientMethodsTestCaseMixin(object): ) ) + def generic(self, method, viewname=None, path=None, *args, **kwargs): + return self.client.generic( + method=method, **self._build_verb_kwargs( + path=path, viewname=viewname, *args, **kwargs + ) + ) + def get(self, viewname=None, path=None, *args, **kwargs): return self.client.get( **self._build_verb_kwargs( @@ -381,8 +393,43 @@ class TestModelTestMixin(object): self.test_object = TestModel.objects.create(**kwargs) +class TestServerTestCaseMixin(object): + def setUp(self): + super(TestServerTestCaseMixin, self).setUp() + self.testserver_prefix = self.get_testserver_prefix() + self.testserver_url = self.get_testserver_url() + self.test_view_request = None + + def _test_view_factory(self, test_object=None): + def test_view(request): + self.test_view_request = request + return HttpResponse() + + return test_view + + def get_testserver_prefix(self): + return furl( + scheme=TEST_SERVER_SCHEME, host=TEST_SERVER_HOST, + ).tostr() + + def get_testserver_url(self): + return furl( + scheme=TEST_SERVER_SCHEME, host=TEST_SERVER_HOST, + path=self.test_view_url + ).tostr() + + class TestViewTestCaseMixin(object): + auto_add_test_view = False has_test_view = False + test_view_object = None + test_view_name = TEST_VIEW_NAME + test_view_url = TEST_VIEW_URL + + def setUp(self): + super(TestViewTestCaseMixin, self).setUp() + if self.auto_add_test_view: + self.add_test_view(test_object=self.test_view_object) def tearDown(self): urlconf = importlib.import_module(settings.ROOT_URLCONF) @@ -392,9 +439,7 @@ class TestViewTestCaseMixin(object): urlconf.urlpatterns.pop(0) super(TestViewTestCaseMixin, self).tearDown() - def add_test_view(self, test_object): - urlconf = importlib.import_module(settings.ROOT_URLCONF) - + def _test_view_factory(self, test_object=None): def test_view(request): template = Template('{{ object }}') context = Context( @@ -402,12 +447,23 @@ class TestViewTestCaseMixin(object): ) return HttpResponse(template.render(context=context)) - urlconf.urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME)) + return test_view + + def add_test_view(self, test_object=None): + urlconf = importlib.import_module(settings.ROOT_URLCONF) + + urlconf.urlpatterns.insert( + 0, url( + regex=self.test_view_url, view=self._test_view_factory( + test_object=test_object + ), name=self.test_view_name + ) + ) clear_url_caches() self.has_test_view = True def get_test_view(self): - response = self.get(TEST_VIEW_NAME) + response = self.get(viewname=self.test_view_name) if isinstance(response.context, ContextList): # template widget rendering causes test client response to be # ContextList rather than RequestContext. Typecast to dictionary @@ -416,5 +472,6 @@ class TestViewTestCaseMixin(object): result.update({'request': response.wsgi_request}) return Context(result) else: - response.context.update({'request': response.wsgi_request}) - return Context(response.context) + result = response.context or {} + result.update({'request': response.wsgi_request}) + return Context(result) diff --git a/mayan/apps/common/tests/mocks.py b/mayan/apps/common/tests/mocks.py new file mode 100644 index 0000000000..d2f5c356c9 --- /dev/null +++ b/mayan/apps/common/tests/mocks.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +import requests + +from .http_adapters import TestClientAdapter + + +def request_method_factory(test_case): + def request(method, url, **kwargs): + with requests.sessions.Session() as session: + session.mount( + prefix=test_case.testserver_prefix, + adapter=TestClientAdapter(test_case=test_case) + ) + return session.request(method=method, url=url, **kwargs) + + return request