forked from GithubBackups/healthchecks
First stab at API, POST /api/v1/checks
This commit is contained in:
parent
144cf0da90
commit
5d2edfa4a0
@ -174,10 +174,7 @@ def set_password(request, token):
|
||||
messages.info(request, "Your password has been set!")
|
||||
return redirect("hc-profile")
|
||||
|
||||
ctx = {
|
||||
}
|
||||
|
||||
return render(request, "accounts/set_password.html", ctx)
|
||||
return render(request, "accounts/set_password.html", {})
|
||||
|
||||
|
||||
def unsubscribe_reports(request, username):
|
||||
|
@ -1,7 +1,9 @@
|
||||
import json
|
||||
import uuid
|
||||
from functools import wraps
|
||||
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
|
||||
|
||||
def uuid_or_400(f):
|
||||
@ -14,3 +16,62 @@ def uuid_or_400(f):
|
||||
|
||||
return f(request, *args, **kwds)
|
||||
return wrapper
|
||||
|
||||
|
||||
def make_error(msg):
|
||||
return JsonResponse({"error": msg}, status=400)
|
||||
|
||||
|
||||
def check_api_key(f):
|
||||
@wraps(f)
|
||||
def wrapper(request, *args, **kwds):
|
||||
try:
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
except ValueError:
|
||||
return make_error("could not parse request body")
|
||||
|
||||
api_key = str(data.get("api_key", ""))
|
||||
if api_key == "":
|
||||
return make_error("wrong api_key")
|
||||
|
||||
try:
|
||||
user = User.objects.get(profile__api_key=api_key)
|
||||
except User.DoesNotExist:
|
||||
return make_error("wrong api_key")
|
||||
|
||||
request.json = data
|
||||
request.user = user
|
||||
return f(request, *args, **kwds)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def validate_json(schema):
|
||||
""" Validate request.json contents against `schema`.
|
||||
|
||||
Supports a tiny subset of JSON schema spec.
|
||||
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapper(request, *args, **kwds):
|
||||
for key, spec in schema["properties"].items():
|
||||
if key not in request.json:
|
||||
continue
|
||||
|
||||
value = request.json[key]
|
||||
if spec["type"] == "string":
|
||||
if not isinstance(value, str):
|
||||
return make_error("%s is not a string" % key)
|
||||
elif spec["type"] == "number":
|
||||
if not isinstance(value, int):
|
||||
return make_error("%s is not a number" % key)
|
||||
if "minimum" in spec and value < spec["minimum"]:
|
||||
return make_error("%s is too small" % key)
|
||||
if "maximum" in spec and value > spec["maximum"]:
|
||||
return make_error("%s is too large" % key)
|
||||
|
||||
return f(request, *args, **kwds)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
8
hc/api/schemas.py
Normal file
8
hc/api/schemas.py
Normal file
@ -0,0 +1,8 @@
|
||||
check = {
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"tags": {"type": "string"},
|
||||
"timeout": {"type": "number", "minimum": 60, "maximum": 604800},
|
||||
"grace": {"type": "number", "minimum": 60, "maximum": 604800}
|
||||
}
|
||||
}
|
67
hc/api/tests/test_create_check.py
Normal file
67
hc/api/tests/test_create_check.py
Normal file
@ -0,0 +1,67 @@
|
||||
import json
|
||||
|
||||
from hc.api.models import Check
|
||||
from hc.test import BaseTestCase
|
||||
from hc.accounts.models import Profile
|
||||
|
||||
|
||||
class CreateCheckTestCase(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CreateCheckTestCase, self).setUp()
|
||||
self.profile = Profile(user=self.alice, api_key="abc")
|
||||
self.profile.save()
|
||||
|
||||
def post(self, url, data):
|
||||
return self.client.post(url, json.dumps(data),
|
||||
content_type="application/json")
|
||||
|
||||
def test_it_works(self):
|
||||
r = self.post("/api/v1/checks/", {
|
||||
"api_key": "abc",
|
||||
"name": "Foo",
|
||||
"tags": "bar,baz",
|
||||
"timeout": 3600,
|
||||
"grace": 60
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 201)
|
||||
self.assertTrue("ping_url" in r.json())
|
||||
|
||||
self.assertEqual(Check.objects.count(), 1)
|
||||
check = Check.objects.get()
|
||||
self.assertEqual(check.name, "Foo")
|
||||
self.assertEqual(check.tags, "bar,baz")
|
||||
self.assertEqual(check.timeout.total_seconds(), 3600)
|
||||
self.assertEqual(check.grace.total_seconds(), 60)
|
||||
|
||||
def test_it_handles_missing_request_body(self):
|
||||
r = self.client.post("/api/v1/checks/",
|
||||
content_type="application/json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.json()["error"], "wrong api_key")
|
||||
|
||||
def test_it_rejects_wrong_api_key(self):
|
||||
r = self.post("/api/v1/checks/", {"api_key": "wrong"})
|
||||
self.assertEqual(r.json()["error"], "wrong api_key")
|
||||
|
||||
def test_it_handles_invalid_json(self):
|
||||
r = self.client.post("/api/v1/checks/", "this is not json",
|
||||
content_type="application/json")
|
||||
self.assertEqual(r.json()["error"], "could not parse request body")
|
||||
|
||||
def test_it_reject_small_timeout(self):
|
||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 0})
|
||||
self.assertEqual(r.json()["error"], "timeout is too small")
|
||||
|
||||
def test_it_rejects_large_timeout(self):
|
||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 604801})
|
||||
self.assertEqual(r.json()["error"], "timeout is too large")
|
||||
|
||||
def test_it_rejects_non_number_timeout(self):
|
||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": "oops"})
|
||||
self.assertEqual(r.json()["error"], "timeout is not a number")
|
||||
|
||||
def test_it_rejects_non_string_name(self):
|
||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "name": False})
|
||||
self.assertEqual(r.json()["error"], "name is not a string")
|
@ -1,17 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from hc.api.models import Check
|
||||
|
||||
|
||||
class StatusTestCase(TestCase):
|
||||
|
||||
def test_it_works(self):
|
||||
check = Check()
|
||||
check.save()
|
||||
|
||||
r = self.client.get("/status/%s/" % check.code)
|
||||
self.assertContains(r, "last_ping", status_code=200)
|
||||
|
||||
def test_it_handles_bad_uuid(self):
|
||||
r = self.client.get("/status/not-uuid/")
|
||||
assert r.status_code == 400
|
@ -5,6 +5,6 @@ from hc.api import views
|
||||
urlpatterns = [
|
||||
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
||||
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
||||
url(r'^status/([\w-]+)/$', views.status, name="hc-status"),
|
||||
url(r'^handle_email/$', views.handle_email, name="hc-handle-email"),
|
||||
url(r'^api/v1/checks/$', views.create_check),
|
||||
]
|
||||
|
@ -1,13 +1,14 @@
|
||||
from datetime import timedelta as td
|
||||
import json
|
||||
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.db.models import F
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from hc.api.decorators import uuid_or_400
|
||||
from hc.api import schemas
|
||||
from hc.api.decorators import check_api_key, uuid_or_400, validate_json
|
||||
from hc.api.models import Check, Ping
|
||||
|
||||
|
||||
@ -75,22 +76,25 @@ def handle_email(request):
|
||||
return response
|
||||
|
||||
|
||||
@uuid_or_400
|
||||
def status(request, code):
|
||||
@csrf_exempt
|
||||
@check_api_key
|
||||
@validate_json(schemas.check)
|
||||
def create_check(request):
|
||||
if request.method != "POST":
|
||||
return HttpResponse(status=405)
|
||||
|
||||
check = Check(user=request.user)
|
||||
check.name = str(request.json.get("name", ""))
|
||||
check.tags = str(request.json.get("tags", ""))
|
||||
if "timeout" in request.json:
|
||||
check.timeout = td(seconds=request.json["timeout"])
|
||||
if "grace" in request.json:
|
||||
check.grace = td(seconds=request.json["grace"])
|
||||
|
||||
check.save()
|
||||
|
||||
response = {
|
||||
"last_ping": None,
|
||||
"last_ping_human": None,
|
||||
"secs_to_alert": None
|
||||
"ping_url": check.url()
|
||||
}
|
||||
|
||||
check = Check.objects.get(code=code)
|
||||
|
||||
if check.last_ping and check.alert_after:
|
||||
response["last_ping"] = check.last_ping.isoformat()
|
||||
response["last_ping_human"] = naturaltime(check.last_ping)
|
||||
|
||||
duration = check.alert_after - timezone.now()
|
||||
response["secs_to_alert"] = int(duration.total_seconds())
|
||||
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/javascript")
|
||||
return JsonResponse(response, status=201)
|
||||
|
Loading…
x
Reference in New Issue
Block a user