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'