Browse Source

First stab at API, POST /api/v1/checks

pull/46/head
Pēteris Caune 9 years ago
parent
commit
5d2edfa4a0
7 changed files with 161 additions and 41 deletions
  1. +1
    -4
      hc/accounts/views.py
  2. +62
    -1
      hc/api/decorators.py
  3. +8
    -0
      hc/api/schemas.py
  4. +67
    -0
      hc/api/tests/test_create_check.py
  5. +0
    -17
      hc/api/tests/test_status.py
  6. +1
    -1
      hc/api/urls.py
  7. +22
    -18
      hc/api/views.py

+ 1
- 4
hc/accounts/views.py View File

@ -174,10 +174,7 @@ def set_password(request, token):
messages.info(request, "Your password has been set!") messages.info(request, "Your password has been set!")
return redirect("hc-profile") 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): def unsubscribe_reports(request, username):


+ 62
- 1
hc/api/decorators.py View File

@ -1,7 +1,9 @@
import json
import uuid import uuid
from functools import wraps 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): def uuid_or_400(f):
@ -14,3 +16,62 @@ def uuid_or_400(f):
return f(request, *args, **kwds) return f(request, *args, **kwds)
return wrapper 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
- 0
hc/api/schemas.py View 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
- 0
hc/api/tests/test_create_check.py View 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")

+ 0
- 17
hc/api/tests/test_status.py View File

@ -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

+ 1
- 1
hc/api/urls.py View File

@ -5,6 +5,6 @@ from hc.api import views
urlpatterns = [ urlpatterns = [
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"), url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"), 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'^handle_email/$', views.handle_email, name="hc-handle-email"),
url(r'^api/v1/checks/$', views.create_check),
] ]

+ 22
- 18
hc/api/views.py View File

@ -1,13 +1,14 @@
from datetime import timedelta as td
import json import json
from django.contrib.humanize.templatetags.humanize import naturaltime
from django.db.models import F 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.utils import timezone
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt 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 from hc.api.models import Check, Ping
@ -75,22 +76,25 @@ def handle_email(request):
return response return response
@uuid_or_400
def status(request, code):
response = {
"last_ping": None,
"last_ping_human": None,
"secs_to_alert": None
}
@csrf_exempt
@check_api_key
@validate_json(schemas.check)
def create_check(request):
if request.method != "POST":
return HttpResponse(status=405)
check = Check.objects.get(code=code)
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"])
if check.last_ping and check.alert_after:
response["last_ping"] = check.last_ping.isoformat()
response["last_ping_human"] = naturaltime(check.last_ping)
check.save()
duration = check.alert_after - timezone.now()
response["secs_to_alert"] = int(duration.total_seconds())
response = {
"ping_url": check.url()
}
return HttpResponse(json.dumps(response),
content_type="application/javascript")
return JsonResponse(response, status=201)

Loading…
Cancel
Save