Skip to content

Commit

Permalink
Fix django health check
Browse files Browse the repository at this point in the history
Add requests to simplifly API calls. Make sure to test all connections
associated with an application. Reduce API calls.
  • Loading branch information
codingjoe committed Jan 22, 2018
1 parent 517fe16 commit 4115581
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 113 deletions.
12 changes: 9 additions & 3 deletions heroku_connect/contrib/heroku_connect_health_check/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']
)
))
66 changes: 33 additions & 33 deletions heroku_connect/utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-e .
coverage
django-health-check
httpretty
isort
psycopg2
pyenchant
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
django-appconf
django>=1.11.0
django>=1.11.0
requests
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Empty file removed tests/conftest.py
Empty file.
54 changes: 36 additions & 18 deletions tests/contrib/test_healthcheck.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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'<td>Heroku Connect</td>' in response.content
20 changes: 20 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -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]
}
114 changes: 60 additions & 54 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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')
Loading

0 comments on commit 4115581

Please sign in to comment.