diff --git a/hc/api/tests/test_ping.py b/hc/api/tests/test_ping.py index ce33698c..6f39a381 100644 --- a/hc/api/tests/test_ping.py +++ b/hc/api/tests/test_ping.py @@ -11,7 +11,7 @@ class PingTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check.objects.create(project=self.project) - self.url = "/ping/%s" % self.check.code + self.url = f"/ping/{self.check.code}" def test_it_works(self): r = self.client.get(self.url) @@ -23,7 +23,7 @@ class PingTestCase(BaseTestCase): expected_aa = self.check.last_ping + td(days=1, hours=1) self.assertEqual(self.check.alert_after, expected_aa) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.scheme, "http") self.assertEqual(ping.kind, None) self.assertEqual(ping.created, self.check.last_ping) @@ -54,7 +54,7 @@ class PingTestCase(BaseTestCase): r = csrf_client.post(self.url, "hello world", content_type="text/plain") self.assertEqual(r.status_code, 200) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.method, "POST") self.assertEqual(ping.body, "hello world") @@ -87,7 +87,7 @@ class PingTestCase(BaseTestCase): r = self.client.get(self.url, HTTP_USER_AGENT=ua) self.assertEqual(r.status_code, 200) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.ua, ua) def test_it_truncates_long_ua(self): @@ -96,26 +96,27 @@ class PingTestCase(BaseTestCase): r = self.client.get(self.url, HTTP_USER_AGENT=ua) self.assertEqual(r.status_code, 200) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(len(ping.ua), 200) assert ua.startswith(ping.ua) def test_it_reads_forwarded_ip(self): ip = "1.1.1.1" r = self.client.get(self.url, HTTP_X_FORWARDED_FOR=ip) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(r.status_code, 200) self.assertEqual(ping.remote_addr, "1.1.1.1") + def test_it_reads_first_forwarded_ip(self): ip = "1.1.1.1, 2.2.2.2" r = self.client.get(self.url, HTTP_X_FORWARDED_FOR=ip, REMOTE_ADDR="3.3.3.3",) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(r.status_code, 200) self.assertEqual(ping.remote_addr, "1.1.1.1") def test_it_reads_forwarded_protocol(self): r = self.client.get(self.url, HTTP_X_FORWARDED_PROTO="https") - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(r.status_code, 200) self.assertEqual(ping.scheme, "https") @@ -132,7 +133,7 @@ class PingTestCase(BaseTestCase): self.assertTrue(self.check.has_confirmation_link) def test_fail_endpoint_works(self): - r = self.client.get("/ping/%s/fail" % self.check.code) + r = self.client.get(self.url + "/fail") self.assertEqual(r.status_code, 200) self.check.refresh_from_db() @@ -151,7 +152,7 @@ class PingTestCase(BaseTestCase): self.check.last_ping = last_ping self.check.save() - r = self.client.get("/ping/%s/start" % self.check.code) + r = self.client.get(self.url + "/start") self.assertEqual(r.status_code, 200) self.check.refresh_from_db() @@ -165,7 +166,7 @@ class PingTestCase(BaseTestCase): self.check.status = "paused" self.check.save() - r = self.client.get("/ping/%s/start" % self.check.code) + r = self.client.get(self.url + "/start") self.assertEqual(r.status_code, 200) self.check.refresh_from_db() @@ -193,7 +194,7 @@ class PingTestCase(BaseTestCase): self.assertEqual(self.check.status, "new") self.assertIsNone(self.check.last_ping) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.scheme, "http") self.assertEqual(ping.kind, "ign") @@ -201,7 +202,7 @@ class PingTestCase(BaseTestCase): def test_it_chops_long_body(self): self.client.post(self.url, "hello world", content_type="text/plain") - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.method, "POST") self.assertEqual(ping.body, "hello") @@ -209,7 +210,7 @@ class PingTestCase(BaseTestCase): def test_it_allows_unlimited_body(self): self.client.post(self.url, "A" * 20000, content_type="text/plain") - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(len(ping.body), 20000) def test_it_handles_manual_resume_flag(self): @@ -223,28 +224,28 @@ class PingTestCase(BaseTestCase): self.check.refresh_from_db() self.assertEqual(self.check.status, "paused") - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.scheme, "http") self.assertEqual(ping.kind, "ign") def test_zero_exit_status_works(self): - r = self.client.get("/ping/%s/0" % self.check.code) + r = self.client.get(self.url + "/0") self.assertEqual(r.status_code, 200) self.check.refresh_from_db() self.assertEqual(self.check.status, "up") - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.kind, None) self.assertEqual(ping.exitstatus, 0) def test_nonzero_exit_status_works(self): - r = self.client.get("/ping/%s/123" % self.check.code) + r = self.client.get(self.url + "/123") self.assertEqual(r.status_code, 200) self.check.refresh_from_db() self.assertEqual(self.check.status, "down") - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertEqual(ping.kind, "fail") self.assertEqual(ping.exitstatus, 123) diff --git a/hc/api/tests/test_ping_by_slug.py b/hc/api/tests/test_ping_by_slug.py new file mode 100644 index 00000000..92d49f0e --- /dev/null +++ b/hc/api/tests/test_ping_by_slug.py @@ -0,0 +1,82 @@ +from django.test import Client +from hc.api.models import Check, Ping +from hc.test import BaseTestCase + + +class PingBySlugTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.check = Check.objects.create(project=self.project, name="foo", slug="foo") + self.url = f"/ping/{self.project.ping_key}/foo" + + def test_it_works(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Access-Control-Allow-Origin"], "*") + + ping = Ping.objects.get() + self.assertEqual(ping.kind, None) + + def test_post_works(self): + csrf_client = Client(enforce_csrf_checks=True) + r = csrf_client.post(self.url, "hello world", content_type="text/plain") + self.assertEqual(r.status_code, 200) + + ping = Ping.objects.get() + self.assertEqual(ping.method, "POST") + self.assertEqual(ping.body, "hello world") + + def test_head_works(self): + csrf_client = Client(enforce_csrf_checks=True) + r = csrf_client.head(self.url) + self.assertEqual(r.status_code, 200) + self.assertEqual(Ping.objects.count(), 1) + + def test_it_handles_missing_check(self): + r = self.client.get(f"/ping/{self.project.ping_key}/bar") + self.assertEqual(r.status_code, 404) + + def test_it_never_caches(self): + r = self.client.get(self.url) + assert "no-cache" in r.get("Cache-Control") + + def test_fail_endpoint_works(self): + r = self.client.get(self.url + "/fail") + self.assertEqual(r.status_code, 200) + + ping = Ping.objects.get() + self.assertEqual(ping.kind, "fail") + + def test_start_endpoint_works(self): + r = self.client.get(self.url + "/start") + self.assertEqual(r.status_code, 200) + + ping = Ping.objects.get() + self.assertEqual(ping.kind, "start") + + def test_zero_exit_status_works(self): + r = self.client.get(self.url + "/0") + self.assertEqual(r.status_code, 200) + + ping = Ping.objects.get() + self.assertEqual(ping.kind, None) + self.assertEqual(ping.exitstatus, 0) + + def test_nonzero_exit_status_works(self): + r = self.client.get(self.url + "/123") + self.assertEqual(r.status_code, 200) + + ping = Ping.objects.get() + self.assertEqual(ping.kind, "fail") + self.assertEqual(ping.exitstatus, 123) + + def test_it_handles_duplicates(self): + # Another check with the same slug: + Check.objects.create(project=self.project, name="foo", slug="foo") + + r = self.client.get(self.url) + self.assertEqual(r.status_code, 409) + + def test_it_handles_wrong_ping_key(self): + r = self.client.get("/ping/rrrrrrrrrrrrrrrrrrrrrr/foo") + self.assertEqual(r.status_code, 404) diff --git a/hc/api/urls.py b/hc/api/urls.py index 833fa2c4..13fb9c66 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -1,6 +1,6 @@ from urllib.parse import quote, unquote -from django.urls import path, register_converter +from django.urls import include, path, register_converter from hc.api import views @@ -27,13 +27,24 @@ class SHA1Converter: register_converter(QuoteConverter, "quoted") register_converter(SHA1Converter, "sha1") +uuid_urls = [ + path("", views.ping, name="hc-ping"), + path("fail", views.ping, {"action": "fail"}), + path("start", views.ping, {"action": "start"}), + path("", views.ping), +] + +slug_urls = [ + path("fail", views.ping_by_slug, {"action": "fail"}), + path("start", views.ping_by_slug, {"action": "start"}), + path("", views.ping_by_slug), +] + urlpatterns = [ - path("ping//", views.ping, name="hc-ping-slash"), - path("ping/", views.ping, name="hc-ping"), - path("ping//fail", views.ping, {"action": "fail"}, name="hc-fail"), - path("ping//start", views.ping, {"action": "start"}, name="hc-start"), - path("ping//", views.ping), + path("ping/", views.ping), + path("ping//", include(uuid_urls)), path("ping//", views.ping_by_slug), + path("ping///", include(slug_urls)), path("api/v1/checks/", views.checks), path("api/v1/checks/", views.single, name="hc-api-single"), path("api/v1/checks/", views.get_check_by_unique_key), diff --git a/hc/api/views.py b/hc/api/views.py index 97aee228..35fc3ff8 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -31,8 +31,9 @@ class BadChannelException(Exception): @csrf_exempt @never_cache -def ping(request, code, action="success", exitstatus=None): - check = get_object_or_404(Check, code=code) +def ping(request, code, check=None, action="success", exitstatus=None): + if check is None: + check = get_object_or_404(Check, code=code) headers = request.META remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"]) @@ -55,9 +56,14 @@ def ping(request, code, action="success", exitstatus=None): return response +@csrf_exempt def ping_by_slug(request, ping_key, slug, action="success", exitstatus=None): - check = get_object_or_404(Check, slug=slug, project__ping_key=ping_key) - return ping(request, check.code, action, exitstatus) + try: + check = get_object_or_404(Check, slug=slug, project__ping_key=ping_key) + except Check.MultipleObjectsReturned: + return HttpResponse("ambiguous slug", status=409) + + return ping(request, check.code, check, action, exitstatus) def _lookup(project, spec): diff --git a/hc/test.py b/hc/test.py index 1e2239b4..2413ad99 100644 --- a/hc/test.py +++ b/hc/test.py @@ -17,6 +17,7 @@ class BaseTestCase(TestCase): self.project = Project(owner=self.alice, api_key="X" * 32) self.project.name = "Alices Project" self.project.badge_key = self.alice.username + self.project.ping_key = "p" * 22 self.project.save() self.profile = Profile(user=self.alice)