From 739c09507eedaa1ff4e1fbb3b267e2edf21662c0 Mon Sep 17 00:00:00 2001 From: Jordan Moldow Date: Thu, 24 Aug 2017 22:00:12 -0700 Subject: [PATCH 1/3] Create RequestsSessionNetwork This class is the same as the former `DefaultNetwork` class, except that it accepts an optional `session` constructor parameter, for clients to specify a session of their choosing rather than having it constructed automatically. `DefaultNetwork` is aliased to `RequestsSessionNetwork`, for backwards-compatibility. This enables the boxsdk's testing framework to start using the `betamax` to record real network calls and responses. Although we currently only add one such test, this is a potential alternative to mock-box functional tests in the future. This also allows for dependents of boxsdk to write `betamax` tests as well. --- .travis.yml | 16 ++-- HISTORY.rst | 4 + boxsdk/network/default_network.py | 18 ++++- requirements-dev.txt | 3 +- setup.py | 2 +- test/conftest.py | 81 ++++++++++++++++++- test/unit/conftest.py | 14 +++- ...ts_session_network_works_with_betamax.json | 1 + test/unit/network/test_network.py | 21 ++++- tox.ini | 4 + 10 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json diff --git a/.travis.yml b/.travis.yml index 31239bb09..06fbbeb3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,25 +11,25 @@ before_cache: matrix: include: - python: 2.6 - env: TOX_ENV=py26 + env: TOX_ENV=py26 IS_CI=true - python: 2.7 - env: TOX_ENV=py27 + env: TOX_ENV=py27 IS_CI=true - python: 3.3 - env: TOX_ENV=py33 + env: TOX_ENV=py33 IS_CI=true - python: 3.4 - env: TOX_ENV=py34 + env: TOX_ENV=py34 IS_CI=true - python: 3.5 - env: TOX_ENV=py35 + env: TOX_ENV=py35 IS_CI=true - python: 3.6 - env: TOX_ENV=py36 + env: TOX_ENV=py36 IS_CI=true - python: pypy - env: TOX_ENV=pypy PYPY_VERSION='2.7-5.8.0' + env: TOX_ENV=pypy IS_CI=true PYPY_VERSION='2.7-5.8.0' - python: 2.7 env: TOX_ENV=pep8 - python: 2.7 env: TOX_ENV=pylint - python: 2.7 - env: TOX_ENV=coverage + env: TOX_ENV=coverage IS_CI=true # commands to install dependencies install: diff --git a/HISTORY.rst b/HISTORY.rst index bf4701596..661ef0141 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -112,6 +112,10 @@ Release History ``BaseObject`` is the parent of all objects that are a part of the REST API. Another subclass of ``BaseAPIJSONObject``, ``APIJSONObject``, was created to represent pseudo-smart objects such as ``Event`` that are not directly accessible through an API endpoint. +- Renamed ``DefaultNetwork`` to ``RequestsSessionNetwork`` (keeping + ``DefaultNetwork`` as an alias for backwards-compatibility), and added an + optional ``session`` argument, for clients to pass a custom + ``requests.Session`` object. - Added ``network_response_constructor`` as an optional property on the ``Network`` interface. Implementations are encouraged to override this property, and use it to construct ``NetworkResponse`` instances. That way, diff --git a/boxsdk/network/default_network.py b/boxsdk/network/default_network.py index 59ea9df92..549cae9f0 100644 --- a/boxsdk/network/default_network.py +++ b/boxsdk/network/default_network.py @@ -9,12 +9,19 @@ from .network_interface import Network, NetworkResponse -class DefaultNetwork(Network): +class RequestsSessionNetwork(Network): """Implementation of the network interface using the requests library.""" - def __init__(self): - super(DefaultNetwork, self).__init__() - self._session = requests.Session() + def __init__(self, session=None): + """Extends baseclass method. + + :param session: + (optional) A specific session to use. + If not given, a default instance will be constructed and used. + :type session: :class:`requests.Session` + """ + super(RequestsSessionNetwork, self).__init__() + self._session = session or requests.Session() def request(self, method, url, access_token, **kwargs): """Base class override. @@ -43,6 +50,9 @@ def network_response_constructor(self): return DefaultNetworkResponse +DefaultNetwork = RequestsSessionNetwork + + class DefaultNetworkResponse(NetworkResponse): """Implementation of the network interface using the requests library.""" diff --git a/requirements-dev.txt b/requirements-dev.txt index a5b26860c..487b796c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -r requirements.txt +betamax bottle jsonpatch mock>=2.0.0 @@ -9,4 +10,4 @@ pytest-cov pytest-xdist sphinx>=1.5,<1.6 sqlalchemy -tox \ No newline at end of file +tox diff --git a/setup.py b/setup.py index 4966244cb..a6dd9239f 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def main(): packages=find_packages(exclude=['demo', 'docs', 'test', 'test*', '*test', '*test*']), install_requires=install_requires, extras_require=extra_requires, - tests_require=['pytest', 'pytest-xdist', 'mock', 'sqlalchemy', 'bottle', 'jsonpatch'], + tests_require=['betamax', 'pytest', 'pytest-xdist', 'mock', 'sqlalchemy', 'bottle', 'jsonpatch'], cmdclass={'test': PyTest}, classifiers=CLASSIFIERS, keywords='box oauth2 sdk', diff --git a/test/conftest.py b/test/conftest.py index 673b44ace..7ad5a9392 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,14 +4,33 @@ import json import logging +import os import sys +from betamax import Betamax from mock import Mock import pytest import requests from six import binary_type -from boxsdk.network.default_network import DefaultNetworkResponse +from boxsdk.network.default_network import DefaultNetworkResponse, RequestsSessionNetwork + + +class RealRequestsSession(requests.Session): + """A real, functioning subclass of requests.Session. + + Instances of this class will always be able to make real network calls, + even if the implementation of `requests.Session.request()` is replaced or + removed via monkey patching. + """ + + # Grab the necessary references before they are replaced or removed. + request = requests.Session.request + + +@pytest.fixture +def real_requests_session(): + return RealRequestsSession() @pytest.fixture(autouse=True, scope='session') @@ -259,3 +278,63 @@ def mock_user_id(): @pytest.fixture(scope='module') def mock_group_id(): return 'fake-group-99' + + +# betamax integration + + +@pytest.fixture(scope='module') +def betamax_cassette_library_dir(request): + """Each directory test/foo/bar that uses betamax has a directory test/foo/bar/cassettes to hold cassettes.""" + return os.path.join(request.fspath.dirname, 'cassettes') + + +@pytest.fixture +def configure_betamax(betamax_cassette_library_dir): + with Betamax.configure() as config: + config.cassette_library_dir = betamax_cassette_library_dir + config.default_cassette_options['re_record_interval'] = 100 + config.default_cassette_options['record'] = 'none' if os.environ.get('IS_CI') else 'once' + + +@pytest.fixture +def betamax_cassette_name(request): + """The betamax cassette name to use for the test. + + The name is the same as the pytest nodeid (e.g. + module_path::parametrized_test_name or + module_path::class_name::parametrized_test_name), but replacing the full + module-path with just the base filename, e.g. test_foo::test_bar[0]. + """ + node_ids = request.node.nodeid.split('::') + node_ids[0] = request.fspath.purebasename + return '::'.join(node_ids) + + +@pytest.fixture(scope='module') +def betamax_use_cassette_kwargs(): + return {} + + +@pytest.fixture +def betamax_recorder(configure_betamax, real_requests_session): # pylint:disable=unused-argument + return Betamax(real_requests_session) + + +@pytest.fixture +def betamax_cassette_recorder(betamax_recorder, betamax_cassette_name, betamax_use_cassette_kwargs): + """Including this fixture causes the test to use a betamax cassette for network requests.""" + with betamax_recorder.use_cassette(betamax_cassette_name, **betamax_use_cassette_kwargs) as cassette_recorder: + yield cassette_recorder + + +@pytest.fixture +def betamax_session(betamax_cassette_recorder): + """A betamax-enabled requests.Session instance.""" + return betamax_cassette_recorder.session + + +@pytest.fixture +def betamax_network(betamax_session): + """A betamax-enabled boxsdk.Network instance.""" + return RequestsSessionNetwork(session=betamax_session) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 6f9b9d4c3..a05f51dc0 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -63,9 +63,21 @@ def mock_network_layer(): return mock_network +def _requests_session_request_monkeypatch(*args, **kwargs): # pylint:disable=unused-argument + raise NotImplementedError('request() is disallowed during unit testing') + + @pytest.fixture(autouse=True) def prevent_tests_from_making_real_network_requests(monkeypatch): - monkeypatch.delattr(default_network.requests.Session, 'request') + """Prevent real network requests by monkey patching requests. + + This monkey patch is bypassed for betamax tests, which may make real + network requests when recording a cassette. + + Set a dummy implementation of `requests.Session.request()`, rather than + deleting it, so that `mock.Mock` objects know about its existence. + """ + monkeypatch.setattr(default_network.requests.Session, 'request', _requests_session_request_monkeypatch) @pytest.fixture(scope='function') diff --git a/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json b/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json new file mode 100644 index 000000000..29af12442 --- /dev/null +++ b/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"encoding": "utf-8", "string": ""}, "headers": {"User-Agent": ["python-requests/2.18.4"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "Connection": ["keep-alive"]}, "method": "GET", "uri": "https://api.box.com/2.0/users/me"}, "response": {"body": {"encoding": null, "string": ""}, "headers": {"Server": ["ATS"], "Date": ["Fri, 25 Aug 2017 02:49:37 GMT"], "Content-Length": ["0"], "Strict-Transport-Security": ["max-age=31536000; includeSubDomains"], "WWW-Authenticate": ["Bearer realm=\"Service\", error=\"invalid_request\", error_description=\"The access token was not found.\""], "Age": ["0"], "Connection": ["keep-alive"]}, "status": {"code": 401, "message": "Unauthorized"}, "url": "https://api.box.com/2.0/users/me"}, "recorded_at": "2017-08-25T02:49:37"}], "recorded_with": "betamax/0.8.0"} \ No newline at end of file diff --git a/test/unit/network/test_network.py b/test/unit/network/test_network.py index 9a734e18a..839d15624 100644 --- a/test/unit/network/test_network.py +++ b/test/unit/network/test_network.py @@ -2,11 +2,11 @@ from __future__ import absolute_import, unicode_literals -from mock import DEFAULT, Mock, patch +from mock import DEFAULT, Mock, NonCallableMock, patch import pytest -from requests import Response +from requests import Response, Session -from boxsdk.network.default_network import DefaultNetworkResponse, DefaultNetwork +from boxsdk.network.default_network import DefaultNetworkResponse, DefaultNetwork, RequestsSessionNetwork @pytest.fixture @@ -69,3 +69,18 @@ def network_response_constructor(self): network = DefaultNetworkSubclass() response = make_network_request(network) assert isinstance(response, DefaultNetworkResponseSubclass) + + +def test_requests_session_network_accepts_custom_session(): + mock_requests_session = NonCallableMock(spec_set=Session, name='requests_session') + network = RequestsSessionNetwork(session=mock_requests_session) + network.request(method='method', url='url', access_token='access_token') + assert len(mock_requests_session.mock_calls) == 1 + assert mock_requests_session.request.call_count == 1 + mock_requests_session.request.assert_called_once_with('method', 'url') + + +def test_requests_session_network_works_with_betamax(betamax_network): + response = betamax_network.request(method='GET', url='https://api.box.com/2.0/users/me', access_token='access_token') + assert response.ok is False + assert response.status_code == 401 diff --git a/tox.ini b/tox.ini index 65afa6fc2..33740618f 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ envlist = commands = pytest {posargs} deps = -rrequirements-dev.txt +passenv = IS_CI [testenv:rst] deps = @@ -38,6 +39,7 @@ commands = pep8 --ignore=E501,W292 test deps = pep8 +skip_install = True [testenv:pylint] commands = @@ -45,10 +47,12 @@ commands = # pylint:disable W0621(redefined-outer-name) - Using py.test fixtures always breaks this rule. pylint --rcfile=.pylintrc test -d W0621 --ignore=mock_box deps = -rrequirements-dev.txt +skip_install = True [testenv:coverage] commands = py.test --cov boxsdk test/unit test/integration deps = -rrequirements-dev.txt +passenv = IS_CI [testenv:docs] changedir = docs From 9deb303af981898c7607a845bb3f89618520624c Mon Sep 17 00:00:00 2001 From: Jordan Moldow Date: Fri, 25 Aug 2017 12:58:54 -0700 Subject: [PATCH 2/3] Add pytest plugin Move the betamax fixtures to an installable pytest plugin, so that dependent applications can also make use of them. --- HISTORY.rst | 3 + boxsdk/pytest_plugin/__init__.py | 9 +++ boxsdk/pytest_plugin/betamax.py | 100 ++++++++++++++++++++++++++++++ requirements-dev.txt | 1 + setup.py | 7 +++ test/conftest.py | 67 ++------------------ test/unit/network/test_network.py | 4 +- 7 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 boxsdk/pytest_plugin/__init__.py create mode 100644 boxsdk/pytest_plugin/betamax.py diff --git a/HISTORY.rst b/HISTORY.rst index 661ef0141..d27e82b1a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -116,6 +116,9 @@ Release History ``DefaultNetwork`` as an alias for backwards-compatibility), and added an optional ``session`` argument, for clients to pass a custom ``requests.Session`` object. +- Added a pytest plugin (which can be installed with ``pip install + boxsdk[pytest]``) that defines ``betamax``-powered fixtures that clients can + use to test their ``boxsdk``-based applications. - Added ``network_response_constructor`` as an optional property on the ``Network`` interface. Implementations are encouraged to override this property, and use it to construct ``NetworkResponse`` instances. That way, diff --git a/boxsdk/pytest_plugin/__init__.py b/boxsdk/pytest_plugin/__init__.py new file mode 100644 index 000000000..8cbce9244 --- /dev/null +++ b/boxsdk/pytest_plugin/__init__.py @@ -0,0 +1,9 @@ +# coding: utf-8 + +from __future__ import absolute_import, unicode_literals + + +__doc__ = """pytest fixtures that can help with testing boxsdk-powered applications.""" # pylint:disable=redefined-builtin + + +pytest_plugins = ['boxsdk.pytest_plugin.betamax'] # pylint:disable=invalid-name diff --git a/boxsdk/pytest_plugin/betamax.py b/boxsdk/pytest_plugin/betamax.py new file mode 100644 index 000000000..973459674 --- /dev/null +++ b/boxsdk/pytest_plugin/betamax.py @@ -0,0 +1,100 @@ +# coding: utf-8 + +from __future__ import absolute_import, unicode_literals + +import os + +from betamax import Betamax +import pytest +import requests + +from boxsdk import Client +from boxsdk.network.default_network import RequestsSessionNetwork +from boxsdk.session.box_session import BoxSession + + +# pylint:disable=redefined-outer-name + + +@pytest.fixture +def real_requests_session(): + return requests.Session() + + +@pytest.fixture(scope='module') +def betamax_cassette_library_dir(request): + """Each directory test/foo/bar that uses betamax has a directory test/foo/bar/cassettes to hold cassettes.""" + return os.path.join(request.fspath.dirname, 'cassettes') + + +@pytest.fixture +def configure_betamax(betamax_cassette_library_dir): + if not os.path.exists(betamax_cassette_library_dir): + os.makedirs(betamax_cassette_library_dir) + with Betamax.configure() as config: + config.cassette_library_dir = betamax_cassette_library_dir + config.default_cassette_options['re_record_interval'] = 100 + config.default_cassette_options['record'] = 'none' if os.environ.get('IS_CI') else 'once' + + +@pytest.fixture +def betamax_cassette_name(request): + """The betamax cassette name to use for the test. + + The name is the same as the pytest nodeid (e.g. + module_path::parametrized_test_name or + module_path::class_name::parametrized_test_name), but replacing the full + module-path with just the base filename, e.g. test_foo::test_bar[0]. + """ + node_ids = request.node.nodeid.split('::') + node_ids[0] = request.fspath.purebasename + return '::'.join(node_ids) + + +@pytest.fixture(scope='module') +def betamax_use_cassette_kwargs(): + return {} + + +@pytest.fixture +def betamax_recorder(configure_betamax, real_requests_session): # pylint:disable=unused-argument + return Betamax(real_requests_session) + + +@pytest.fixture +def betamax_cassette_recorder(betamax_recorder, betamax_cassette_name, betamax_use_cassette_kwargs): + """Including this fixture causes the test to use a betamax cassette for network requests.""" + with betamax_recorder.use_cassette(betamax_cassette_name, **betamax_use_cassette_kwargs) as cassette_recorder: + yield cassette_recorder + + +@pytest.fixture +def betamax_session(betamax_cassette_recorder): + """A betamax-enabled requests.Session instance.""" + return betamax_cassette_recorder.session + + +@pytest.fixture +def betamax_boxsdk_network(betamax_session): + """A betamax-enabled boxsdk.Network instance.""" + return RequestsSessionNetwork(session=betamax_session) + + +@pytest.fixture +def betamax_boxsdk_session(betamax_boxsdk_network, betamax_boxsdk_auth): + """A betamax-enabled boxsdk.BoxSession instance. + + Requires an implementation of the abstract `betamax_boxsdk_auth` fixture, + of type `boxsdk.OAuth2`. + """ + return BoxSession(oauth=betamax_boxsdk_auth, network_layer=betamax_boxsdk_network) + + +@pytest.fixture +def betamax_boxsdk_client(betamax_boxsdk_session, betamax_boxsdk_auth): + """A betamax-enabled boxsdk.Client instance. + + Requires an implementation of the abstract `betamax_boxsdk_auth` fixture, + of type `boxsdk.OAuth2`. + """ + return Client(oauth=betamax_boxsdk_auth, session=betamax_boxsdk_session) diff --git a/requirements-dev.txt b/requirements-dev.txt index 487b796c4..3ce87ebb7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +-e .[pytest] -r requirements.txt betamax bottle diff --git a/setup.py b/setup.py index a6dd9239f..ab4671f60 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ def main(): jwt_requires = ['pyjwt>=1.3.0', 'cryptography>=0.9.2'] extra_requires = defaultdict(list) extra_requires.update({'jwt': jwt_requires, 'redis': redis_requires, 'all': jwt_requires + redis_requires}) + extra_requires['pytest'] = ['betamax', 'pytest>=3.0.0'] conditional_dependencies = { # Newer versions of pip and wheel, which support PEP 426, allow # environment markers for conditional dependencies to use operators @@ -97,6 +98,12 @@ def main(): install_requires=install_requires, extras_require=extra_requires, tests_require=['betamax', 'pytest', 'pytest-xdist', 'mock', 'sqlalchemy', 'bottle', 'jsonpatch'], + entry_points={ + 'pytest11': [ + # pytest fixtures that can help with testing boxsdk-powered applications. + 'boxsdk = boxsdk.pytest_plugin [pytest]', + ], + }, cmdclass={'test': PyTest}, classifiers=CLASSIFIERS, keywords='box oauth2 sdk', diff --git a/test/conftest.py b/test/conftest.py index 7ad5a9392..c2717e7b4 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,16 +4,17 @@ import json import logging -import os import sys -from betamax import Betamax from mock import Mock import pytest import requests from six import binary_type -from boxsdk.network.default_network import DefaultNetworkResponse, RequestsSessionNetwork +from boxsdk.network.default_network import DefaultNetworkResponse + + +pytest_plugins = ['boxsdk'] # pylint:disable=invalid-name class RealRequestsSession(requests.Session): @@ -278,63 +279,3 @@ def mock_user_id(): @pytest.fixture(scope='module') def mock_group_id(): return 'fake-group-99' - - -# betamax integration - - -@pytest.fixture(scope='module') -def betamax_cassette_library_dir(request): - """Each directory test/foo/bar that uses betamax has a directory test/foo/bar/cassettes to hold cassettes.""" - return os.path.join(request.fspath.dirname, 'cassettes') - - -@pytest.fixture -def configure_betamax(betamax_cassette_library_dir): - with Betamax.configure() as config: - config.cassette_library_dir = betamax_cassette_library_dir - config.default_cassette_options['re_record_interval'] = 100 - config.default_cassette_options['record'] = 'none' if os.environ.get('IS_CI') else 'once' - - -@pytest.fixture -def betamax_cassette_name(request): - """The betamax cassette name to use for the test. - - The name is the same as the pytest nodeid (e.g. - module_path::parametrized_test_name or - module_path::class_name::parametrized_test_name), but replacing the full - module-path with just the base filename, e.g. test_foo::test_bar[0]. - """ - node_ids = request.node.nodeid.split('::') - node_ids[0] = request.fspath.purebasename - return '::'.join(node_ids) - - -@pytest.fixture(scope='module') -def betamax_use_cassette_kwargs(): - return {} - - -@pytest.fixture -def betamax_recorder(configure_betamax, real_requests_session): # pylint:disable=unused-argument - return Betamax(real_requests_session) - - -@pytest.fixture -def betamax_cassette_recorder(betamax_recorder, betamax_cassette_name, betamax_use_cassette_kwargs): - """Including this fixture causes the test to use a betamax cassette for network requests.""" - with betamax_recorder.use_cassette(betamax_cassette_name, **betamax_use_cassette_kwargs) as cassette_recorder: - yield cassette_recorder - - -@pytest.fixture -def betamax_session(betamax_cassette_recorder): - """A betamax-enabled requests.Session instance.""" - return betamax_cassette_recorder.session - - -@pytest.fixture -def betamax_network(betamax_session): - """A betamax-enabled boxsdk.Network instance.""" - return RequestsSessionNetwork(session=betamax_session) diff --git a/test/unit/network/test_network.py b/test/unit/network/test_network.py index 839d15624..a3149b6c9 100644 --- a/test/unit/network/test_network.py +++ b/test/unit/network/test_network.py @@ -80,7 +80,7 @@ def test_requests_session_network_accepts_custom_session(): mock_requests_session.request.assert_called_once_with('method', 'url') -def test_requests_session_network_works_with_betamax(betamax_network): - response = betamax_network.request(method='GET', url='https://api.box.com/2.0/users/me', access_token='access_token') +def test_requests_session_network_works_with_betamax(betamax_boxsdk_network): + response = betamax_boxsdk_network.request(method='GET', url='https://api.box.com/2.0/users/me', access_token='access_token') assert response.ok is False assert response.status_code == 401 From d90dd57b8f4e2882d5a384f897ffc747e94a687c Mon Sep 17 00:00:00 2001 From: Jordan Moldow Date: Fri, 25 Aug 2017 14:33:15 -0700 Subject: [PATCH 3/3] Use prettyjson and disable gzip with betamax --- boxsdk/pytest_plugin/betamax.py | 9 ++- setup.py | 4 +- ...ts_session_network_works_with_betamax.json | 65 ++++++++++++++++++- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/boxsdk/pytest_plugin/betamax.py b/boxsdk/pytest_plugin/betamax.py index 973459674..6bc8d5914 100644 --- a/boxsdk/pytest_plugin/betamax.py +++ b/boxsdk/pytest_plugin/betamax.py @@ -5,6 +5,7 @@ import os from betamax import Betamax +from betamax_serializers import pretty_json import pytest import requests @@ -31,6 +32,7 @@ def betamax_cassette_library_dir(request): def configure_betamax(betamax_cassette_library_dir): if not os.path.exists(betamax_cassette_library_dir): os.makedirs(betamax_cassette_library_dir) + Betamax.register_serializer(pretty_json.PrettyJSONSerializer) with Betamax.configure() as config: config.cassette_library_dir = betamax_cassette_library_dir config.default_cassette_options['re_record_interval'] = 100 @@ -64,6 +66,7 @@ def betamax_recorder(configure_betamax, real_requests_session): # pylint:disab @pytest.fixture def betamax_cassette_recorder(betamax_recorder, betamax_cassette_name, betamax_use_cassette_kwargs): """Including this fixture causes the test to use a betamax cassette for network requests.""" + betamax_use_cassette_kwargs.setdefault('serialize_with', 'prettyjson') with betamax_recorder.use_cassette(betamax_cassette_name, **betamax_use_cassette_kwargs) as cassette_recorder: yield cassette_recorder @@ -87,7 +90,11 @@ def betamax_boxsdk_session(betamax_boxsdk_network, betamax_boxsdk_auth): Requires an implementation of the abstract `betamax_boxsdk_auth` fixture, of type `boxsdk.OAuth2`. """ - return BoxSession(oauth=betamax_boxsdk_auth, network_layer=betamax_boxsdk_network) + return BoxSession( + oauth=betamax_boxsdk_auth, + network_layer=betamax_boxsdk_network, + default_headers={'Accept-Encoding': ' '}, # Turn off gzip so that raw JSON responses are recorded. + ) @pytest.fixture diff --git a/setup.py b/setup.py index ab4671f60..9c6d63e9a 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ def main(): jwt_requires = ['pyjwt>=1.3.0', 'cryptography>=0.9.2'] extra_requires = defaultdict(list) extra_requires.update({'jwt': jwt_requires, 'redis': redis_requires, 'all': jwt_requires + redis_requires}) - extra_requires['pytest'] = ['betamax', 'pytest>=3.0.0'] + extra_requires['pytest'] = ['betamax', 'betamax-serializers', 'pytest>=3.0.0'] conditional_dependencies = { # Newer versions of pip and wheel, which support PEP 426, allow # environment markers for conditional dependencies to use operators @@ -97,7 +97,7 @@ def main(): packages=find_packages(exclude=['demo', 'docs', 'test', 'test*', '*test', '*test*']), install_requires=install_requires, extras_require=extra_requires, - tests_require=['betamax', 'pytest', 'pytest-xdist', 'mock', 'sqlalchemy', 'bottle', 'jsonpatch'], + tests_require=['betamax', 'betamax-serializers', 'pytest', 'pytest-xdist', 'mock', 'sqlalchemy', 'bottle', 'jsonpatch'], entry_points={ 'pytest11': [ # pytest fixtures that can help with testing boxsdk-powered applications. diff --git a/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json b/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json index 29af12442..a725d7d44 100644 --- a/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json +++ b/test/unit/network/cassettes/test_network::test_requests_session_network_works_with_betamax.json @@ -1 +1,64 @@ -{"http_interactions": [{"request": {"body": {"encoding": "utf-8", "string": ""}, "headers": {"User-Agent": ["python-requests/2.18.4"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "Connection": ["keep-alive"]}, "method": "GET", "uri": "https://api.box.com/2.0/users/me"}, "response": {"body": {"encoding": null, "string": ""}, "headers": {"Server": ["ATS"], "Date": ["Fri, 25 Aug 2017 02:49:37 GMT"], "Content-Length": ["0"], "Strict-Transport-Security": ["max-age=31536000; includeSubDomains"], "WWW-Authenticate": ["Bearer realm=\"Service\", error=\"invalid_request\", error_description=\"The access token was not found.\""], "Age": ["0"], "Connection": ["keep-alive"]}, "status": {"code": 401, "message": "Unauthorized"}, "url": "https://api.box.com/2.0/users/me"}, "recorded_at": "2017-08-25T02:49:37"}], "recorded_with": "betamax/0.8.0"} \ No newline at end of file +{ + "http_interactions": [ + { + "recorded_at": "2017-08-25T21:21:46", + "request": { + "body": { + "encoding": "utf-8", + "string": "" + }, + "headers": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Connection": [ + "keep-alive" + ], + "User-Agent": [ + "python-requests/2.18.4" + ] + }, + "method": "GET", + "uri": "https://api.box.com/2.0/users/me" + }, + "response": { + "body": { + "encoding": null, + "string": "" + }, + "headers": { + "Age": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ], + "Date": [ + "Fri, 25 Aug 2017 21:21:45 GMT" + ], + "Server": [ + "ATS" + ], + "Strict-Transport-Security": [ + "max-age=31536000; includeSubDomains" + ], + "WWW-Authenticate": [ + "Bearer realm=\"Service\", error=\"invalid_request\", error_description=\"The access token was not found.\"" + ] + }, + "status": { + "code": 401, + "message": "Unauthorized" + }, + "url": "https://api.box.com/2.0/users/me" + } + } + ], + "recorded_with": "betamax/0.8.0" +} \ No newline at end of file