Skip to content

Commit

Permalink
Rename json=1 to format=json and respect accept header order
Browse files Browse the repository at this point in the history
  • Loading branch information
codingjoe committed Apr 25, 2019
1 parent a964002 commit b880d53
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 13 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Getting machine readable JSON reports

If you want machine readable status reports you can request the ``/ht/``
endpoint with the ``Accept`` HTTP header set to ``application/json``
or pass ``json=1`` as a query parameter.
or pass ``format=json`` as a query parameter.

The backend will return a JSON response:

Expand All @@ -168,7 +168,7 @@ The backend will return a JSON response:
$ curl -v -X GET http://www.example.com/ht/?json=1
> GET /ht/?json=1 HTTP/1.1
> GET /ht/?format=json HTTP/1.1
> Host: www.example.com
>
< HTTP/1.1 200 OK
Expand Down
53 changes: 47 additions & 6 deletions health_check/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,43 @@
from health_check.plugins import plugin_dir


class MediaType:
"""
Sortable object representing HTTP's accept header.
.. seealso:: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
"""

def __init__(self, mime_type, weight=1.0):
self.mime_type = mime_type
self.weight = float(weight)

@classmethod
def from_string(cls, value):
"""Return single instance parsed from given accept header string."""
try:
return cls(*value.split(';'))
except ValueError:
return cls(value)

@classmethod
def parse_header(cls, value='*/*'):
"""Parse HTTP accept header and return instances sorted by weight."""
yield from sorted((cls.from_string(token.strip()) for token in value.split(',')), reverse=True)

def __str__(self):
return "%s;%d" % (self.mime_type, self.weight)

def __repr__(self):
return "%s: %s" % (type(self).__name__, self.__str__())

def __eq__(self, other):
return self.weight == other.weight and self.mime_type == other.mime_type

def __lt__(self, other):
return self.weight.__lt__(other.weight)


class MainView(TemplateView):
template_name = 'health_check/index.html'

Expand Down Expand Up @@ -43,14 +80,18 @@ def _run(plugin):

status_code = 500 if errors else 200

accept_format = request.META.get('HTTP_ACCEPT', '')
accepts_json = 'application/json' in accept_format
format_override = request.GET.get('format')

if accepts_json or request.GET.get('json', False):
if format_override == 'json':
return self.render_to_response_json(plugins, status_code)
else:
context = {'plugins': plugins, 'status_code': status_code}
return self.render_to_response(context, status=status_code)

accept_header = request.META.get('HTTP_ACCEPT', '*/*')
for media in MediaType.parse_header(accept_header):
if media.mime_type in ['text/html', ' application/xhtml+xml', '*/*']:
context = {'plugins': plugins, 'status_code': status_code}
return self.render_to_response(context, status=status_code)
if 'application/json' == media.mime_type:
return self.render_to_response_json(plugins, status_code)

def render_to_response_json(self, plugins, status):
return JsonResponse(
Expand Down
83 changes: 78 additions & 5 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,48 @@
from health_check.conf import HEALTH_CHECK
from health_check.exceptions import ServiceWarning
from health_check.plugins import plugin_dir
from health_check.views import MediaType

try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse



class TestMediaType:

def test_lt(self):
assert not MediaType('*/*') < MediaType('*/*')
assert not MediaType('*/*') < MediaType('*/*', 0.9)
assert MediaType('*/*', 0.9) < MediaType('*/*')

def test_str(self):
assert str(MediaType('*/*')) == "*/*;1"

def test_repr(self):
assert repr(MediaType('*/*')) == "MediaType: */*;1"

def test_eq(self):
assert MediaType('*/*') == MediaType('*/*')
assert MediaType('*/*', 0.9) != MediaType('*/*')

def test_from_string(self):
assert MediaType.from_string('*/*') == MediaType('*/*')
assert MediaType.from_string('*/*;0.9') == MediaType('*/*', 0.9)
assert MediaType.from_string('*/*;0.9') == MediaType('*/*', 0.9)

def test_parse_header(self):
assert list(MediaType.parse_header()) == [
MediaType('*/*'),
]
assert list(MediaType.parse_header('text/html;0.1,application/xhtml+xml;0.1,application/json')) == [
MediaType('application/json'),
MediaType('text/html', 0.1),
MediaType('application/xhtml+xml', 0.1),
]


class TestMainView:
url = reverse('health_check:health_check_home')

Expand Down Expand Up @@ -71,10 +106,48 @@ def run_check(self):
plugin_dir.reset()
plugin_dir.register(JSONSuccessBackend)
response = client.get(self.url, HTTP_ACCEPT='application/json')
assert response.status_code == 200, response.content.decode('utf-8')
assert response['content-type'] == 'application/json'
assert json.loads(response.content.decode('utf-8')) == \
{JSONSuccessBackend().identifier(): JSONSuccessBackend().pretty_status()}

def test_success_accept_order(self, client):
class JSONSuccessBackend(BaseHealthCheckBackend):
def run_check(self):
pass

plugin_dir.reset()
plugin_dir.register(JSONSuccessBackend)
response = client.get(self.url, HTTP_ACCEPT='text/html,application/xhtml+xml,application/json;0.9,*/*;0.1')
assert response['content-type'] == 'text/html; charset=utf-8'

def test_success_accept_order__reverse(self, client):
class JSONSuccessBackend(BaseHealthCheckBackend):
def run_check(self):
pass

plugin_dir.reset()
plugin_dir.register(JSONSuccessBackend)
response = client.get(self.url, HTTP_ACCEPT='text/html;0.1,application/xhtml+xml;0.1,application/json')
assert response['content-type'] == 'application/json'

def test_format_override(self, client):
class JSONSuccessBackend(BaseHealthCheckBackend):
def run_check(self):
pass

plugin_dir.reset()
plugin_dir.register(JSONSuccessBackend)
response = client.get(self.url + '?format=json', HTTP_ACCEPT='text/html')
assert response['content-type'] == 'application/json'

def test_format_no_accept_header(self, client):
class JSONSuccessBackend(BaseHealthCheckBackend):
def run_check(self):
pass

plugin_dir.reset()
plugin_dir.register(JSONSuccessBackend)
response = client.get(self.url)
assert response.status_code == 200, response.content.decode('utf-8')
assert response['content-type'] == 'text/html; charset=utf-8'

def test_error_accept_json(self, client):
class JSONErrorBackend(BaseHealthCheckBackend):
Expand All @@ -95,7 +168,7 @@ def run_check(self):

plugin_dir.reset()
plugin_dir.register(JSONSuccessBackend)
response = client.get(self.url, {'json': 1})
response = client.get(self.url, {'format': 'json'})
assert response.status_code == 200, response.content.decode('utf-8')
assert response['content-type'] == 'application/json'
assert json.loads(response.content.decode('utf-8')) == \
Expand All @@ -108,7 +181,7 @@ def run_check(self):

plugin_dir.reset()
plugin_dir.register(JSONErrorBackend)
response = client.get(self.url, {'json': 1})
response = client.get(self.url, {'format': 'json'})
assert response.status_code == 500, response.content.decode('utf-8')
assert response['content-type'] == 'application/json'
assert 'JSON Error' in json.loads(response.content.decode('utf-8'))[JSONErrorBackend().identifier()]

0 comments on commit b880d53

Please sign in to comment.