diff --git a/heroku_connect/contrib/heroku_connect_health_check/backends.py b/heroku_connect/contrib/heroku_connect_health_check/backends.py index 71631f15..8e54974c 100644 --- a/heroku_connect/contrib/heroku_connect_health_check/backends.py +++ b/heroku_connect/contrib/heroku_connect_health_check/backends.py @@ -3,7 +3,7 @@ import logging from ...conf import settings -from ...utils import get_connection_id, get_connection_status +from ...utils import get_connections try: from health_check.backends import BaseHealthCheckBackend @@ -24,5 +24,11 @@ def check_status(self): if not (settings.HEROKU_AUTH_TOKEN and settings.HEROKU_CONNECT_APP_NAME): raise ServiceUnavailable('Both App Name and Auth Token are required') - connection_id = get_connection_id() - return get_connection_status(connection_id) == 'IDLE' + connections = get_connections(settings.HEROKU_CONNECT_APP_NAME) + for connection in connections: + if connection['state'] != 'IDLE': + self.add_error(ServiceUnavailable( + "Connection state for '%s' is '%s'" % ( + connection['name'], connection['state'] + ) + )) diff --git a/heroku_connect/utils.py b/heroku_connect/utils.py index a445c981..00c0789c 100644 --- a/heroku_connect/utils.py +++ b/heroku_connect/utils.py @@ -1,9 +1,7 @@ """Utility methods for Django Heroku Connect.""" +import os -import json -import urllib.request -from urllib.error import URLError - +import requests from django.db import DEFAULT_DB_ALIAS, connections from django.utils import timezone @@ -106,9 +104,15 @@ def create_heroku_connect_schema(using=DEFAULT_DB_ALIAS, **kwargs): return True -def get_connection_id(): +def _get_authorization_headers(): + return { + 'Authorization': 'Bearer %s' % settings.HEROKU_AUTH_TOKEN + } + + +def get_connections(app): """ - Get the first Heroku Connect's Connection ID from the connections API response. + Return all Heroku Connect connections setup with the given app. For more details check the link - https://devcenter.heroku.com/articles/heroku-connect-api#step-4-retrieve-the-new-connection-s-id @@ -126,28 +130,27 @@ def get_connection_id(): … } + Args: + app (str): Heroku application name. + Returns: - String: The connection ID. + List[dict]: List of all Heroku Connect connections associated with the Heroku application. Raises: - URLError: An error occurred when accessing the connections API. + requests.HTTPError: If an error occurred when accessing the connections API. + ValueError: If response is not a valid JSON. """ - req = urllib.request.Request('%s/v3/connections?app=%s' % ( - settings.HEROKU_CONNECT_API_ENDPOINT, settings.HEROKU_CONNECT_APP_NAME)) - req.add_header('-H', '"Authorization: Bearer %s"' % settings.HEROKU_AUTH_TOKEN) - try: - output = urllib.request.urlopen(req) - except URLError as e: - raise URLError('Unable to fetch connections') from e - - json_output = json.load(output) - return json_output['results'][0]['id'] + payload = {'app': app} + url = os.path.join(settings.HEROKU_CONNECT_API_ENDPOINT, 'connections') + response = requests.get(url, data=payload, headers=_get_authorization_headers()) + response.raise_for_status() + return response.json()['results'] -def get_connection_status(connection_id): +def get_connection(connection_id, deep=False): """ - Get Connection Status from the connection detail API response. + Get Heroku Connection connection information. For more details check the link - https://devcenter.heroku.com/articles/heroku-connect-api#step-8-monitor-the-connection-and-mapping-status @@ -181,22 +184,19 @@ def get_connection_status(connection_id): Args: connection_id (str): ID for Heroku Connect's connection. + deep (bool): Return information about the connection’s mappings, + in addition to the connection itself. Defaults to ``False``. Returns: - String: State for the Heroku Connect's connection. + dict: Heroku Connection connection information. Raises: - URLError: An error occurred when accessing the connection detail API. + requests.HTTPError: If an error occurred when accessing the connection detail API. + ValueError: If response is not a valid JSON. """ - req = urllib.request.Request('%s/connections/%s?deep=true' % ( - settings.HEROKU_CONNECT_API_ENDPOINT, connection_id)) - req.add_header('-H', '"Authorization: Bearer %s"' % settings.HEROKU_AUTH_TOKEN) - - try: - output = urllib.request.urlopen(req) - except URLError as e: - raise URLError('Unable to fetch connection details') from e - - json_output = json.load(output) - return json_output['state'] + url = os.path.join(settings.HEROKU_CONNECT_API_ENDPOINT, 'connections', connection_id) + payload = {'deep': deep} + response = requests.get(url, data=payload, headers=_get_authorization_headers()) + response.raise_for_status() + return response.json() diff --git a/requirements-dev.txt b/requirements-dev.txt index 9147f96c..f20ff470 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ -e . coverage django-health-check +httpretty isort psycopg2 pyenchant diff --git a/requirements.txt b/requirements.txt index debbb8e1..9b9b81b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ django-appconf -django>=1.11.0 \ No newline at end of file +django>=1.11.0 +requests diff --git a/setup.cfg b/setup.cfg index 1e263a1a..594f5eb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,4 +47,5 @@ line_length = 79 skip = manage.py,docs,env,.tox,.eggs known_first_party = heroku_connect,tests known_third_party = django,pytest,health_check +default_section=THIRDPARTY combine_as_imports = true diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/contrib/test_healthcheck.py b/tests/contrib/test_healthcheck.py index b32b4c9a..9dc4b0ad 100644 --- a/tests/contrib/test_healthcheck.py +++ b/tests/contrib/test_healthcheck.py @@ -1,29 +1,43 @@ +import json import secrets -from unittest import mock +import httpretty import pytest from health_check.exceptions import ServiceUnavailable from heroku_connect.contrib.heroku_connect_health_check.backends import ( HerokuConnectHealthCheck ) -from tests.test_utils import ( - ALL_CONNECTIONS_API_CALL_OUTPUT, CONNECTION_DETAILS_API_CALL_OUTPUT, - MockUrlLibResponse -) +from tests import fixtures + +@httpretty.activate +def test_check_status(): + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections", + body=json.dumps(fixtures.connections), + status=200, + content_type='application/json', + ) + hc = HerokuConnectHealthCheck() + hc.check_status() + assert not hc.errors -@mock.patch('urllib.request.urlopen') -def test_check_status(mock_get): - mock_get.side_effect = [MockUrlLibResponse(ALL_CONNECTIONS_API_CALL_OUTPUT), - MockUrlLibResponse(CONNECTION_DETAILS_API_CALL_OUTPUT)] - assert HerokuConnectHealthCheck().check_status() + connection = fixtures.connection.copy() + connection['state'] = 'error' + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections", + body=json.dumps({'results': [connection]}), + status=200, + content_type='application/json', + ) + hc = HerokuConnectHealthCheck() + hc.check_status() + assert hc.errors + assert hc.errors[0].message == "Connection state for 'sample name' is 'error'" -@mock.patch('urllib.request.urlopen') -def test_settings_exception(mock_get, settings): - mock_get.side_effect = [MockUrlLibResponse(ALL_CONNECTIONS_API_CALL_OUTPUT), - MockUrlLibResponse(CONNECTION_DETAILS_API_CALL_OUTPUT)] +def test_settings_exception(settings): settings.HEROKU_AUTH_TOKEN = None settings.HEROKU_CONNECT_APP_NAME = secrets.token_urlsafe() with pytest.raises(ServiceUnavailable): @@ -40,10 +54,14 @@ def test_settings_exception(mock_get, settings): HerokuConnectHealthCheck().check_status() -@mock.patch('urllib.request.urlopen') -def test_health_check_url(mock_get, client): - mock_get.side_effect = [MockUrlLibResponse(ALL_CONNECTIONS_API_CALL_OUTPUT), - MockUrlLibResponse(CONNECTION_DETAILS_API_CALL_OUTPUT)] +@httpretty.activate +def test_health_check_url(client): + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections", + body=json.dumps(fixtures.connections), + status=200, + content_type='application/json', + ) response = client.get('/ht/') assert response.status_code == 200 assert b'Heroku Connect' in response.content diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..8bb141fd --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,20 @@ +connection = { + "id": "1", + "name": "sample name", + "resource_name": "resource name", + "schema_name": "salesforce", + "db_key": "DATABASE_URL", + "state": "IDLE", + "mappings": [ + { + "id": "XYZ", + "object_name": "Account", + "state": "SCHEMA_CHANGED" + } + ] +} + +connections = { + "count": 1, + "results": [connection] +} diff --git a/tests/test_utils.py b/tests/test_utils.py index 57694a41..6db5e124 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,48 +1,16 @@ -from unittest import mock +import json from urllib.error import URLError +import httpretty import pytest +import requests from django.db import models from django.utils import timezone from heroku_connect import utils from heroku_connect.db.models import HerokuConnectModel - -class MockUrlLibResponse: - def __init__(self, data): - self.data = data - - def read(self): - return self.data.encode() - - -ALL_CONNECTIONS_API_CALL_OUTPUT = """{ - "count": 1, - "results": [ - { - "id": "1", - "name": "sample name", - "resource_name": "resource name" - } - ] - }""" - -CONNECTION_DETAILS_API_CALL_OUTPUT = """{ - "id": "1", - "name": "sample name", - "resource_name": "resource name", - "schema_name": "salesforce", - "db_key": "DATABASE_URL", - "state": "IDLE", - "mappings": [ - { - "id": "XYZ", - "object_name": "Account", - "state": "SCHEMA_CHANGED" - } - ] - }""" +from . import fixtures def test_get_heroku_connect_models(): @@ -178,21 +146,59 @@ def test_get_mapping(settings): ] -@mock.patch('urllib.request.urlopen') -def test_all_connections_api(mock_get): - mock_get.return_value = MockUrlLibResponse(ALL_CONNECTIONS_API_CALL_OUTPUT) - assert utils.get_connection_id() == '1' - mock_get.side_effect = URLError('not found') - with pytest.raises(URLError) as e: - utils.get_connection_id() - assert 'Unable to fetch connections' in str(e) - - -@mock.patch('urllib.request.urlopen') -def test_connection_detail_api(mock_get): - mock_get.return_value = MockUrlLibResponse(CONNECTION_DETAILS_API_CALL_OUTPUT) - assert utils.get_connection_status('1') == 'IDLE' - mock_get.side_effect = URLError('not found') - with pytest.raises(URLError) as e: - utils.get_connection_status('1') - assert 'Unable to fetch connection details' in str(e) +@httpretty.activate +def test_get_connections(): + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections", + body=json.dumps(fixtures.connections), + status=200, + content_type='application/json', + ) + assert utils.get_connections('ninja') == [fixtures.connection] + + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections", + body=json.dumps({'error': 'something is wrong'}), + status=500, + content_type='application/json', + ) + with pytest.raises(requests.HTTPError): + utils.get_connections('ninja') + + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections", + body='not-a-json', + status=200, + content_type='application/json', + ) + with pytest.raises(ValueError): + utils.get_connections('ninja') + + +@httpretty.activate +def test_get_connection(): + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections/1", + body=json.dumps(fixtures.connection), + status=200, + content_type='application/json', + ) + assert utils.get_connection('1') == fixtures.connection + + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections/1", + body=json.dumps({'error': 'something is wrong'}), + status=500, + content_type='application/json', + ) + with pytest.raises(requests.HTTPError): + utils.get_connection('1') + + httpretty.register_uri( + httpretty.GET, "https://connect-eu.heroku.com/api/v3/connections/1", + body='not-a-json', + status=200, + content_type='application/json', + ) + with pytest.raises(ValueError): + utils.get_connection('1') diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index ed054d74..44c45697 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -1,6 +1,7 @@ import os - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import secrets + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -8,7 +9,7 @@ # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '^-(6k&#x%oev5q*0yyqv3ohfwbxlc12klxf63d#uho%*s^4gfl' +SECRET_KEY = secrets.token_urlsafe() # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -118,5 +119,4 @@ HEROKU_CONNECT_APP_NAME = 'ninja' HEROKU_CONNECT_ORGANIZATION_ID = '1234567890' -HEROKU_AUTH_TOKEN = '1111111' -HEROKU_CONNECT_API_ENDPOINT = 'https://connect-eu.heroku.com/api/v3' +HEROKU_AUTH_TOKEN = secrets.token_hex()