From e9bb24074e52f4d85d41bdea426a7ea1880cc0ab Mon Sep 17 00:00:00 2001 From: naemono Date: Tue, 27 Oct 2015 15:46:24 -0500 Subject: [PATCH 1/4] Implementing Setting/Getting/Running of Objectrocket Acl Sync on instances via ApiV2. Fixing failing auth tests. Using responses library for mocking/testing api requests. Proper tests for get/set/run acl sync. Completes #22 --- objectrocket/auth.py | 11 ++- objectrocket/bases.py | 49 +++++++++++- objectrocket/instances/mongodb.py | 1 + requirements/dev.txt | 1 + tests/conftest.py | 13 +-- tests/test_acl_sync.py | 127 ++++++++++++++++++++++++++++++ tests/test_auth.py | 86 ++++++++++++++------ 7 files changed, 251 insertions(+), 37 deletions(-) create mode 100644 tests/test_acl_sync.py diff --git a/objectrocket/auth.py b/objectrocket/auth.py index c6c4cfd..bf4e77f 100644 --- a/objectrocket/auth.py +++ b/objectrocket/auth.py @@ -46,8 +46,15 @@ def authenticate(self, username, password): # Attempt to extract authentication data. try: - json_data = resp.json() - token = json_data['data']['token'] + if resp.status_code == 200: + json_data = resp.json() + token = json_data['data']['token'] + elif resp.status_code == 401: + raise errors.AuthFailure(resp.json().get('message', 'Authentication Failure.')) + else: + raise errors.AuthFailure("Unknown exception while authenticating: '{}'".format(resp.text)) + except errors.AuthFailure: + raise except Exception as ex: logging.exception(ex) raise errors.AuthFailure('{}: {}'.format(ex.__class__.__name__, ex)) diff --git a/objectrocket/bases.py b/objectrocket/bases.py index f6f4d2a..76bde32 100644 --- a/objectrocket/bases.py +++ b/objectrocket/bases.py @@ -1,6 +1,7 @@ """Base classes used throughout the library.""" import abc - +import json +import requests import six from objectrocket import errors @@ -120,6 +121,52 @@ def __repr__(self): ) return rep + def acl_sync(self, aws_sync=None, rackspace_sync=None): + """Adjust Amazon Web Services and/or Rackspace Acl Sync feature for this instance. + + :param bool aws_sync: True/False whether to enable AWS acl sync for this instance. + :param bool rackspace_sync: True/False whether to enable Rackspace acl sync for this instance. + """ + url = self._url + 'acl_sync' + + data = {"aws_acl_sync_enabled": False, "rackspace_acl_sync_enabled": False} + + # Let's get current status of acl sync for this intance to set proper defaults. + response = requests.get(url, **self._instances._default_request_kwargs) + + if response.status_code == 200: + resp_json = response.json() + current_status = resp_json.get('data', {}) + data.update({"aws_acl_sync_enabled": current_status.get("aws_acl_sync_enabled", False), + "rackspace_acl_sync_enabled": current_status.get("rackspace_acl_sync_enabled", False)}) + if aws_sync is not None: + data.update({"aws_acl_sync_enabled": aws_sync}) + if rackspace_sync is not None: + data.update({"rackspace_acl_sync_enabled": rackspace_sync}) + + response = requests.put(url, data=json.dumps(data), **self._instances._default_request_kwargs) + return response.json() + else: + raise errors.ObjectRocketException("Couldn't get current status of instance, failing. Error: {}".format(response.text)) + + def run_acl_sync(self, aws_sync=False, rackspace_sync=False): + """Run Acl sync for this instance. + + :param bool aws_sync: True/False whether to run AWS acl sync for this instance immediately. + :param bool rackspace_sync: True/False whether to run Rackspace acl sync for this instance immediately. + """ + url = self._url + 'acl_sync' + + data = {"aws_acl_sync_enabled": False, "rackspace_acl_sync_enabled": False} + + if aws_sync: + data.update({"aws_acl_sync_enabled": True}) + if rackspace_sync: + data.update({"rackspace_acl_sync_enabled": True}) + + response = requests.post(url, data=json.dumps(data), **self._instances._default_request_kwargs) + return response.json() + @property def connect_string(self): """This instance's connection string.""" diff --git a/objectrocket/instances/mongodb.py b/objectrocket/instances/mongodb.py index 5e6fea8..b6bb590 100644 --- a/objectrocket/instances/mongodb.py +++ b/objectrocket/instances/mongodb.py @@ -8,6 +8,7 @@ from objectrocket import bases from objectrocket import util +from objectrocket import errors logger = logging.getLogger(__name__) diff --git a/requirements/dev.txt b/requirements/dev.txt index dde18fa..403f377 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,3 +8,4 @@ sphinx_rtd_theme tox>=2.0 wheel>=0.22.0 git+https://github.com/mverteuil/pytest-ipdb.git +responses>=0.4.0 diff --git a/tests/conftest.py b/tests/conftest.py index d431bac..ad81465 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ from objectrocket import instances from objectrocket import constants - # def pytest_generate_tests(metafunc): # """Generate tests for the different instance types.""" # if '_docs' in metafunc.fixturenames: @@ -93,15 +92,15 @@ def mongodb_sharded_doc(): @pytest.fixture -def mongodb_replicaset_instance(client_token_auth, mongodb_replica_doc): +def mongodb_replicaset_instance(client, mongodb_replica_doc): return instances.MongodbInstance(instance_document=mongodb_replica_doc, - client=client_token_auth) + instances=client.instances) @pytest.fixture -def mongodb_sharded_instance(client_token_auth, mongodb_sharded_doc): +def mongodb_sharded_instance(client, mongodb_sharded_doc): return instances.MongodbInstance(instance_document=mongodb_sharded_doc, - client=client_token_auth) + instances=client.instances) ###################### @@ -122,10 +121,6 @@ def patched_requests_map(request): """ patches = {} - mocked = mock.patch('objectrocket.auth.requests', autospec=True) - request.addfinalizer(mocked.stop) - patches['auth'] = mocked.start() - mocked = mock.patch('objectrocket.instances.requests', autospec=True) request.addfinalizer(mocked.stop) patches['instances'] = mocked.start() diff --git a/tests/test_acl_sync.py b/tests/test_acl_sync.py new file mode 100644 index 0000000..6433e02 --- /dev/null +++ b/tests/test_acl_sync.py @@ -0,0 +1,127 @@ +import copy +import json +import mock +import pytest +import responses + +from objectrocket.bases import BaseInstance + +@pytest.yield_fixture(autouse=True) +def ensure_production_url(mongodb_sharded_instance, instance_acl_url): + """Fixture that ensures that the proper production URLs are used in tests, + instead of the potentially overridden ones from environment variables. + + See objectrocket.constants.OR_DEFAULT_API_URL + """ + inst = mongodb_sharded_instance + with mock.patch.object(BaseInstance, '_url', new_callable=mock.PropertyMock) as mock_url: + type(mock_url).return_value = instance_acl_url.replace('acl_sync', '') + yield + +@pytest.fixture +def instance_acl_url(mongodb_sharded_instance): + return "https://sjc-api.objectrocket.com/v2/instances/{}/acl_sync".format(mongodb_sharded_instance.name) + +class TestSetGetAclSync: + + @responses.activate + def test_acl_sync_disables(self, client, mongodb_sharded_instance, instance_acl_url): + inst = mongodb_sharded_instance + responses.add(responses.PUT, instance_acl_url, + status=200, + body=json.dumps({'data': {'aws_acl_sync_enabled': False, + 'rackspace_acl_sync_enabled': False}}), + content_type="application/json") + responses.add(responses.GET, instance_acl_url, status=200, + body=json.dumps({'data': {'aws_acl_sync_enabled': True, + 'rackspace_acl_sync_enabled': True}}), + content_type="application/json") + + response = inst.acl_sync(aws_sync=False, rackspace_sync=False) + + assert isinstance(response, dict) is True + assert response.get('data', {}).get('aws_acl_sync_enabled', True) is False + assert response.get('data', {}).get('rackspace_acl_sync_enabled', True) is False + + @responses.activate + def test_acl_sync_enables(self, client, mongodb_sharded_instance, instance_acl_url): + inst = mongodb_sharded_instance + responses.add(responses.PUT, instance_acl_url, + status=200, + body=json.dumps({'data': {'aws_acl_sync_enabled': True, + 'rackspace_acl_sync_enabled': True}}), + content_type="application/json") + responses.add(responses.GET, instance_acl_url, status=200, + body=json.dumps({'data': {'aws_acl_sync_enabled': False, + 'rackspace_acl_sync_enabled': False}}), + content_type="application/json") + + response = inst.acl_sync(aws_sync=False, rackspace_sync=False) + + assert isinstance(response, dict) is True + assert response.get('data', {}).get('aws_acl_sync_enabled', False) is True + assert response.get('data', {}).get('rackspace_acl_sync_enabled', False) is True + + @responses.activate + def test_acl_sync_just_returns_status(self, client, mongodb_sharded_instance, instance_acl_url): + inst = mongodb_sharded_instance + responses.add(responses.GET, instance_acl_url, status=200, + body=json.dumps({'data': {'aws_acl_sync_enabled': True, + 'rackspace_acl_sync_enabled': True}}), + content_type="application/json") + responses.add(responses.PUT, instance_acl_url, + status=200, + body=json.dumps({'data': {'aws_acl_sync_enabled': False, + 'rackspace_acl_sync_enabled': False}}), + content_type="application/json") + + response = inst.acl_sync() + + assert isinstance(response, dict) is True + assert response.get('data', {}).get('aws_acl_sync_enabled', True) is False + assert response.get('data', {}).get('rackspace_acl_sync_enabled', True) is False + + @responses.activate + def test_acl_sync_raises(self, client, mongodb_sharded_instance, instance_acl_url): + inst = mongodb_sharded_instance + exception = Exception('Test Exception') + responses.add(responses.GET, instance_acl_url, status=500, + body=exception) + + with pytest.raises(Exception) as exinfo: + response = inst.acl_sync() + + assert exinfo is not None + assert exinfo.value[0] == 'Test Exception' + +class TestRunAclSync: + + @responses.activate + def test_run_sync_without_args_is_unchanged(self, client, mongodb_sharded_instance, instance_acl_url): + inst = mongodb_sharded_instance + responses.add(responses.POST, instance_acl_url, + status=200, + body=json.dumps({'data': {'aws_acl_sync_state': 'unchanged', + 'rackspace_acl_sync_state': 'unchanged'}}), + content_type="application/json") + + response = inst.run_acl_sync() + + assert isinstance(response, dict) is True + assert response.get('data', {}).get('aws_acl_sync_state', None) == 'unchanged' + assert response.get('data', {}).get('rackspace_acl_sync_state', None) == 'unchanged' + + @responses.activate + def test_run_sync_starts(self, client, mongodb_sharded_instance, instance_acl_url): + inst = mongodb_sharded_instance + responses.add(responses.POST, instance_acl_url, + status=200, + body=json.dumps({'data': {'aws_acl_sync_state': 'started', + 'rackspace_acl_sync_state': 'started'}}), + content_type="application/json") + + response = inst.run_acl_sync(aws_sync=True, rackspace_sync=True) + + assert isinstance(response, dict) is True + assert response.get('data', {}).get('aws_acl_sync_state', None) == 'started' + assert response.get('data', {}).get('rackspace_acl_sync_state', None) == 'started' diff --git a/tests/test_auth.py b/tests/test_auth.py index 79b98d2..428682b 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,33 +1,61 @@ """Tests for the objectrocket.auth module.""" +import base64 +import json import mock import pytest +import responses from objectrocket import errors from objectrocket.auth import Auth - +from objectrocket.client import Client + +@pytest.fixture +def auth_url(mongodb_sharded_instance): + return "https://sjc-api.objectrocket.com/v2/tokens/" + +@pytest.yield_fixture(autouse=True) +def ensure_auth_production_url(auth_url): + """Fixture that ensures that the proper production URLs are used in tests, + instead of the potentially overridden ones from environment variables. + + See objectrocket.constants.OR_DEFAULT_API_URL + """ + with mock.patch.object(Auth, '_url', new_callable=mock.PropertyMock) as mock_auth_url: + type(mock_auth_url).return_value = auth_url + with mock.patch.object(Client, '_url', new_callable=mock.PropertyMock) as mock_client_url: + type(mock_client_url).return_value = auth_url.replace('tokens/', '') + yield + +@pytest.fixture() +def base64_basic_auth_header(): + """Just returns a properly formatted basic author header for testing""" + return 'Basic {}'.format(base64.encodestring('%s:%s' % ('tester', 'testpass')).replace('\n', '')) #################################### # Tests for Auth public interface. # #################################### -def test_authenticate_makes_expected_request(client, mocked_response, patched_requests_map): +@responses.activate +def test_authenticate_makes_expected_request(client, mocked_response, auth_url, base64_basic_auth_header): username, password, return_token = 'tester', 'testpass', 'return_token' - patched_requests_map['auth'].get.return_value = mocked_response - mocked_response.json.return_value = {'data': {'token': return_token}} + base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '') + responses.add(responses.GET, auth_url, status=200, + body=json.dumps({'data': {'token': return_token}}), + content_type="application/json") output = client.auth.authenticate(username, password) assert output == return_token - patched_requests_map['auth'].get.assert_called_with( - client.auth._url, - auth=(username, password), - **client.auth._default_request_kwargs - ) + + assert responses.calls[0].request.headers.get('Authorization') == base64_basic_auth_header + assert responses.calls[0].request.headers.get('Content-Type') == 'application/json' -def test_authenticate_binds_given_credentials(client, mocked_response, patched_requests_map): +@responses.activate +def test_authenticate_binds_given_credentials(client, mocked_response, auth_url): username, password, return_token = 'tester', 'testpass', 'return_token' - patched_requests_map['auth'].get.return_value = mocked_response - mocked_response.json.return_value = {'data': {'token': return_token}} + responses.add(responses.GET, auth_url, status=200, + body=json.dumps({'data': {'token': return_token}}), + content_type="application/json") orig_username, orig_password = client.auth._username, client.auth._password client.auth.authenticate(username, password) @@ -38,10 +66,12 @@ def test_authenticate_binds_given_credentials(client, mocked_response, patched_r assert client.auth._password == password -def test_authenticate_binds_auth_token_properly(client, mocked_response, patched_requests_map): +@responses.activate +def test_authenticate_binds_auth_token_properly(client, mocked_response, auth_url): username, password, return_token = 'tester', 'testpass', 'return_token' - patched_requests_map['auth'].get.return_value = mocked_response - mocked_response.json.return_value = {'data': {'token': return_token}} + responses.add(responses.GET, auth_url, status=200, + body=json.dumps({'data': {'token': return_token}}), + content_type="application/json") orig_token = client.auth._token client.auth.authenticate(username, password) @@ -50,11 +80,13 @@ def test_authenticate_binds_auth_token_properly(client, mocked_response, patched assert client.auth._token == return_token -def test_authenticate_raises_when_no_data_returned(client, mocked_response, patched_requests_map): +@responses.activate +def test_authenticate_raises_when_no_data_returned(client, mocked_response, auth_url): username, password = 'tester', 'testpass' auth = Auth(base_client=client) - patched_requests_map['auth'].get.return_value = mocked_response - mocked_response.json.return_value = {} + responses.add(responses.GET, auth_url, status=200, + body=json.dumps({}), + content_type="application/json") with pytest.raises(errors.AuthFailure) as exinfo: auth.authenticate(username, password) @@ -62,11 +94,13 @@ def test_authenticate_raises_when_no_data_returned(client, mocked_response, patc assert exinfo.value.args == ("KeyError: 'data'",) -def test_authenticate_raises_when_no_token_returned(client, mocked_response, patched_requests_map): +@responses.activate +def test_authenticate_raises_when_no_token_returned(client, mocked_response, auth_url): username, password = 'tester', 'testpass' auth = Auth(base_client=client) - patched_requests_map['auth'].get.return_value = mocked_response - mocked_response.json.return_value = {'data': {}} + responses.add(responses.GET, auth_url, status=200, + body=json.dumps({'data': {}}), + content_type="application/json") with pytest.raises(errors.AuthFailure) as exinfo: auth.authenticate(username, password) @@ -97,11 +131,13 @@ def test_auth_password_setter(client): assert orig_val is not testval -def test_auth_refresh_simply_invokes_authenticate_with_current_creds(client, mocked_response, patched_requests_map): +@responses.activate +def test_auth_refresh_simply_invokes_authenticate_with_current_creds(client, mocked_response, auth_url): # Assemble. username, password, return_token = 'tester', 'testpass', 'return_token' - patched_requests_map['auth'].get.return_value = mocked_response - mocked_response.json.return_value = {'data': {'token': return_token}} + responses.add(responses.GET, auth_url, status=200, + body=json.dumps({'data': {'token': return_token}}), + content_type="application/json") auth_output = client.auth.authenticate(username, password) bound_username, bound_password = client.auth._username, client.auth._password @@ -111,7 +147,7 @@ def test_auth_refresh_simply_invokes_authenticate_with_current_creds(client, moc refresh_output = client.auth._refresh() # Assert. - assert auth_output is refresh_output + assert auth_output == refresh_output patched_auth.assert_called_once_with(bound_username, bound_password) From 36d8b3333774991c8c0de0bfca97ef3fef750618 Mon Sep 17 00:00:00 2001 From: naemono Date: Tue, 3 Nov 2015 15:51:43 -0600 Subject: [PATCH 2/4] Version bump. --- objectrocket/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/objectrocket/__init__.py b/objectrocket/__init__.py index 0656f67..ec55a39 100644 --- a/objectrocket/__init__.py +++ b/objectrocket/__init__.py @@ -2,5 +2,5 @@ # Import this object for ease of access. from objectrocket.client import Client # noqa -VERSION = ('0', '3', '0') +VERSION = ('0', '3', '1') __version__ = '.'.join(VERSION) From b81e7b5ca60b1ed71f7d7cad73942dfde423af75 Mon Sep 17 00:00:00 2001 From: naemono Date: Tue, 3 Nov 2015 20:12:00 -0600 Subject: [PATCH 3/4] Resolving issues with python3.4 --- tests/test_acl_sync.py | 2 +- tests/test_auth.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_acl_sync.py b/tests/test_acl_sync.py index 6433e02..8a832e3 100644 --- a/tests/test_acl_sync.py +++ b/tests/test_acl_sync.py @@ -92,7 +92,7 @@ def test_acl_sync_raises(self, client, mongodb_sharded_instance, instance_acl_ur response = inst.acl_sync() assert exinfo is not None - assert exinfo.value[0] == 'Test Exception' + assert exinfo.value.args[0] == 'Test Exception' class TestRunAclSync: diff --git a/tests/test_auth.py b/tests/test_auth.py index 428682b..17380ae 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -29,7 +29,9 @@ def ensure_auth_production_url(auth_url): @pytest.fixture() def base64_basic_auth_header(): """Just returns a properly formatted basic author header for testing""" - return 'Basic {}'.format(base64.encodestring('%s:%s' % ('tester', 'testpass')).replace('\n', '')) + username = 'tester' + password = 'testpass' + return 'Basic ' + base64.encodestring(('%s:%s' % (username,password)).encode()).decode().replace('\n', '') #################################### # Tests for Auth public interface. # @@ -37,7 +39,6 @@ def base64_basic_auth_header(): @responses.activate def test_authenticate_makes_expected_request(client, mocked_response, auth_url, base64_basic_auth_header): username, password, return_token = 'tester', 'testpass', 'return_token' - base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '') responses.add(responses.GET, auth_url, status=200, body=json.dumps({'data': {'token': return_token}}), content_type="application/json") From 798dcf9f062d5cdb89da135fc34e1b3940c923b1 Mon Sep 17 00:00:00 2001 From: naemono Date: Thu, 5 Nov 2015 10:27:37 -0600 Subject: [PATCH 4/4] Removing json import and using requests library kwargs instead. Linter changes. --- objectrocket/bases.py | 5 ++--- tests/test_auth.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/objectrocket/bases.py b/objectrocket/bases.py index 76b0263..2f27f6d 100644 --- a/objectrocket/bases.py +++ b/objectrocket/bases.py @@ -1,6 +1,5 @@ """Base classes used throughout the library.""" import abc -import json import requests import six @@ -150,7 +149,7 @@ def acl_sync(self, aws_sync=None, rackspace_sync=None): if rackspace_sync is not None: data.update({"rackspace_acl_sync_enabled": rackspace_sync}) - response = requests.put(url, data=json.dumps(data), **self._instances._default_request_kwargs) + response = requests.put(url, json=data, **self._instances._default_request_kwargs) return response.json() else: raise errors.ObjectRocketException("Couldn't get current status of instance, failing. Error: {}".format(response.text)) @@ -170,7 +169,7 @@ def run_acl_sync(self, aws_sync=False, rackspace_sync=False): if rackspace_sync: data.update({"rackspace_acl_sync_enabled": True}) - response = requests.post(url, data=json.dumps(data), **self._instances._default_request_kwargs) + response = requests.post(url, json=data, **self._instances._default_request_kwargs) return response.json() @property diff --git a/tests/test_auth.py b/tests/test_auth.py index 17380ae..63fce15 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -9,6 +9,7 @@ from objectrocket.auth import Auth from objectrocket.client import Client + @pytest.fixture def auth_url(mongodb_sharded_instance): return "https://sjc-api.objectrocket.com/v2/tokens/"