From d88f99a712454adbef7798b2415fc957263fa648 Mon Sep 17 00:00:00 2001 From: James Kirsop Date: Tue, 25 Feb 2020 12:48:54 +1100 Subject: [PATCH 1/3] Changes to prototype this for testing with real data --- hc/api/tests/test_update_check.py | 5 ----- hc/api/urls.py | 1 + hc/api/views.py | 21 +++++++++++++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/hc/api/tests/test_update_check.py b/hc/api/tests/test_update_check.py index a46eec39..7c9225b6 100644 --- a/hc/api/tests/test_update_check.py +++ b/hc/api/tests/test_update_check.py @@ -72,11 +72,6 @@ class UpdateCheckTestCase(BaseTestCase): check = Check.objects.get() self.assertEqual(check.channel_set.count(), 0) - def test_it_requires_post(self): - url = "/api/v1/checks/%s" % self.check.code - r = self.client.get(url, HTTP_X_API_KEY="X" * 32) - self.assertEqual(r.status_code, 405) - def test_it_handles_invalid_uuid(self): r = self.post("not-an-uuid", {"api_key": "X" * 32}) self.assertEqual(r.status_code, 404) diff --git a/hc/api/urls.py b/hc/api/urls.py index 5ff1b5f3..f4e5a3ef 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path("ping//fail", views.ping, {"action": "fail"}, name="hc-fail"), path("ping//start", views.ping, {"action": "start"}, name="hc-start"), path("api/v1/checks/", views.checks), + path("api/v1/checks/", views.single, name="hc-api-single"), path("api/v1/checks/", views.update, name="hc-api-update"), path("api/v1/checks//pause", views.pause, name="hc-api-pause"), path("api/v1/notifications//bounce", views.bounce, name="hc-api-bounce"), diff --git a/hc/api/views.py b/hc/api/views.py index bc675d57..a8ec08f1 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -19,6 +19,7 @@ from hc.api import schemas from hc.api.decorators import authorize, authorize_read, cors, validate_json from hc.api.models import Check, Notification, Channel from hc.lib.badges import check_signature, get_badge_svg +from hc.lib.jsonschema import ValidationError, validate class BadChannelException(Exception): @@ -178,18 +179,19 @@ def channels(request): @csrf_exempt -@cors("POST", "DELETE") -@validate_json(schemas.check) +@cors("POST", "DELETE", "GET") +@validate_json() @authorize -def update(request, code): +def single(request, code): check = get_object_or_404(Check, code=code) if check.project != request.project: return HttpResponseForbidden() if request.method == "POST": try: + validate(request.json, schemas.check) _update(check, request.json) - except BadChannelException as e: + except (BadChannelException,ValidationError) as e: return JsonResponse({"error": str(e)}, status=400) return JsonResponse(check.to_dict()) @@ -199,10 +201,21 @@ def update(request, code): check.delete() return JsonResponse(response) + elif request.method == "GET": + return JsonResponse(check.to_dict()) + # Otherwise, method not allowed return HttpResponse(status=405) +@csrf_exempt +@cors("POST", "DELETE") +@validate_json(schemas.check) +@authorize +def update(request, code): + single(request, code) + + @cors("POST") @csrf_exempt @validate_json() From 6373db8aa1d57f94f6aa02b6c9c43db622bb062e Mon Sep 17 00:00:00 2001 From: James Kirsop Date: Tue, 25 Feb 2020 12:48:54 +1100 Subject: [PATCH 2/3] Changes to prototype this for testing with real data --- hc/api/tests/test_update_check.py | 5 ----- hc/api/urls.py | 1 + hc/api/views.py | 21 +++++++++++++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/hc/api/tests/test_update_check.py b/hc/api/tests/test_update_check.py index a46eec39..7c9225b6 100644 --- a/hc/api/tests/test_update_check.py +++ b/hc/api/tests/test_update_check.py @@ -72,11 +72,6 @@ class UpdateCheckTestCase(BaseTestCase): check = Check.objects.get() self.assertEqual(check.channel_set.count(), 0) - def test_it_requires_post(self): - url = "/api/v1/checks/%s" % self.check.code - r = self.client.get(url, HTTP_X_API_KEY="X" * 32) - self.assertEqual(r.status_code, 405) - def test_it_handles_invalid_uuid(self): r = self.post("not-an-uuid", {"api_key": "X" * 32}) self.assertEqual(r.status_code, 404) diff --git a/hc/api/urls.py b/hc/api/urls.py index 5ff1b5f3..f4e5a3ef 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path("ping//fail", views.ping, {"action": "fail"}, name="hc-fail"), path("ping//start", views.ping, {"action": "start"}, name="hc-start"), path("api/v1/checks/", views.checks), + path("api/v1/checks/", views.single, name="hc-api-single"), path("api/v1/checks/", views.update, name="hc-api-update"), path("api/v1/checks//pause", views.pause, name="hc-api-pause"), path("api/v1/notifications//bounce", views.bounce, name="hc-api-bounce"), diff --git a/hc/api/views.py b/hc/api/views.py index bc675d57..a8ec08f1 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -19,6 +19,7 @@ from hc.api import schemas from hc.api.decorators import authorize, authorize_read, cors, validate_json from hc.api.models import Check, Notification, Channel from hc.lib.badges import check_signature, get_badge_svg +from hc.lib.jsonschema import ValidationError, validate class BadChannelException(Exception): @@ -178,18 +179,19 @@ def channels(request): @csrf_exempt -@cors("POST", "DELETE") -@validate_json(schemas.check) +@cors("POST", "DELETE", "GET") +@validate_json() @authorize -def update(request, code): +def single(request, code): check = get_object_or_404(Check, code=code) if check.project != request.project: return HttpResponseForbidden() if request.method == "POST": try: + validate(request.json, schemas.check) _update(check, request.json) - except BadChannelException as e: + except (BadChannelException,ValidationError) as e: return JsonResponse({"error": str(e)}, status=400) return JsonResponse(check.to_dict()) @@ -199,10 +201,21 @@ def update(request, code): check.delete() return JsonResponse(response) + elif request.method == "GET": + return JsonResponse(check.to_dict()) + # Otherwise, method not allowed return HttpResponse(status=405) +@csrf_exempt +@cors("POST", "DELETE") +@validate_json(schemas.check) +@authorize +def update(request, code): + single(request, code) + + @cors("POST") @csrf_exempt @validate_json() From 456a80f1fa7a91871fe770bd549a112b92bb6c73 Mon Sep 17 00:00:00 2001 From: James Kirsop Date: Mon, 23 Mar 2020 11:37:32 +1100 Subject: [PATCH 3/3] Adding tests and docs --- hc/api/models.py | 2 +- hc/api/tests/test_get_check.py | 55 ++++++++++++++++++++++++++++++++++ hc/api/urls.py | 1 - templates/docs/api.html | 38 +++++++++++++++++++++++ templates/docs/api.md | 40 +++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 hc/api/tests/test_get_check.py diff --git a/hc/api/models.py b/hc/api/models.py index 3f980b2c..dcc8105a 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -224,7 +224,7 @@ class Check(models.Model): if readonly: result["unique_key"] = self.unique_key else: - update_rel_url = reverse("hc-api-update", args=[self.code]) + update_rel_url = reverse("hc-api-single", args=[self.code]) pause_rel_url = reverse("hc-api-pause", args=[self.code]) result["ping_url"] = self.url() diff --git a/hc/api/tests/test_get_check.py b/hc/api/tests/test_get_check.py new file mode 100644 index 00000000..3b0c30a8 --- /dev/null +++ b/hc/api/tests/test_get_check.py @@ -0,0 +1,55 @@ +from datetime import timedelta as td +import uuid + +from django.utils.timezone import now +from hc.api.models import Channel, Check +from hc.test import BaseTestCase + + +class GetCheckTestCase(BaseTestCase): + def setUp(self): + super(GetCheckTestCase, self).setUp() + + self.now = now().replace(microsecond=0) + + self.a1 = Check(project=self.project, name="Alice 1") + self.a1.timeout = td(seconds=3600) + self.a1.grace = td(seconds=900) + self.a1.n_pings = 0 + self.a1.status = "new" + self.a1.tags = "a1-tag a1-additional-tag" + self.a1.desc = "This is description" + self.a1.save() + + self.c1 = Channel.objects.create(project=self.project) + self.a1.channel_set.add(self.c1) + + def get(self, code): + url = "/api/v1/checks/%s" % code + return self.client.get(url, HTTP_X_API_KEY="X" * 32) + + def test_it_works(self): + r = self.get(self.a1.code) + self.assertEqual(r.status_code, 200) + self.assertEqual(r["Access-Control-Allow-Origin"], "*") + + doc = r.json() + self.assertEqual(len(doc), 13) + + self.assertEqual(doc["timeout"], 3600) + self.assertEqual(doc["grace"], 900) + self.assertEqual(doc["ping_url"], self.a1.url()) + self.assertEqual(doc["last_ping"], None) + self.assertEqual(doc["n_pings"], 0) + self.assertEqual(doc["status"], "new") + self.assertEqual(doc["channels"], str(self.c1.code)) + self.assertEqual(doc["desc"], "This is description") + + def test_it_handles_invalid_uuid(self): + r = self.get("not-an-uuid") + self.assertEqual(r.status_code, 404) + + def test_it_handles_missing_check(self): + made_up_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02" + r = self.get(made_up_code) + self.assertEqual(r.status_code, 404) \ No newline at end of file diff --git a/hc/api/urls.py b/hc/api/urls.py index f4e5a3ef..28050a25 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -23,7 +23,6 @@ urlpatterns = [ path("ping//start", views.ping, {"action": "start"}, name="hc-start"), path("api/v1/checks/", views.checks), path("api/v1/checks/", views.single, name="hc-api-single"), - path("api/v1/checks/", views.update, name="hc-api-update"), path("api/v1/checks//pause", views.pause, name="hc-api-pause"), path("api/v1/notifications//bounce", views.bounce, name="hc-api-bounce"), path("api/v1/channels/", views.channels), diff --git a/templates/docs/api.html b/templates/docs/api.html index 9f0801a6..c896f2f3 100644 --- a/templates/docs/api.html +++ b/templates/docs/api.html @@ -15,6 +15,10 @@ checks in user's account.

