forked from GithubBackups/healthchecks
parent
e090aa5403
commit
855d188981
@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Increase "Success / Failure Keywords" field lengths to 200
|
- Increase "Success / Failure Keywords" field lengths to 200
|
||||||
- Django 3.2.2
|
- Django 3.2.2
|
||||||
- Improve the handling of unknown email addresses in the Sign In form
|
- Improve the handling of unknown email addresses in the Sign In form
|
||||||
|
- Add support for "... is UP" SMS notifications
|
||||||
|
|
||||||
## v1.20.0 - 2020-04-22
|
## v1.20.0 - 2020-04-22
|
||||||
|
|
||||||
|
27
hc/api/migrations/0078_sms_values.py
Normal file
27
hc/api/migrations/0078_sms_values.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.2.2 on 2021-05-21 09:15
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_sms_values(apps, schema_editor):
|
||||||
|
Channel = apps.get_model("api", "Channel")
|
||||||
|
for ch in Channel.objects.filter(kind="sms").only("value"):
|
||||||
|
if ch.value.startswith("{"):
|
||||||
|
doc = json.loads(ch.value)
|
||||||
|
phone_number = doc["value"]
|
||||||
|
else:
|
||||||
|
phone_number = ch.value
|
||||||
|
|
||||||
|
ch.value = json.dumps({"value": phone_number, "up": False, "down": True})
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("api", "0077_auto_20210506_0755"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(normalize_sms_values, migrations.RunPython.noop)]
|
@ -739,6 +739,18 @@ class Channel(models.Model):
|
|||||||
doc = json.loads(self.value)
|
doc = json.loads(self.value)
|
||||||
return doc["down"]
|
return doc["down"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sms_notify_up(self):
|
||||||
|
assert self.kind == "sms"
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
return doc["up"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sms_notify_down(self):
|
||||||
|
assert self.kind == "sms"
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
return doc["down"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def opsgenie_key(self):
|
def opsgenie_key(self):
|
||||||
assert self.kind == "opsgenie"
|
assert self.kind == "opsgenie"
|
||||||
|
@ -141,6 +141,7 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_call_limit(self, mock_post):
|
def test_call_limit(self, mock_post):
|
||||||
# At limit already:
|
# At limit already:
|
||||||
|
self.profile.call_limit = 50
|
||||||
self.profile.last_call_date = now()
|
self.profile.last_call_date = now()
|
||||||
self.profile.calls_sent = 50
|
self.profile.calls_sent = 50
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
@ -164,11 +165,12 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_call_limit_reset(self, mock_post):
|
def test_call_limit_reset(self, mock_post):
|
||||||
# At limit, but also into a new month
|
# At limit, but also into a new month
|
||||||
|
self.profile.call_limit = 50
|
||||||
self.profile.calls_sent = 50
|
self.profile.calls_sent = 50
|
||||||
self.profile.last_call_date = now() - td(days=100)
|
self.profile.last_call_date = now() - td(days=100)
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
self._setup_data("sms", "+1234567890")
|
self._setup_data("call", "+1234567890")
|
||||||
mock_post.return_value.status_code = 200
|
mock_post.return_value.status_code = 200
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
|
@ -10,23 +10,24 @@ from hc.api.models import Channel, Check, Notification
|
|||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
class NotifyTestCase(BaseTestCase):
|
class NotifySmsTestCase(BaseTestCase):
|
||||||
def _setup_data(self, value):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
self.check = Check(project=self.project)
|
self.check = Check(project=self.project)
|
||||||
self.check.status = "down"
|
self.check.status = "down"
|
||||||
self.check.last_ping = now() - td(minutes=61)
|
self.check.last_ping = now() - td(minutes=61)
|
||||||
self.check.save()
|
self.check.save()
|
||||||
|
|
||||||
|
spec = {"value": "+1234567890", "up": False, "down": True}
|
||||||
self.channel = Channel(project=self.project, kind="sms")
|
self.channel = Channel(project=self.project, kind="sms")
|
||||||
self.channel.value = value
|
self.channel.value = json.dumps(spec)
|
||||||
self.channel.save()
|
self.channel.save()
|
||||||
self.channel.checks.add(self.check)
|
self.channel.checks.add(self.check)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_it_works(self, mock_post):
|
def test_it_works(self, mock_post):
|
||||||
self._setup_data("+1234567890")
|
|
||||||
self.check.last_ping = now() - td(hours=2)
|
self.check.last_ping = now() - td(hours=2)
|
||||||
|
|
||||||
mock_post.return_value.status_code = 200
|
mock_post.return_value.status_code = 200
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
@ -34,7 +35,8 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
args, kwargs = mock_post.call_args
|
args, kwargs = mock_post.call_args
|
||||||
payload = kwargs["data"]
|
payload = kwargs["data"]
|
||||||
self.assertEqual(payload["To"], "+1234567890")
|
self.assertEqual(payload["To"], "+1234567890")
|
||||||
self.assertFalse("\xa0" in payload["Body"])
|
self.assertNotIn("\xa0", payload["Body"])
|
||||||
|
self.assertIn("is DOWN", payload["Body"])
|
||||||
|
|
||||||
n = Notification.objects.get()
|
n = Notification.objects.get()
|
||||||
callback_path = f"/api/v1/notifications/{n.code}/status"
|
callback_path = f"/api/v1/notifications/{n.code}/status"
|
||||||
@ -44,21 +46,6 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
self.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertEqual(self.profile.sms_sent, 1)
|
self.assertEqual(self.profile.sms_sent, 1)
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
|
||||||
def test_it_handles_json_value(self, mock_post):
|
|
||||||
value = {"label": "foo", "value": "+1234567890"}
|
|
||||||
self._setup_data(json.dumps(value))
|
|
||||||
self.check.last_ping = now() - td(hours=2)
|
|
||||||
|
|
||||||
mock_post.return_value.status_code = 200
|
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
assert Notification.objects.count() == 1
|
|
||||||
|
|
||||||
args, kwargs = mock_post.call_args
|
|
||||||
payload = kwargs["data"]
|
|
||||||
self.assertEqual(payload["To"], "+1234567890")
|
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_it_enforces_limit(self, mock_post):
|
def test_it_enforces_limit(self, mock_post):
|
||||||
# At limit already:
|
# At limit already:
|
||||||
@ -66,8 +53,6 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
self.profile.sms_sent = 50
|
self.profile.sms_sent = 50
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
self._setup_data("+1234567890")
|
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
self.assertFalse(mock_post.called)
|
self.assertFalse(mock_post.called)
|
||||||
|
|
||||||
@ -88,7 +73,6 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
self.profile.last_sms_date = now() - td(days=100)
|
self.profile.last_sms_date = now() - td(days=100)
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
self._setup_data("+1234567890")
|
|
||||||
mock_post.return_value.status_code = 200
|
mock_post.return_value.status_code = 200
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
@ -96,7 +80,6 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_it_does_not_escape_special_characters(self, mock_post):
|
def test_it_does_not_escape_special_characters(self, mock_post):
|
||||||
self._setup_data("+1234567890")
|
|
||||||
self.check.name = "Foo > Bar & Co"
|
self.check.name = "Foo > Bar & Co"
|
||||||
self.check.last_ping = now() - td(hours=2)
|
self.check.last_ping = now() - td(hours=2)
|
||||||
|
|
||||||
@ -107,3 +90,26 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
args, kwargs = mock_post.call_args
|
args, kwargs = mock_post.call_args
|
||||||
payload = kwargs["data"]
|
payload = kwargs["data"]
|
||||||
self.assertIn("Foo > Bar & Co", payload["Body"])
|
self.assertIn("Foo > Bar & Co", payload["Body"])
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_it_handles_disabled_down_notification(self, mock_post):
|
||||||
|
payload = {"value": "+123123123", "up": True, "down": False}
|
||||||
|
self.channel.value = json.dumps(payload)
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
self.assertFalse(mock_post.called)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_it_sends_up_notification(self, mock_post):
|
||||||
|
payload = {"value": "+123123123", "up": True, "down": False}
|
||||||
|
self.channel.value = json.dumps(payload)
|
||||||
|
|
||||||
|
self.check.last_ping = now()
|
||||||
|
self.check.status = "up"
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
args, kwargs = mock_post.call_args
|
||||||
|
payload = kwargs["data"]
|
||||||
|
self.assertIn("is UP", payload["Body"])
|
||||||
|
@ -491,7 +491,10 @@ class Sms(HttpTransport):
|
|||||||
URL = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json"
|
URL = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json"
|
||||||
|
|
||||||
def is_noop(self, check):
|
def is_noop(self, check):
|
||||||
return check.status != "down"
|
if check.status == "down":
|
||||||
|
return not self.channel.sms_notify_down
|
||||||
|
else:
|
||||||
|
return not self.channel.sms_notify_up
|
||||||
|
|
||||||
def notify(self, check):
|
def notify(self, check):
|
||||||
profile = Profile.objects.for_user(self.channel.project.owner)
|
profile = Profile.objects.for_user(self.channel.project.owner)
|
||||||
|
@ -24,7 +24,7 @@ class AddSmsTestCase(BaseTestCase):
|
|||||||
self.assertContains(r, "upgrade to a")
|
self.assertContains(r, "upgrade to a")
|
||||||
|
|
||||||
def test_it_creates_channel(self):
|
def test_it_creates_channel(self):
|
||||||
form = {"label": "My Phone", "phone": "+1234567890"}
|
form = {"label": "My Phone", "phone": "+1234567890", "down": True}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
@ -34,6 +34,8 @@ class AddSmsTestCase(BaseTestCase):
|
|||||||
self.assertEqual(c.kind, "sms")
|
self.assertEqual(c.kind, "sms")
|
||||||
self.assertEqual(c.phone_number, "+1234567890")
|
self.assertEqual(c.phone_number, "+1234567890")
|
||||||
self.assertEqual(c.name, "My Phone")
|
self.assertEqual(c.name, "My Phone")
|
||||||
|
self.assertTrue(c.sms_notify_down)
|
||||||
|
self.assertFalse(c.sms_notify_up)
|
||||||
self.assertEqual(c.project, self.project)
|
self.assertEqual(c.project, self.project)
|
||||||
|
|
||||||
def test_it_rejects_bad_number(self):
|
def test_it_rejects_bad_number(self):
|
||||||
@ -95,3 +97,17 @@ class AddSmsTestCase(BaseTestCase):
|
|||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.phone_number, "+1234567890")
|
self.assertEqual(c.phone_number, "+1234567890")
|
||||||
|
|
||||||
|
def test_it_obeys_up_down_flags(self):
|
||||||
|
form = {"label": "My Phone", "phone": "+1234567890"}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post(self.url, form)
|
||||||
|
self.assertRedirects(r, self.channels_url)
|
||||||
|
|
||||||
|
c = Channel.objects.get()
|
||||||
|
self.assertEqual(c.kind, "sms")
|
||||||
|
self.assertEqual(c.phone_number, "+1234567890")
|
||||||
|
self.assertEqual(c.name, "My Phone")
|
||||||
|
self.assertFalse(c.sms_notify_down)
|
||||||
|
self.assertFalse(c.sms_notify_up)
|
||||||
|
@ -121,3 +121,23 @@ class ChannelsTestCase(BaseTestCase):
|
|||||||
self.assertNotContains(r, "Add Integration", status_code=200)
|
self.assertNotContains(r, "Add Integration", status_code=200)
|
||||||
self.assertNotContains(r, "ic-delete")
|
self.assertNotContains(r, "ic-delete")
|
||||||
self.assertNotContains(r, "edit_webhook")
|
self.assertNotContains(r, "edit_webhook")
|
||||||
|
|
||||||
|
def test_it_shows_down_only_note_for_sms(self):
|
||||||
|
channel = Channel(project=self.project, kind="sms")
|
||||||
|
channel.value = json.dumps({"value": "+123123123", "up": False, "down": True})
|
||||||
|
channel.save()
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.get(self.channels_url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertContains(r, "(down only)")
|
||||||
|
|
||||||
|
def test_it_shows_up_only_note_for_sms(self):
|
||||||
|
channel = Channel(project=self.project, kind="sms")
|
||||||
|
channel.value = json.dumps({"value": "+123123123", "up": True, "down": False})
|
||||||
|
channel.save()
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.get(self.channels_url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertContains(r, "(up only)")
|
||||||
|
@ -1638,7 +1638,7 @@ def add_telegram(request):
|
|||||||
def add_sms(request, code):
|
def add_sms(request, code):
|
||||||
project = _get_rw_project_for_user(request, code)
|
project = _get_rw_project_for_user(request, code)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = forms.PhoneNumberForm(request.POST)
|
form = forms.PhoneUpDownForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
channel = Channel(project=project, kind="sms")
|
channel = Channel(project=project, kind="sms")
|
||||||
channel.name = form.cleaned_data["label"]
|
channel.name = form.cleaned_data["label"]
|
||||||
@ -1648,7 +1648,7 @@ def add_sms(request, code):
|
|||||||
channel.assign_all_checks()
|
channel.assign_all_checks()
|
||||||
return redirect("hc-channels", project.code)
|
return redirect("hc-channels", project.code)
|
||||||
else:
|
else:
|
||||||
form = forms.PhoneNumberForm()
|
form = forms.PhoneUpDownForm(initial={"up": False})
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"page": "channels",
|
"page": "channels",
|
||||||
|
@ -68,6 +68,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif ch.kind == "sms" %}
|
{% elif ch.kind == "sms" %}
|
||||||
SMS to <span>{{ ch.phone_number }}</span>
|
SMS to <span>{{ ch.phone_number }}</span>
|
||||||
|
{% if ch.sms_notify_down and not ch.sms_notify_up %}
|
||||||
|
(down only)
|
||||||
|
{% endif %}
|
||||||
|
{% if ch.sms_notify_up and not ch.sms_notify_down %}
|
||||||
|
(up only)
|
||||||
|
{% endif %}
|
||||||
{% elif ch.kind == "call" %}
|
{% elif ch.kind == "call" %}
|
||||||
Phone call to <span>{{ ch.phone_number }}</span>
|
Phone call to <span>{{ ch.phone_number }}</span>
|
||||||
{% elif ch.kind == "trello" %}
|
{% elif ch.kind == "trello" %}
|
||||||
|
@ -73,6 +73,35 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="add-email-notify-group" class="form-group {{ form.down.css_classes }}">
|
||||||
|
<label class="col-sm-2 control-label">Notify When</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="down"
|
||||||
|
value="true"
|
||||||
|
{% if form.down.value %} checked {% endif %}>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
A check goes <strong>down</strong>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="up"
|
||||||
|
value="true"
|
||||||
|
{% if form.up.value %} checked {% endif %}>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
A check goes <strong>up</strong>
|
||||||
|
</span>
|
||||||
|
{% if form.down.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{{ form.down.errors|join:"" }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||||
|
@ -1 +1,6 @@
|
|||||||
{% load humanize %}{{ site_name }}: The check "{{ check.name_then_code|safe }}" is DOWN. Last ping was {{ check.last_ping|naturaltime }}.
|
{% load humanize %}
|
||||||
|
{% if check.status == "down" %}
|
||||||
|
{{ site_name }}: The check "{{ check.name_then_code|safe }}" is DOWN. Last ping was {{ check.last_ping|naturaltime }}.
|
||||||
|
{% else %}
|
||||||
|
{{ site_name }}: The check "{{ check.name_then_code|safe }}" is UP.
|
||||||
|
{% endif %}
|
Loading…
x
Reference in New Issue
Block a user