GET SITE_ROOT/api/v1/checks/ +Create a single check +GET SITE_ROOT/api/v1/checks/<uuid> + + Create a new check POST SITE_ROOT/api/v1/checks/ @@ -174,6 +178,40 @@ is added. This identifier is stable across API calls. Example:

+

Get a single Check

+

GET SITE_ROOT/api/v1/checks/<uuid>

+

Returns a JSON object containing information information from a single check.

+

Response Codes

+
+
200 OK
+
The request succeeded.
+
401 Unauthorized
+
The API key is either missing or invalid.
+
+

Example Request

+
curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/<uuid>
+
+ + +

Example Response

+
{
+  "channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc",
+  "desc": "Longer free-form description goes here",
+  "grace": 900,
+  "last_ping": "2017-01-04T13:24:39.903464+00:00",
+  "n_pings": 1,
+  "name": "Api test 1",
+  "next_ping": "2017-01-04T14:24:39.903464+00:00",
+  "pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
+  "ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6",
+  "status": "up",
+  "tags": "foo",
+  "timeout": 3600,
+  "update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"
+}
+
+ +

Create a Check

POST SITE_ROOT/api/v1/checks/

Creates a new check and returns its ping URL. diff --git a/templates/docs/api.md b/templates/docs/api.md index f23d69b6..d04bac9e 100644 --- a/templates/docs/api.md +++ b/templates/docs/api.md @@ -8,6 +8,7 @@ checks in user's account. Endpoint Name | Endpoint Address ------------------------------------------------------|------- [Get a list of existing checks](#list-checks) | `GET SITE_ROOT/api/v1/checks/` +[Create a single check](#get-check) | `GET SITE_ROOT/api/v1/checks/` [Create a new check](#create-check) | `POST SITE_ROOT/api/v1/checks/` [Update an existing check](#update-check) | `POST SITE_ROOT/api/v1/checks/` [Pause monitoring of a check](#pause-check) | `POST SITE_ROOT/api/v1/checks//pause` @@ -157,6 +158,45 @@ is added. This identifier is stable across API calls. Example: } ``` +## Get a single Check {: #get-check .rule } +`GET SITE_ROOT/api/v1/checks/` + +Returns a JSON object containing information information from a single check. + +### Response Codes + +200 OK +: The request succeeded. + +401 Unauthorized +: The API key is either missing or invalid. + +### Example Request + +```bash +curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/ +``` + +### Example Response + +```json +{ + "channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc", + "desc": "Longer free-form description goes here", + "grace": 900, + "last_ping": "2017-01-04T13:24:39.903464+00:00", + "n_pings": 1, + "name": "Api test 1", + "next_ping": "2017-01-04T14:24:39.903464+00:00", + "pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause", + "ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6", + "status": "up", + "tags": "foo", + "timeout": 3600, + "update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6" +} +``` + ## Create a Check {: #create-check .rule } `POST SITE_ROOT/api/v1/checks/`