class ChannelChecksTestCase(BaseTestCase):

    def setUp(self):
        super(ChannelChecksTestCase, self).setUp()
        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        url = "/integrations/%s/checks/" % self.channel.code

        self.client.login(username="******", password="******")
        r = self.client.get(url)
        self.assertContains(r, "*****@*****.**", status_code=200)

    def test_it_checks_owner(self):
        mallory = User(username="******", email="*****@*****.**")
        mallory.set_password("password")
        mallory.save()

        # channel does not belong to mallory so this should come back
        # with 403 Forbidden:
        url = "/integrations/%s/checks/" % self.channel.code
        self.client.login(username="******", password="******")
        r = self.client.get(url)
        assert r.status_code == 403

    def test_missing_channel(self):
        # Valid UUID but there is no channel for it:
        url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/checks/"

        self.client.login(username="******", password="******")
        r = self.client.get(url)
        assert r.status_code == 404
class VerifyEmailTestCase(BaseTestCase):

    def setUp(self):
        super(VerifyEmailTestCase, self).setUp()
        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        token = self.channel.make_token()
        url = "/integrations/%s/verify/%s/" % (self.channel.code, token)

        r = self.client.post(url)
        assert r.status_code == 200, r.status_code

        channel = Channel.objects.get(code=self.channel.code)
        assert channel.email_verified

    def test_it_handles_bad_token(self):
        url = "/integrations/%s/verify/bad-token/" % self.channel.code

        r = self.client.post(url)
        assert r.status_code == 200, r.status_code

        channel = Channel.objects.get(code=self.channel.code)
        assert not channel.email_verified

    def test_missing_channel(self):
        # Valid UUID, and even valid token but there is no channel for it:
        code = "6837d6ec-fc08-4da5-a67f-08a9ed1ccf62"
        token = self.channel.make_token()
        url = "/integrations/%s/verify/%s/" % (code, token)

        r = self.client.post(url)
        assert r.status_code == 404
Esempio n. 3
0
    def test_it_shows_channel_list_with_pushbullet(self):
        self.client.login(username="******", password="******")

        ch = Channel(user=self.alice, kind="pushbullet", value="test-token")
        ch.save()

        r = self.client.get("/admin/api/channel/")
        self.assertContains(r, "Pushbullet")
Esempio n. 4
0
    def test_it_assigns_channels(self):
        channel = Channel(user=self.alice)
        channel.save()

        r = self.post({"api_key": "abc", "channels": "*"})

        self.assertEqual(r.status_code, 201)
        check = Check.objects.get()
        self.assertEqual(check.channel_set.get(), channel)
Esempio n. 5
0
def add_hipchat(request):
    if "installable_url" in request.GET:
        url = request.GET["installable_url"]
        assert url.startswith("https://api.hipchat.com")
        response = requests.get(url)
        if "oauthId" not in response.json():
            messages.warning(request, "Something went wrong!")
            return redirect("hc-channels")

        channel = Channel(kind="hipchat")
        channel.user = request.team.user
        channel.value = response.text
        channel.save()

        channel.refresh_hipchat_access_token()
        channel.assign_all_checks()
        messages.success(request, "The HipChat integration has been added!")
        return redirect("hc-channels")

    install_url = "https://www.hipchat.com/addons/install?" + urlencode({
        "url": settings.SITE_ROOT + reverse("hc-hipchat-capabilities")
    })

    ctx = {
        "page": "channels",
        "install_url": install_url
    }
    return render(request, "integrations/add_hipchat.html", ctx)
Esempio n. 6
0
    def test_it_shows_pushover_notifications(self):
        ch = Channel(kind="po", user=self.alice)
        ch.save()

        Notification(owner=self.check, channel=ch, check_status="down").save()

        url = "/checks/%s/log/" % self.check.code

        self.client.login(username="******", password="******")
        r = self.client.get(url)
        self.assertContains(r, "Sent a Pushover notification", status_code=200)
Esempio n. 7
0
    def test_it_shows_webhook_notifications(self):
        ch = Channel(kind="webhook", user=self.alice, value="foo/$NAME")
        ch.save()

        Notification(owner=self.check, channel=ch, check_status="down").save()

        url = "/checks/%s/log/" % self.check.code

        self.client.login(username="******", password="******")
        r = self.client.get(url)
        self.assertContains(r, "Called webhook foo/$NAME", status_code=200)
    def test_it_refreshes_hipchat_access_token(self, mock_post):
        mock_post.return_value.json.return_value = {"expires_in": 100}

        channel = Channel(kind="hipchat", user=self.alice, value=json.dumps({
            "oauthId": "foo",
            "oauthSecret": "bar"
        }))

        channel.refresh_hipchat_access_token()

        # It should request a token using a correct tokenUrl
        mock_post.assert_called()
        self.assertTrue("expires_at" in channel.value)
class RemoveChannelTestCase(BaseTestCase):

    def setUp(self):
        super(RemoveChannelTestCase, self).setUp()
        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        url = "/integrations/%s/remove/" % self.channel.code

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        self.assertRedirects(r, "/integrations/")

        assert Channel.objects.count() == 0

    def test_team_access_works(self):
        url = "/integrations/%s/remove/" % self.channel.code

        self.client.login(username="******", password="******")
        self.client.post(url)
        assert Channel.objects.count() == 0

    def test_it_handles_bad_uuid(self):
        url = "/integrations/not-uuid/remove/"

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        assert r.status_code == 400

    def test_it_checks_owner(self):
        url = "/integrations/%s/remove/" % self.channel.code

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        assert r.status_code == 403

    def test_it_handles_missing_uuid(self):
        # Valid UUID but there is no channel for it:
        url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/"

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        assert r.status_code == 302

    def test_it_rejects_get(self):
        url = "/integrations/%s/remove/" % self.channel.code
        self.client.login(username="******", password="******")
        r = self.client.get(url)
        self.assertEqual(r.status_code, 405)
Esempio n. 10
0
def add_slack_btn(request):
    code = request.GET.get("code", "")
    if len(code) < 8:
        return HttpResponseBadRequest()

    result = requests.post("https://slack.com/api/oauth.access", {
        "client_id": settings.SLACK_CLIENT_ID,
        "client_secret": settings.SLACK_CLIENT_SECRET,
        "code": code
    })

    doc = result.json()
    if doc.get("ok"):
        channel = Channel()
        channel.user = request.team.user
        channel.kind = "slack"
        channel.value = result.text
        channel.save()
        channel.assign_all_checks()
        messages.success(request, "The Slack integration has been added!")
    else:
        s = doc.get("error")
        messages.warning(request, "Error message from slack: %s" % s)

    return redirect("hc-channels")
    def test_it_checks_check_user(self):
        charlies_channel = Channel(user=self.charlie, kind="email")
        charlies_channel.email = "*****@*****.**"
        charlies_channel.save()

        payload = {
            "channel": charlies_channel.code,
            "check-%s" % self.check.code: True
        }
        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)

        # mc belongs to charlie but self.check does not--
        assert r.status_code == 403
Esempio n. 12
0
    def test_it_unassigns_channels(self):
        channel = Channel(user=self.alice)
        channel.save()

        self.check.assign_all_channels()

        r = self.post(self.check.code, {
            "api_key": "abc",
            "channels": ""
        })

        self.assertEqual(r.status_code, 200)
        check = Check.objects.get()
        self.assertEqual(check.channel_set.count(), 0)
Esempio n. 13
0
    def test_it_formats_complex_slack_value(self):
        ch = Channel(kind="slack", user=self.alice)
        ch.value = json.dumps({
            "ok": True,
            "team_name": "foo-team",
            "incoming_webhook": {
                "url": "http://example.org",
                "channel": "#bar"
            }
        })
        ch.save()

        self.client.login(username="******", password="******")
        r = self.client.get("/integrations/")
        self.assertContains(r, "foo-team", status_code=200)
        self.assertContains(r, "#bar")
    def setUp(self):
        super(UpdateChannelTestCase, self).setUp()
        self.check = Check(user=self.alice)
        self.check.save()

        self.channel = Channel(user=self.alice, kind="email")
        self.channel.email = "*****@*****.**"
        self.channel.save()
Esempio n. 15
0
class RemoveChannelTestCase(TestCase):

    def setUp(self):
        super(RemoveChannelTestCase, self).setUp()
        self.alice = User(username="******", email="*****@*****.**")
        self.alice.set_password("password")
        self.alice.save()

        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        url = "/integrations/%s/remove/" % self.channel.code

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        self.assertRedirects(r, "/integrations/")

        assert Channel.objects.count() == 0

    def test_it_handles_bad_uuid(self):
        url = "/integrations/not-uuid/remove/"

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        assert r.status_code == 400

    def test_it_checks_owner(self):
        url = "/integrations/%s/remove/" % self.channel.code

        mallory = User(username="******", email="*****@*****.**")
        mallory.set_password("password")
        mallory.save()

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        assert r.status_code == 403

    def test_it_handles_missing_uuid(self):
        # Valid UUID but there is no channel for it:
        url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/"

        self.client.login(username="******", password="******")
        r = self.client.post(url)
        assert r.status_code == 404
Esempio n. 16
0
    def setUp(self):
        self.alice = User(username="******")
        self.alice.set_password("password")
        self.alice.save()

        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()
Esempio n. 17
0
    def setUp(self):
        super(ChannelChecksTestCase, self).setUp()
        self.alice = User(username="******", email="*****@*****.**")
        self.alice.set_password("password")
        self.alice.save()

        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()
Esempio n. 18
0
    def test_it_checks_check_user(self):
        mallory = User(username="******", email="*****@*****.**")
        mallory.set_password("password")
        mallory.save()

        mc = Channel(user=mallory, kind="email")
        mc.email = "*****@*****.**"
        mc.save()

        payload = {
            "channel": mc.code,
            "check-%s" % self.check.code: True
        }
        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)

        # mc belongs to mallorym but self.check does not--
        assert r.status_code == 403
Esempio n. 19
0
    def _setup_data(self, kind, value, status="down", email_verified=True):
        self.check = Check()
        self.check.status = status
        self.check.save()

        self.channel = Channel(user=self.alice)
        self.channel.kind = kind
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)
Esempio n. 20
0
def add_telegram(request):
    chat_id, chat_type, chat_name = None, None, None
    qs = request.META["QUERY_STRING"]
    if qs:
        chat_id, chat_type, chat_name = signing.loads(qs, max_age=600)

    if request.method == "POST":
        channel = Channel(user=request.team.user, kind="telegram")
        channel.value = json.dumps({
            "id": chat_id,
            "type": chat_type,
            "name": chat_name
        })
        channel.save()

        channel.assign_all_checks()
        messages.success(request, "The Telegram integration has been added!")
        return redirect("hc-channels")

    ctx = {
        "chat_id": chat_id,
        "chat_type": chat_type,
        "chat_name": chat_name,
        "bot_name": settings.TELEGRAM_BOT_NAME
    }

    return render(request, "integrations/add_telegram.html", ctx)
Esempio n. 21
0
def add_zendesk(request):
    if settings.ZENDESK_CLIENT_ID is None:
        raise Http404("zendesk integration is not available")

    if request.method == "POST":
        domain = request.POST.get("subdomain")
        request.session["subdomain"] = domain
        redirect_uri = settings.SITE_ROOT + reverse("hc-add-zendesk")
        auth_url = "https://%s.zendesk.com/oauth/authorizations/new?" % domain
        auth_url += urlencode({
            "client_id": settings.ZENDESK_CLIENT_ID,
            "redirect_uri": redirect_uri,
            "response_type": "code",
            "scope": "requests:read requests:write",
            "state": _prepare_state(request, "zendesk")
        })

        return redirect(auth_url)

    if "code" in request.GET:
        code = _get_validated_code(request, "zendesk")
        if code is None:
            return HttpResponseBadRequest()

        domain = request.session.pop("subdomain")
        url = "https://%s.zendesk.com/oauth/tokens" % domain

        redirect_uri = settings.SITE_ROOT + reverse("hc-add-zendesk")
        result = requests.post(url, {
            "client_id": settings.ZENDESK_CLIENT_ID,
            "client_secret": settings.ZENDESK_CLIENT_SECRET,
            "code": code,
            "grant_type": "authorization_code",
            "redirect_uri": redirect_uri,
            "scope": "read"
        })

        doc = result.json()
        if "access_token" in doc:
            doc["subdomain"] = domain

            channel = Channel(kind="zendesk")
            channel.user = request.team.user
            channel.value = json.dumps(doc)
            channel.save()
            channel.assign_all_checks()
            messages.success(request,
                             "The Zendesk integration has been added!")
        else:
            messages.warning(request, "Something went wrong")

        return redirect("hc-channels")

    ctx = {"page": "channels"}
    return render(request, "integrations/add_zendesk.html", ctx)
class UnsubscribeEmailTestCase(BaseTestCase):

    def setUp(self):
        super(UnsubscribeEmailTestCase, self).setUp()
        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        token = self.channel.make_token()
        url = "/integrations/%s/unsub/%s/" % (self.channel.code, token)

        r = self.client.get(url)
        self.assertContains(r, "has been unsubscribed", status_code=200)

        q = Channel.objects.filter(code=self.channel.code)
        self.assertEqual(q.count(), 0)

    def test_it_checks_token(self):
        url = "/integrations/%s/unsub/faketoken/" % self.channel.code

        r = self.client.get(url)
        self.assertContains(r, "link you just used is incorrect",
                            status_code=200)

    def test_it_checks_channel_kind(self):
        self.channel.kind = "webhook"
        self.channel.save()

        token = self.channel.make_token()
        url = "/integrations/%s/unsub/%s/" % (self.channel.code, token)

        r = self.client.get(url)
        self.assertEqual(r.status_code, 400)
Esempio n. 23
0
def add_slack(request):
    if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated:
        return redirect("hc-login")

    if request.method == "POST":
        form = AddUrlForm(request.POST)
        if form.is_valid():
            channel = Channel(user=request.team.user, kind="slack")
            channel.value = form.cleaned_data["value"]
            channel.save()

            channel.assign_all_checks()
            return redirect("hc-channels")
    else:
        form = AddUrlForm()

    ctx = {
        "page": "channels",
        "form": form,
        "slack_client_id": settings.SLACK_CLIENT_ID
    }

    if settings.SLACK_CLIENT_ID:
        ctx["state"] = _prepare_state(request, "slack")

    return render(request, "integrations/add_slack.html", ctx)
class ChannelChecksTestCase(BaseTestCase):

    def setUp(self):
        super(ChannelChecksTestCase, self).setUp()
        self.channel = Channel(user=self.alice, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        url = "/integrations/%s/checks/" % self.channel.code

        self.client.login(username="******", password="******")
        r = self.client.get(url)
        self.assertContains(r, "Assign Checks to Channel", status_code=200)

    def test_team_access_works(self):
        url = "/integrations/%s/checks/" % self.channel.code

        # Logging in as bob, not alice. Bob has team access so this
        # should work.
        self.client.login(username="******", password="******")
        r = self.client.get(url)
        self.assertContains(r, "Assign Checks to Channel", status_code=200)

    def test_it_checks_owner(self):
        # channel does not belong to mallory so this should come back
        # with 403 Forbidden:
        url = "/integrations/%s/checks/" % self.channel.code
        self.client.login(username="******", password="******")
        r = self.client.get(url)
        assert r.status_code == 403

    def test_missing_channel(self):
        # Valid UUID but there is no channel for it:
        url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/checks/"

        self.client.login(username="******", password="******")
        r = self.client.get(url)
        assert r.status_code == 404
Esempio n. 25
0
def add_pushover(request):
    if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
        raise Http404("pushover integration is not available")

    if request.method == "POST":
        # Initiate the subscription
        nonce = get_random_string()
        request.session["po_nonce"] = nonce

        failure_url = settings.SITE_ROOT + reverse("hc-channels")
        success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
            "nonce": nonce,
            "prio": request.POST.get("po_priority", "0"),
        })
        subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
            "success": success_url,
            "failure": failure_url,
        })

        return redirect(subscription_url)

    # Handle successful subscriptions
    if "pushover_user_key" in request.GET:
        if "nonce" not in request.GET or "prio" not in request.GET:
            return HttpResponseBadRequest()

        # Validate nonce
        if request.GET["nonce"] != request.session.get("po_nonce"):
            return HttpResponseForbidden()

        # Validate priority
        if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
            return HttpResponseBadRequest()

        # All looks well--
        del request.session["po_nonce"]

        if request.GET.get("pushover_unsubscribed") == "1":
            # Unsubscription: delete all Pushover channels for this user
            Channel.objects.filter(user=request.user, kind="po").delete()
            return redirect("hc-channels")
        else:
            # Subscription
            user_key = request.GET["pushover_user_key"]
            priority = int(request.GET["prio"])

            channel = Channel(user=request.team.user, kind="po")
            channel.value = "%s|%d" % (user_key, priority)
            channel.save()
            channel.assign_all_checks()
            return redirect("hc-channels")

    # Show Integration Settings form
    ctx = {
        "page": "channels",
        "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
        "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
    }
    return render(request, "integrations/add_pushover.html", ctx)
Esempio n. 26
0
    def _setup_data(self, channel_kind, channel_value, email_verified=True):
        self.alice = User(username="******")
        self.alice.save()

        self.check = Check()
        self.check.status = "down"
        self.check.save()

        self.channel = Channel(user=self.alice)
        self.channel.kind = channel_kind
        self.channel.value = channel_value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)
Esempio n. 27
0
def add_email(request):
    if request.method == "POST":
        form = AddEmailForm(request.POST)
        if form.is_valid():
            channel = Channel(user=request.team.user, kind="email")
            channel.value = form.cleaned_data["value"]
            channel.save()

            channel.assign_all_checks()
            channel.send_verify_link()
            return redirect("hc-channels")
    else:
        form = AddEmailForm()

    ctx = {"page": "channels", "form": form}
    return render(request, "integrations/add_email.html", ctx)
Esempio n. 28
0
def _make_user(email):
    username = str(uuid.uuid4())[:30]
    user = User(username=username, email=email)
    user.save()

    channel = Channel()
    channel.user = user
    channel.kind = "email"
    channel.value = email
    channel.email_verified = True
    channel.save()

    return user
Esempio n. 29
0
def _make_user(email):
    username = str(uuid.uuid4())[:30]
    user = User(username=username, email=email)
    user.set_unusable_password()
    user.save()

    # Ensure a profile gets created
    Profile.objects.for_user(user)

    channel = Channel()
    channel.user = user
    channel.kind = "email"
    channel.value = email
    channel.email_verified = True
    channel.save()

    return user
Esempio n. 30
0
def _make_user(email):
    username = str(uuid.uuid4())[:30]
    user = User(username=username, email=email)
    user.set_unusable_password()
    user.save()

    profile = Profile(user=user)
    profile.save()

    channel = Channel()
    channel.user = user
    channel.kind = "email"
    channel.value = email
    channel.email_verified = True
    channel.save()

    return user
Esempio n. 31
0
class NotifyTestCase(TestCase):

    def _setup_data(self, channel_kind, channel_value, email_verified=True):
        self.alice = User(username="******")
        self.alice.save()

        self.check = Check()
        self.check.status = "down"
        self.check.save()

        self.channel = Channel(user=self.alice)
        self.channel.kind = channel_kind
        self.channel.value = channel_value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.models.requests.get")
    def test_webhook(self, mock_get):
        self._setup_data("webhook", "http://example")

        self.channel.notify(self.check)
        mock_get.assert_called_with(u"http://example", timeout=5)

    @patch("hc.api.models.requests.get", side_effect=ReadTimeout)
    def test_webhooks_handle_timeouts(self, mock_get):
        self._setup_data("webhook", "http://example")
        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

    def test_email(self):
        self._setup_data("email", "*****@*****.**")
        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

    def test_it_skips_unverified_email(self):
        self._setup_data("email", "*****@*****.**", email_verified=False)
        self.channel.notify(self.check)

        assert Notification.objects.count() == 0
        self.assertEqual(len(mail.outbox), 0)

    @patch("hc.api.models.requests.post")
    def test_pd(self, mock_post):
        self._setup_data("pd", "123")
        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        assert "trigger" in kwargs["data"]
Esempio n. 32
0
class NotifySlackTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.name = "Foobar"
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "slack"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_slack(self, mock_post):
        self._setup_data("123")
        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["json"]
        attachment = payload["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "an hour ago")

        # The payload should not contain check's code
        serialized = json.dumps(payload)
        self.assertNotIn(str(self.check.code), serialized)

    @patch("hc.api.transports.requests.request")
    def test_slack_with_complex_value(self, mock_post):
        v = json.dumps({"incoming_webhook": {"url": "123"}})
        self._setup_data(v)
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        self.assertEqual(args[1], "123")

    @patch("hc.api.transports.requests.request")
    def test_slack_handles_500(self, mock_post):
        self._setup_data("123")
        mock_post.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_slack_handles_timeout(self, mock_post):
        self._setup_data("123")

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request")
    def test_slack_with_tabs_in_schedule(self, mock_post):
        self._setup_data("123")
        self.check.kind = "cron"
        self.check.schedule = "*\t* * * *"
        self.check.save()
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)
        self.assertTrue(mock_post.called)

    @override_settings(SLACK_ENABLED=False)
    def test_it_requires_slack_enabled(self):
        self._setup_data("123")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Slack notifications are not enabled.")
Esempio n. 33
0
class NotifyTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "zulip"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_zulip(self, mock_post):
        definition = {
            "bot_email": "*****@*****.**",
            "api_key": "fake-key",
            "mtype": "stream",
            "to": "general",
        }
        self._setup_data(json.dumps(definition))
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args

        method, url = args
        self.assertEqual(url, "https://example.org/api/v1/messages")

        payload = kwargs["data"]
        self.assertIn("DOWN", payload["topic"])

    @patch("hc.api.transports.requests.request")
    def test_zulip_returns_error(self, mock_post):
        definition = {
            "bot_email": "*****@*****.**",
            "api_key": "fake-key",
            "mtype": "stream",
            "to": "general",
        }
        self._setup_data(json.dumps(definition))
        mock_post.return_value.status_code = 403
        mock_post.return_value.json.return_value = {"msg": "Nice try"}

        self.channel.notify(self.check)

        n = Notification.objects.first()
        self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')

    @patch("hc.api.transports.requests.request")
    def test_zulip_uses_site_parameter(self, mock_post):
        definition = {
            "bot_email": "*****@*****.**",
            "site": "https://custom.example.org",
            "api_key": "fake-key",
            "mtype": "stream",
            "to": "general",
        }
        self._setup_data(json.dumps(definition))
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args

        method, url = args
        self.assertEqual(url, "https://custom.example.org/api/v1/messages")

        payload = kwargs["data"]
        self.assertIn("DOWN", payload["topic"])

    @override_settings(ZULIP_ENABLED=False)
    def test_it_requires_zulip_enabled(self):
        definition = {
            "bot_email": "*****@*****.**",
            "api_key": "fake-key",
            "mtype": "stream",
            "to": "general",
        }
        self._setup_data(json.dumps(definition))

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Zulip notifications are not enabled.")
Esempio n. 34
0
def add_pushover(request):
    if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
        raise Http404("pushover integration is not available")

    if request.method == "POST":
        # Initiate the subscription
        nonce = get_random_string()
        request.session["po_nonce"] = nonce

        failure_url = settings.SITE_ROOT + reverse("hc-channels")
        success_url = settings.SITE_ROOT + reverse(
            "hc-add-pushover") + "?" + urlencode(
                {
                    "nonce": nonce,
                    "prio": request.POST.get("po_priority", "0"),
                })
        subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode(
            {
                "success": success_url,
                "failure": failure_url,
            })

        return redirect(subscription_url)

    # Handle successful subscriptions
    if "pushover_user_key" in request.GET:
        if "nonce" not in request.GET or "prio" not in request.GET:
            return HttpResponseBadRequest()

        # Validate nonce
        if request.GET["nonce"] != request.session.get("po_nonce"):
            return HttpResponseForbidden()

        # Validate priority
        if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
            return HttpResponseBadRequest()

        # All looks well--
        del request.session["po_nonce"]

        if request.GET.get("pushover_unsubscribed") == "1":
            # Unsubscription: delete all Pushover channels for this user
            Channel.objects.filter(user=request.user, kind="po").delete()
            return redirect("hc-channels")
        else:
            # Subscription
            user_key = request.GET["pushover_user_key"]
            priority = int(request.GET["prio"])

            channel = Channel(user=request.team.user, kind="po")
            channel.value = "%s|%d" % (user_key, priority)
            channel.save()
            channel.assign_all_checks()
            return redirect("hc-channels")

    # Show Integration Settings form
    ctx = {
        "page": "channels",
        "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
        "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
    }
    return render(request, "integrations/add_pushover.html", ctx)
Esempio n. 35
0
 def test_it_handles_legacy_opsgenie_value(self):
     c = Channel(kind="opsgenie", value="foo123")
     self.assertEqual(c.opsgenie_key, "foo123")
     self.assertEqual(c.opsgenie_region, "us")
Esempio n. 36
0
class EditWebhookTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        definition = {
            "method_down": "GET",
            "url_down": "http://example.org/down",
            "body_down": "$NAME is down",
            "headers_down": {
                "User-Agent": "My-Custom-UA"
            },
            "method_up": "GET",
            "url_up": "http://example.org/up",
            "body_up": "$NAME is up",
            "headers_up": {},
        }

        self.channel = Channel(project=self.project, kind="webhook")
        self.channel.name = "Call example.org"
        self.channel.value = json.dumps(definition)
        self.channel.save()

        self.url = "/integrations/%s/edit_webhook/" % self.channel.code

    def test_it_shows_form(self):
        self.client.login(username="******", password="******")
        r = self.client.get(self.url)
        self.assertContains(r, "Webhook Settings")

        self.assertContains(r, "Call example.org")

        # down
        self.assertContains(r, "http://example.org/down")
        self.assertContains(r, "My-Custom-UA")
        self.assertContains(r, "$NAME is down")

        # up
        self.assertContains(r, "http://example.org/up")
        self.assertContains(r, "$NAME is up")

    def test_it_saves_form_and_redirects(self):
        form = {
            "name": "Call foo.com / bar.com",
            "method_down": "POST",
            "url_down": "http://foo.com",
            "headers_down": "X-Foo: 1\nX-Bar: 2",
            "body_down": "going down",
            "method_up": "POST",
            "url_up": "https://bar.com",
            "headers_up": "Content-Type: text/plain",
            "body_up": "going up",
        }

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, form)
        self.assertRedirects(r, self.channels_url)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.name, "Call foo.com / bar.com")

        down_spec = self.channel.down_webhook_spec
        self.assertEqual(down_spec["method"], "POST")
        self.assertEqual(down_spec["url"], "http://foo.com")
        self.assertEqual(down_spec["body"], "going down")
        self.assertEqual(down_spec["headers"], {"X-Foo": "1", "X-Bar": "2"})

        up_spec = self.channel.up_webhook_spec
        self.assertEqual(up_spec["method"], "POST")
        self.assertEqual(up_spec["url"], "https://bar.com")
        self.assertEqual(up_spec["body"], "going up")
        self.assertEqual(up_spec["headers"], {"Content-Type": "text/plain"})

    def test_it_requires_kind_webhook(self):
        self.channel.kind = "email"
        self.channel.value = "*****@*****.**"
        self.channel.save()

        self.client.login(username="******", password="******")
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 400)

    def test_it_requires_rw_access(self):
        self.bobs_membership.rw = False
        self.bobs_membership.save()

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, {})
        self.assertEqual(r.status_code, 403)
 def setUp(self):
     super(VerifyEmailTestCase, self).setUp()
     self.channel = Channel(user=self.alice, kind="email")
     self.channel.value = "*****@*****.**"
     self.channel.save()
Esempio n. 38
0
class EditEmailTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        self.check = Check.objects.create(project=self.project)

        self.channel = Channel(project=self.project, kind="email")
        self.channel.value = json.dumps({
            "value": "*****@*****.**",
            "up": True,
            "down": True
        })
        self.channel.email_verified = True
        self.channel.save()

        self.url = f"/integrations/{self.channel.code}/edit/"

    def test_it_shows_form(self):
        self.client.login(username="******", password="******")
        r = self.client.get(self.url)
        self.assertContains(
            r, "Get an email message when check goes up or down.")
        self.assertContains(r, "*****@*****.**")
        self.assertContains(r, "Email Settings")

    def test_it_saves_changes(self):
        form = {"value": "*****@*****.**", "down": "true", "up": "false"}

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, form)
        self.assertRedirects(r, self.channels_url)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.email_value, "*****@*****.**")
        self.assertTrue(self.channel.email_notify_down)
        self.assertFalse(self.channel.email_notify_up)

        # It should send a verification link
        email = mail.outbox[0]
        self.assertTrue(email.subject.startswith("Verify email address on"))
        self.assertEqual(email.to[0], "*****@*****.**")

        # Make sure it does not call assign_all_checks
        self.assertFalse(self.channel.checks.exists())

    def test_it_skips_verification_if_email_unchanged(self):
        form = {"value": "*****@*****.**", "down": "false", "up": "true"}

        self.client.login(username="******", password="******")
        self.client.post(self.url, form)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.email_value, "*****@*****.**")
        self.assertFalse(self.channel.email_notify_down)
        self.assertTrue(self.channel.email_notify_up)
        self.assertTrue(self.channel.email_verified)

        # The email address did not change, so we should skip verification
        self.assertEqual(len(mail.outbox), 0)

    def test_team_access_works(self):
        form = {"value": "*****@*****.**", "down": "true", "up": "true"}

        self.client.login(username="******", password="******")
        self.client.post(self.url, form)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.email_value, "*****@*****.**")

    @override_settings(EMAIL_USE_VERIFICATION=False)
    def test_it_hides_confirmation_needed_notice(self):
        self.client.login(username="******", password="******")
        r = self.client.get(self.url)
        self.assertNotContains(r, "Requires confirmation")

    @override_settings(EMAIL_USE_VERIFICATION=False)
    def test_it_auto_verifies_email(self):
        form = {"value": "*****@*****.**", "down": "true", "up": "true"}

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, form)
        self.assertRedirects(r, self.channels_url)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.email_value, "*****@*****.**")

        # Email should *not* have been sent
        self.assertEqual(len(mail.outbox), 0)

    def test_it_auto_verifies_own_email(self):
        form = {"value": "*****@*****.**", "down": "true", "up": "true"}

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, form)
        self.assertRedirects(r, self.channels_url)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.email_value, "*****@*****.**")

        # Email should *not* have been sent
        self.assertEqual(len(mail.outbox), 0)

    def test_it_resets_disabled_flag(self):
        self.channel.disabled = True
        self.channel.save()

        form = {
            "value": "*****@*****.**",
            "down": "true",
            "up": "true"
        }
        self.client.login(username="******", password="******")
        r = self.client.post(self.url, form)
        self.assertRedirects(r, self.channels_url)

        self.channel.refresh_from_db()
        self.assertFalse(self.channel.disabled)
        self.assertFalse(self.channel.email_verified)

        # It should send a verification link
        email = mail.outbox[0]
        self.assertTrue(email.subject.startswith("Verify email address on"))

    def test_it_requires_rw_access(self):
        self.bobs_membership.role = "r"
        self.bobs_membership.save()

        self.client.login(username="******", password="******")
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 403)
Esempio n. 39
0
class NotifyTestCase(BaseTestCase):
    def _setup_data(self, kind, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = kind
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_webhook(self, mock_get):
        self._setup_data("webhook", "http://example")
        mock_get.return_value.status_code = 200

        self.channel.notify(self.check)
        mock_get.assert_called_with(
            "get",
            "http://example",
            headers={"User-Agent": "healthchecks.io"},
            timeout=5,
        )

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_webhooks_handle_timeouts(self, mock_get):
        self._setup_data("webhook", "http://example")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
    def test_webhooks_handle_connection_errors(self, mock_get):
        self._setup_data("webhook", "http://example")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection failed")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_ignore_up_events(self, mock_get):
        self._setup_data("webhook", "http://example", status="up")
        self.channel.notify(self.check)

        self.assertFalse(mock_get.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_500(self, mock_get):
        self._setup_data("webhook", "http://example")
        mock_get.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables(self, mock_get):
        template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
        self._setup_data("webhook", template)
        self.check.name = "Hello World"
        self.check.tags = "foo bar"
        self.check.save()

        self.channel.notify(self.check)

        url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code

        args, kwargs = mock_get.call_args
        self.assertEqual(args[0], "get")
        self.assertEqual(args[1], url)
        self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
        self.assertEqual(kwargs["timeout"], 5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_variable_variables(self, mock_get):
        self._setup_data("webhook", "http://host/$$NAMETAG1")
        self.check.tags = "foo bar"
        self.check.save()

        self.channel.notify(self.check)

        # $$NAMETAG1 should *not* get transformed to "foo"
        args, kwargs = mock_get.call_args
        self.assertEqual(args[1], "http://host/$TAG1")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_post(self, mock_request):
        template = "http://example.com\n\nThe Time Is $NOW"
        self._setup_data("webhook", template)
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args
        self.assertEqual(args[0], "post")
        self.assertEqual(args[1], "http://example.com")

        # spaces should not have been urlencoded:
        payload = kwargs["data"].decode()
        self.assertTrue(payload.startswith("The Time Is 2"))

    @patch("hc.api.transports.requests.request")
    def test_webhooks_dollarsign_escaping(self, mock_get):
        # If name or tag contains what looks like a variable reference,
        # that should be left alone:

        template = "http://host/$NAME"
        self._setup_data("webhook", template)
        self.check.name = "$TAG1"
        self.check.tags = "foo"
        self.check.save()

        self.channel.notify(self.check)

        url = "http://host/%24TAG1"
        mock_get.assert_called_with(
            "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5
        )

    @patch("hc.api.transports.requests.request")
    def test_webhook_fires_on_up_event(self, mock_get):
        self._setup_data("webhook", "http://foo\nhttp://bar", status="up")

        self.channel.notify(self.check)

        mock_get.assert_called_with(
            "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_unicode_post_body(self, mock_request):
        template = "http://example.com\n\n(╯°□°)╯︵ ┻━┻"
        self._setup_data("webhook", template)
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args

        # unicode should be encoded into utf-8
        self.assertIsInstance(kwargs["data"], bytes)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_json_value(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {},
        }
        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=5
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_json_up_event(self, mock_request):
        definition = {
            "method_up": "GET",
            "url_up": "http://bar",
            "body_up": "",
            "headers_up": {},
        }

        self._setup_data("webhook", json.dumps(definition), status="up")
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io"}
        mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_post_headers(self, mock_request):
        definition = {
            "method_down": "POST",
            "url_down": "http://foo.com",
            "body_down": "data",
            "headers_down": {"Content-Type": "application/json"},
        }

        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
        mock_request.assert_called_with(
            "post", "http://foo.com", data=b"data", headers=headers, timeout=5
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_get_headers(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {"Content-Type": "application/json"},
        }

        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=5
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_allow_user_agent_override(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {"User-Agent": "My-Agent"},
        }

        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "My-Agent"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=5
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables_in_headers(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {"X-Message": "$NAME is DOWN"},
        }

        self._setup_data("webhook", json.dumps(definition))
        self.check.name = "Foo"
        self.check.save()

        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=5
        )

    def test_email(self):
        self._setup_data("email", "*****@*****.**")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertTrue("X-Bounce-Url" in email.extra_headers)
        self.assertTrue("List-Unsubscribe" in email.extra_headers)
        self.assertTrue("List-Unsubscribe-Post" in email.extra_headers)

    def test_email_transport_handles_json_value(self):
        payload = {"value": "*****@*****.**", "up": True, "down": True}
        self._setup_data("email", json.dumps(payload))
        self.channel.notify(self.check)

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")

    def test_it_skips_unverified_email(self):
        self._setup_data("email", "*****@*****.**", email_verified=False)
        self.channel.notify(self.check)

        # If an email is not verified, it should be skipped over
        # without logging a notification:
        self.assertEqual(Notification.objects.count(), 0)
        self.assertEqual(len(mail.outbox), 0)

    def test_email_checks_up_down_flags(self):
        payload = {"value": "*****@*****.**", "up": True, "down": False}
        self._setup_data("email", json.dumps(payload))
        self.channel.notify(self.check)

        # This channel should not notify on "down" events:
        self.assertEqual(Notification.objects.count(), 0)
        self.assertEqual(len(mail.outbox), 0)

    @patch("hc.api.transports.requests.request")
    def test_pd(self, mock_post):
        self._setup_data("pd", "123")
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")
        self.assertEqual(payload["service_key"], "123")

    @patch("hc.api.transports.requests.request")
    def test_pd_complex(self, mock_post):
        self._setup_data("pd", json.dumps({"service_key": "456"}))
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")
        self.assertEqual(payload["service_key"], "456")

    @patch("hc.api.transports.requests.request")
    def test_pagertree(self, mock_post):
        self._setup_data("pagertree", "123")
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")

    @patch("hc.api.transports.requests.request")
    def test_pagerteam(self, mock_post):
        self._setup_data("pagerteam", "123")
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")

    @patch("hc.api.transports.requests.request")
    def test_slack(self, mock_post):
        self._setup_data("slack", "123")
        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["json"]
        attachment = payload["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "an hour ago")

    @patch("hc.api.transports.requests.request")
    def test_slack_with_complex_value(self, mock_post):
        v = json.dumps({"incoming_webhook": {"url": "123"}})
        self._setup_data("slack", v)
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        self.assertEqual(args[1], "123")

    @patch("hc.api.transports.requests.request")
    def test_slack_handles_500(self, mock_post):
        self._setup_data("slack", "123")
        mock_post.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_slack_handles_timeout(self, mock_post):
        self._setup_data("slack", "123")

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request")
    def test_slack_with_tabs_in_schedule(self, mock_post):
        self._setup_data("slack", "123")
        self.check.kind = "cron"
        self.check.schedule = "*\t* * * *"
        self.check.save()
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)
        self.assertTrue(mock_post.called)

    @patch("hc.api.transports.requests.request")
    def test_hipchat(self, mock_post):
        self._setup_data("hipchat", "123")

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_with_legacy_value(self, mock_post):
        self._setup_data("opsgenie", "123")
        mock_post.return_value.status_code = 202

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        self.assertEqual(mock_post.call_count, 1)
        args, kwargs = mock_post.call_args
        self.assertIn("api.opsgenie.com", args[1])
        payload = kwargs["json"]
        self.assertIn("DOWN", payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_up(self, mock_post):
        self._setup_data("opsgenie", "123", status="up")
        mock_post.return_value.status_code = 202

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        self.assertEqual(mock_post.call_count, 1)
        args, kwargs = mock_post.call_args
        method, url = args
        self.assertTrue(str(self.check.code) in url)

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_with_json_value(self, mock_post):
        self._setup_data("opsgenie", json.dumps({"key": "456", "region": "eu"}))
        mock_post.return_value.status_code = 202

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        self.assertEqual(mock_post.call_count, 1)
        args, kwargs = mock_post.call_args
        self.assertIn("api.eu.opsgenie.com", args[1])

    @patch("hc.api.transports.requests.request")
    def test_pushover(self, mock_post):
        self._setup_data("po", "123|0")
        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.assertIn("DOWN", payload["title"])

    @patch("hc.api.transports.requests.request")
    def test_pushover_up_priority(self, mock_post):
        self._setup_data("po", "123|0|2", status="up")
        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.assertIn("UP", payload["title"])
        self.assertEqual(payload["priority"], 2)
        self.assertIn("retry", payload)
        self.assertIn("expire", payload)

    @patch("hc.api.transports.requests.request")
    def test_victorops(self, mock_post):
        self._setup_data("victorops", "123")
        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["json"]
        self.assertEqual(payload["message_type"], "CRITICAL")

    @patch("hc.api.transports.requests.request")
    def test_discord(self, mock_post):
        v = json.dumps({"webhook": {"url": "123"}})
        self._setup_data("discord", v)
        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["json"]
        attachment = payload["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "an hour ago")

    @patch("hc.api.transports.requests.request")
    def test_pushbullet(self, mock_post):
        self._setup_data("pushbullet", "fake-token")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        _, kwargs = mock_post.call_args
        self.assertEqual(kwargs["json"]["type"], "note")
        self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")

    @patch("hc.api.transports.requests.request")
    def test_telegram(self, mock_post):
        v = json.dumps({"id": 123})
        self._setup_data("telegram", v)
        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["json"]
        self.assertEqual(payload["chat_id"], 123)
        self.assertTrue("The check" in payload["text"])

    @patch("hc.api.transports.requests.request")
    def test_sms(self, mock_post):
        self._setup_data("sms", "+1234567890")
        self.check.last_ping = now() - td(hours=2)

        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertEqual(payload["To"], "+1234567890")
        self.assertFalse("\xa0" in payload["Body"])

        # sent SMS counter should go up
        self.profile.refresh_from_db()
        self.assertEqual(self.profile.sms_sent, 1)

    @patch("hc.api.transports.requests.request")
    def test_sms_handles_json_value(self, mock_post):
        value = {"label": "foo", "value": "+1234567890"}
        self._setup_data("sms", 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")
    def test_sms_limit(self, mock_post):
        # At limit already:
        self.profile.last_sms_date = now()
        self.profile.sms_sent = 50
        self.profile.save()

        self._setup_data("sms", "+1234567890")

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)

        n = Notification.objects.get()
        self.assertTrue("Monthly SMS limit exceeded" in n.error)

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertEqual(email.subject, "Monthly SMS Limit Reached")

    @patch("hc.api.transports.requests.request")
    def test_sms_limit_reset(self, mock_post):
        # At limit, but also into a new month
        self.profile.sms_sent = 50
        self.profile.last_sms_date = now() - td(days=100)
        self.profile.save()

        self._setup_data("sms", "+1234567890")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertTrue(mock_post.called)

    @patch("hc.api.transports.requests.request")
    def test_whatsapp(self, mock_post):
        definition = {"value": "+1234567890", "up": True, "down": True}

        self._setup_data("whatsapp", json.dumps(definition))
        self.check.last_ping = now() - td(hours=2)

        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertEqual(payload["To"], "whatsapp:+1234567890")

        # sent SMS counter should go up
        self.profile.refresh_from_db()
        self.assertEqual(self.profile.sms_sent, 1)

    @patch("hc.api.transports.requests.request")
    def test_whatsapp_obeys_up_down_flags(self, mock_post):
        definition = {"value": "+1234567890", "up": True, "down": False}

        self._setup_data("whatsapp", json.dumps(definition))
        self.check.last_ping = now() - td(hours=2)

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 0)

        self.assertFalse(mock_post.called)

    @patch("hc.api.transports.requests.request")
    def test_whatsapp_limit(self, mock_post):
        # At limit already:
        self.profile.last_sms_date = now()
        self.profile.sms_sent = 50
        self.profile.save()

        definition = {"value": "+1234567890", "up": True, "down": True}
        self._setup_data("whatsapp", json.dumps(definition))

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)

        n = Notification.objects.get()
        self.assertTrue("Monthly message limit exceeded" in n.error)

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertEqual(email.subject, "Monthly WhatsApp Limit Reached")

    @patch("apprise.Apprise")
    @override_settings(APPRISE_ENABLED=True)
    def test_apprise_enabled(self, mock_apprise):
        self._setup_data("apprise", "123")

        mock_aobj = Mock()
        mock_aobj.add.return_value = True
        mock_aobj.notify.return_value = True
        mock_apprise.return_value = mock_aobj
        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

        self.check.status = "up"
        self.assertEqual(Notification.objects.count(), 1)

    @patch("apprise.Apprise")
    @override_settings(APPRISE_ENABLED=False)
    def test_apprise_disabled(self, mock_apprise):
        self._setup_data("apprise", "123")

        mock_aobj = Mock()
        mock_aobj.add.return_value = True
        mock_aobj.notify.return_value = True
        mock_apprise.return_value = mock_aobj
        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

    def test_not_implimented(self):
        self._setup_data("webhook", "http://example")
        self.channel.kind = "invalid"

        with self.assertRaises(NotImplementedError):
            self.channel.notify(self.check)

    @patch("hc.api.transports.requests.request")
    def test_msteams(self, mock_post):
        self._setup_data("msteams", "http://example.com/webhook")
        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["json"]
        self.assertEqual(payload["@type"], "MessageCard")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=True)
    def test_shell(self, mock_system):
        definition = {"cmd_down": "logger hello", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))
        mock_system.return_value = 0

        self.channel.notify(self.check)
        mock_system.assert_called_with("logger hello")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=True)
    def test_shell_handles_nonzero_exit_code(self, mock_system):
        definition = {"cmd_down": "logger hello", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))
        mock_system.return_value = 123

        self.channel.notify(self.check)
        n = Notification.objects.get()
        self.assertEqual(n.error, "Command returned exit code 123")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=True)
    def test_shell_supports_variables(self, mock_system):
        definition = {"cmd_down": "logger $NAME is $STATUS ($TAG1)", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))
        mock_system.return_value = 0

        self.check.name = "Database"
        self.check.tags = "foo bar"
        self.check.save()
        self.channel.notify(self.check)

        mock_system.assert_called_with("logger Database is down (foo)")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=False)
    def test_shell_disabled(self, mock_system):
        definition = {"cmd_down": "logger hello", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))

        self.channel.notify(self.check)
        self.assertFalse(mock_system.called)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Shell commands are not enabled")
Esempio n. 40
0
class NotificationStatusTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        self.check = Check(project=self.project, status="up")
        self.check.save()

        self.channel = Channel(project=self.project, kind="email")
        self.channel.value = "*****@*****.**"
        self.channel.email_verified = True
        self.channel.save()

        self.n = Notification(owner=self.check, channel=self.channel)
        self.n.save()

        self.url = "/api/v1/notifications/%s/status" % self.n.code

    def test_it_handles_twilio_failed_status(self):
        r = self.client.post(self.url, {"MessageStatus": "failed"})
        self.assertEqual(r.status_code, 200)

        self.n.refresh_from_db()
        self.assertEqual(self.n.error, "Delivery failed (status=failed).")

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error,
                         "Delivery failed (status=failed).")
        self.assertTrue(self.channel.email_verified)

    def test_it_handles_twilio_undelivered_status(self):
        r = self.client.post(self.url, {"MessageStatus": "undelivered"})
        self.assertEqual(r.status_code, 200)

        self.n.refresh_from_db()
        self.assertEqual(self.n.error, "Delivery failed (status=undelivered).")

        self.channel.refresh_from_db()
        self.assertIn("status=undelivered", self.channel.last_error)

    def test_it_handles_twilio_delivered_status(self):
        r = self.client.post(self.url, {"MessageStatus": "delivered"})
        self.assertEqual(r.status_code, 200)

        self.n.refresh_from_db()
        self.assertEqual(self.n.error, "")

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error, "")

    def test_it_checks_ttl(self):
        self.n.created = self.n.created - timedelta(minutes=61)
        self.n.save()

        r = self.client.post(self.url, {"MessageStatus": "failed"})
        self.assertEqual(r.status_code, 200)

        # The notification should not have the error field set:
        self.n.refresh_from_db()
        self.assertEqual(self.n.error, "")

    def test_it_handles_missing_notification(self):
        fake_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02"
        url = f"/api/v1/notifications/{fake_code}/status"
        r = self.client.post(url, {"MessageStatus": "failed"})
        self.assertEqual(r.status_code, 200)

    def test_it_requires_post(self):
        r = self.client.get(self.url)
        self.assertEqual(r.status_code, 405)

    def test_it_handles_error_key(self):
        r = self.client.post(self.url, {"error": "Something went wrong."})
        self.assertEqual(r.status_code, 200)

        self.n.refresh_from_db()
        self.assertEqual(self.n.error, "Something went wrong.")

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error, "Something went wrong.")
        self.assertTrue(self.channel.email_verified)

    def test_it_handles_mark_not_verified_key(self):
        payload = {"error": "Received complaint.", "mark_not_verified": "1"}

        r = self.client.post(self.url, payload)
        self.assertEqual(r.status_code, 200)

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error, "Received complaint.")
        self.assertFalse(self.channel.email_verified)

    def test_it_handles_twilio_call_status_failed(self):
        r = self.client.post(self.url, {"CallStatus": "failed"})
        self.assertEqual(r.status_code, 200)

        self.n.refresh_from_db()
        self.assertEqual(self.n.error, "Delivery failed (status=failed).")

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error,
                         "Delivery failed (status=failed).")
        self.assertTrue(self.channel.email_verified)
Esempio n. 41
0
class NotifyTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "opsgenie"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_with_legacy_value(self, mock_post):
        self._setup_data("123")
        mock_post.return_value.status_code = 202

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        self.assertEqual(mock_post.call_count, 1)
        args, kwargs = mock_post.call_args
        self.assertIn("api.opsgenie.com", args[1])
        payload = kwargs["json"]
        self.assertIn("DOWN", payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_up(self, mock_post):
        self._setup_data("123", status="up")
        mock_post.return_value.status_code = 202

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        self.assertEqual(mock_post.call_count, 1)
        args, kwargs = mock_post.call_args
        method, url = args
        self.assertTrue(str(self.check.code) in url)

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_with_json_value(self, mock_post):
        self._setup_data(json.dumps({"key": "456", "region": "eu"}))
        mock_post.return_value.status_code = 202

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        self.assertEqual(mock_post.call_count, 1)
        args, kwargs = mock_post.call_args
        self.assertIn("api.eu.opsgenie.com", args[1])

    @patch("hc.api.transports.requests.request")
    def test_opsgenie_returns_error(self, mock_post):
        self._setup_data("123")
        mock_post.return_value.status_code = 403
        mock_post.return_value.json.return_value = {"message": "Nice try"}

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(
            n.error, 'Received status code 403 with a message: "Nice try"')

    @override_settings(OPSGENIE_ENABLED=False)
    def test_it_requires_opsgenie_enabled(self):
        self._setup_data("123")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Opsgenie notifications are not enabled.")
Esempio n. 42
0
class NotifyTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        self.check = Check(project=self.project)
        self.check.name = "Daily Backup"
        self.check.desc = "Line 1\nLine2"
        self.check.tags = "foo bar"
        self.check.status = "down"
        self.check.last_ping = now() - td(minutes=61)
        self.check.n_pings = 112233
        self.check.save()

        self.ping = Ping(owner=self.check)
        self.ping.remote_addr = "1.2.3.4"
        self.ping.body = "Body Line 1\nBody Line 2"
        self.ping.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "email"
        self.channel.value = "*****@*****.**"
        self.channel.email_verified = True
        self.channel.save()
        self.channel.checks.add(self.check)

    def test_email(self):
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertTrue("X-Status-Url" in email.extra_headers)
        self.assertTrue("List-Unsubscribe" in email.extra_headers)
        self.assertTrue("List-Unsubscribe-Post" in email.extra_headers)

        html = email.alternatives[0][0]
        self.assertIn("Daily Backup", html)
        self.assertIn("Line 1<br>Line2", html)
        self.assertIn("Alices Project", html)
        self.assertIn("foo</code>", html)
        self.assertIn("bar</code>", html)
        self.assertIn("1 day", html)
        self.assertIn("from 1.2.3.4", html)
        self.assertIn("112233", html)
        self.assertIn("Body Line 1<br>Body Line 2", html)

    def test_it_shows_cron_schedule(self):
        self.check.kind = "cron"
        self.check.schedule = "0 18-23,0-8 * * *"
        self.check.save()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        html = email.alternatives[0][0]

        self.assertIn("<code>0 18-23,0-8 * * *</code>", html)

    def test_it_truncates_long_body(self):
        self.ping.body = "X" * 10000 + ", and the rest gets cut off"
        self.ping.save()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        html = email.alternatives[0][0]

        self.assertIn("[truncated]", html)
        self.assertNotIn("the rest gets cut off", html)

    def test_it_handles_missing_ping_object(self):
        self.ping.delete()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        html = email.alternatives[0][0]

        self.assertIn("Daily Backup", html)

    def test_it_handles_missing_profile(self):
        self.channel.value = "*****@*****.**"
        self.channel.save()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")

        html = email.alternatives[0][0]
        self.assertIn("Daily Backup", html)
        self.assertNotIn("Projects Overview", html)

    def test_email_transport_handles_json_value(self):
        payload = {"value": "*****@*****.**", "up": True, "down": True}
        self.channel.value = json.dumps(payload)
        self.channel.save()

        self.channel.notify(self.check)

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")

    def test_it_reports_unverified_email(self):
        self.channel.email_verified = False
        self.channel.save()

        self.channel.notify(self.check)

        # If an email is not verified, it should say so in the notification:
        n = Notification.objects.get()
        self.assertEqual(n.error, "Email not verified")

    def test_email_checks_up_down_flags(self):
        payload = {"value": "*****@*****.**", "up": True, "down": False}
        self.channel.value = json.dumps(payload)
        self.channel.save()

        self.channel.notify(self.check)

        # This channel should not notify on "down" events:
        self.assertEqual(Notification.objects.count(), 0)
        self.assertEqual(len(mail.outbox), 0)

    def test_email_handles_amperstand(self):
        self.check.name = "Foo & Bar"
        self.check.save()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        self.assertEqual(email.subject, "DOWN | Foo & Bar")
Esempio n. 43
0
class NotifyTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "msteams"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_msteams(self, mock_post):
        self._setup_data("http://example.com/webhook")
        mock_post.return_value.status_code = 200

        self.check.name = "_underscores_ & more"

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        payload = kwargs["json"]
        self.assertEqual(payload["@type"], "MessageCard")

        # summary and title should be the same, except
        # title should have any special HTML characters escaped
        self.assertEqual(payload["summary"], "“_underscores_ & more” is DOWN.")
        self.assertEqual(payload["title"],
                         "“_underscores_ &amp; more” is DOWN.")

        # The payload should not contain check's code
        serialized = json.dumps(payload)
        self.assertNotIn(str(self.check.code), serialized)

    @patch("hc.api.transports.requests.request")
    def test_msteams_escapes_html_and_markdown_in_desc(self, mock_post):
        self._setup_data("http://example.com/webhook")
        mock_post.return_value.status_code = 200

        self.check.desc = """
            TEST _underscore_ `backticks` <u>underline</u> \\backslash\\ "quoted"
        """

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        text = kwargs["json"]["sections"][0]["text"]

        self.assertIn(r"\_underscore\_", text)
        self.assertIn(r"\`backticks\`", text)
        self.assertIn("&lt;u&gt;underline&lt;/u&gt;", text)
        self.assertIn(r"\\backslash\\ ", text)
        self.assertIn("&quot;quoted&quot;", text)

    @override_settings(MSTEAMS_ENABLED=False)
    def test_it_requires_msteams_enabled(self):
        self._setup_data("http://example.com/webhook")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "MS Teams notifications are not enabled.")
class NotifySignalTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        self.check = Check(project=self.project)
        self.check.name = "Daily Backup"
        self.check.status = "down"
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        payload = {"value": "+123456789", "up": True, "down": True}
        self.channel = Channel(project=self.project)
        self.channel.kind = "signal"
        self.channel.value = json.dumps(payload)
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.socket.socket")
    def test_it_works(self, socket):
        socketobj = setup_mock(socket, {})

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        params = socketobj.req["params"]
        self.assertIn("is DOWN", params["message"])
        self.assertIn("+123456789", params["recipient"])

        # Only one check in the project, so there should be no note about
        # other checks:
        self.assertNotIn("All the other checks are up.", params["message"])

    @patch("hc.api.transports.socket.socket")
    def test_it_obeys_down_flag(self, socket):
        payload = {"value": "+123456789", "up": True, "down": False}
        self.channel.value = json.dumps(payload)
        self.channel.save()

        self.channel.notify(self.check)

        # This channel should not notify on "down" events:
        self.assertEqual(Notification.objects.count(), 0)

        self.assertFalse(socket.called)

    @patch("hc.api.transports.socket.socket")
    def test_it_requires_signal_cli_socket(self, socket):
        with override_settings(SIGNAL_CLI_SOCKET=None):
            self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Signal notifications are not enabled")

        self.assertFalse(socket.called)

    @patch("hc.api.transports.socket.socket")
    def test_it_does_not_escape_special_characters(self, socket):
        socketobj = setup_mock(socket, {})

        self.check.name = "Foo & Bar"
        self.check.save()

        self.channel.notify(self.check)

        self.assertIn("Foo & Bar", socketobj.req["params"]["message"])

    @override_settings(SECRET_KEY="test-secret")
    @patch("hc.api.transports.socket.socket")
    def test_it_obeys_rate_limit(self, socket):
        # "2862..." is sha1("+123456789test-secret")
        obj = TokenBucket(
            value="signal-2862991ccaa15c8856e7ee0abaf3448fb3c292e0")
        obj.tokens = 0
        obj.save()

        self.channel.notify(self.check)
        n = Notification.objects.get()
        self.assertEqual(n.error, "Rate limit exceeded")

        self.assertFalse(socket.called)

    @patch("hc.api.transports.socket.socket")
    def test_it_shows_all_other_checks_up_note(self, socket):
        socketobj = setup_mock(socket, {})

        other = Check(project=self.project)
        other.name = "Foobar"
        other.status = "up"
        other.last_ping = now() - td(minutes=61)
        other.save()

        self.channel.notify(self.check)

        message = socketobj.req["params"]["message"]
        self.assertIn("All the other checks are up.", message)

    @patch("hc.api.transports.socket.socket")
    def test_it_lists_other_down_checks(self, socket):
        socketobj = setup_mock(socket, {})

        other = Check(project=self.project)
        other.name = "Foobar"
        other.status = "down"
        other.last_ping = now() - td(minutes=61)
        other.save()

        self.channel.notify(self.check)

        message = socketobj.req["params"]["message"]
        self.assertIn("The following checks are also down", message)
        self.assertIn("Foobar", message)

    @patch("hc.api.transports.socket.socket")
    def test_it_does_not_show_more_than_10_other_checks(self, socket):
        socketobj = setup_mock(socket, {})

        for i in range(0, 11):
            other = Check(project=self.project)
            other.name = f"Foobar #{i}"
            other.status = "down"
            other.last_ping = now() - td(minutes=61)
            other.save()

        self.channel.notify(self.check)

        message = socketobj.req["params"]["message"]
        self.assertNotIn("Foobar", message)
        self.assertIn("11 other checks are also down.", message)

    @patch("hc.api.transports.socket.socket")
    def test_it_handles_unregistered_user(self, socket):
        setup_mock(socket, {"error": {"message": "UnregisteredUserException"}})

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Recipient not found")

    @patch("hc.api.transports.socket.socket")
    def test_it_handles_error_code(self, socket):
        setup_mock(socket, {"error": {"code": 123}})

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "signal-cli call failed (123)")

    @patch("hc.api.transports.socket.socket")
    def test_it_handles_oserror(self, socket):
        setup_mock(socket, {}, side_effect=OSError("oops"))

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "signal-cli call failed (oops)")

    @patch("hc.api.transports.socket.socket")
    def test_it_checks_jsonrpc_id(self, socket):
        socketobj = setup_mock(socket, {})
        # Add a message with an unexpected id in the outbox.
        # The socket reader should skip over it.
        socketobj.outbox += b'{"id": "surprise"}\n'

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        # outbox should be empty now
        self.assertEqual(socketobj.outbox, b"")
class NotifyPushoverTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.name = "Foo"
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "po"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_it_works(self, mock_post):
        self._setup_data("123|0")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

        args, kwargs = mock_post.call_args
        self.assertEqual(args[1], API + "/messages.json")

        payload = kwargs["data"]
        self.assertEqual(payload["title"], "Foo is DOWN")
        self.assertIn(self.check.cloaked_url(), payload["message"])
        # Only one check in the project, so there should be no note about
        # other checks:
        self.assertNotIn("All the other checks are up.", payload["message"])
        self.assertEqual(payload["tags"], self.check.unique_key)

    @patch("hc.api.transports.requests.request")
    def test_it_supports_up_priority(self, mock_post):
        self._setup_data("123|0|2", status="up")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertIn("UP", payload["title"])
        self.assertEqual(payload["priority"], 2)
        self.assertIn("retry", payload)
        self.assertIn("expire", payload)

    @override_settings(SECRET_KEY="test-secret")
    @patch("hc.api.transports.requests.request")
    def test_it_obeys_rate_limit(self, mock_post):
        self._setup_data("123|0")

        # "c0ca..." is sha1("123test-secret")
        obj = TokenBucket(value="po-c0ca2a9774952af32cabf86453f69e442c4ed0eb")
        obj.tokens = 0
        obj.save()

        self.channel.notify(self.check)
        n = Notification.objects.get()
        self.assertEqual(n.error, "Rate limit exceeded")

    @patch("hc.api.transports.requests.request")
    def test_it_cancels_emergency_notification(self, mock_post):
        self._setup_data("123|2|0", status="up")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)

        self.assertEqual(mock_post.call_count, 2)

        cancel_args, cancel_kwargs = mock_post.call_args_list[0]
        expected = "/receipts/cancel_by_tag/%s.json" % self.check.unique_key
        self.assertEqual(cancel_args[1], API + expected)

        up_args, up_kwargs = mock_post.call_args_list[1]
        payload = up_kwargs["data"]
        self.assertIn("UP", payload["title"])

    @patch("hc.api.transports.requests.request")
    def test_it_shows_all_other_checks_up_note(self, mock_post):
        self._setup_data("123|0")
        mock_post.return_value.status_code = 200

        other = Check(project=self.project)
        other.name = "Foobar"
        other.status = "up"
        other.last_ping = now() - td(minutes=61)
        other.save()

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertIn("All the other checks are up.", payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_it_lists_other_down_checks(self, mock_post):
        self._setup_data("123|0")
        mock_post.return_value.status_code = 200

        other = Check(project=self.project)
        other.name = "Foobar"
        other.status = "down"
        other.last_ping = now() - td(minutes=61)
        other.save()

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertIn("The following checks are also down", payload["message"])
        self.assertIn("Foobar", payload["message"])
        self.assertIn(other.cloaked_url(), payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_it_does_not_show_more_than_10_other_checks(self, mock_post):
        self._setup_data("123|0")
        mock_post.return_value.status_code = 200

        for i in range(0, 11):
            other = Check(project=self.project)
            other.name = f"Foobar #{i}"
            other.status = "down"
            other.last_ping = now() - td(minutes=61)
            other.save()

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertNotIn("Foobar", payload["message"])
        self.assertIn("11 other checks are also down.", payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_it_does_not_escape_title(self, mock_post):
        self._setup_data("123|0")
        self.check.name = "Foo & Bar"
        self.check.save()
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertEqual(payload["title"], "Foo & Bar is DOWN")
class UpdateChannelTestCase(BaseTestCase):

    def setUp(self):
        super(UpdateChannelTestCase, self).setUp()
        self.check = Check(user=self.alice)
        self.check.save()

        self.channel = Channel(user=self.alice, kind="email")
        self.channel.email = "*****@*****.**"
        self.channel.save()

    def test_it_works(self):
        payload = {
            "channel": self.channel.code,
            "check-%s" % self.check.code: True
        }

        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)
        self.assertRedirects(r, "/integrations/")

        channel = Channel.objects.get(code=self.channel.code)
        checks = channel.checks.all()
        assert len(checks) == 1
        assert checks[0].code == self.check.code

    def test_team_access_works(self):
        payload = {
            "channel": self.channel.code,
            "check-%s" % self.check.code: True
        }

        # Logging in as bob, not alice. Bob has team access so this
        # should work.
        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload, follow=True)
        self.assertEqual(r.status_code, 200)

    def test_it_checks_channel_user(self):
        payload = {"channel": self.channel.code}

        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)

        # self.channel does not belong to charlie, this should fail--
        assert r.status_code == 403

    def test_it_checks_check_user(self):
        charlies_channel = Channel(user=self.charlie, kind="email")
        charlies_channel.email = "*****@*****.**"
        charlies_channel.save()

        payload = {
            "channel": charlies_channel.code,
            "check-%s" % self.check.code: True
        }
        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)

        # mc belongs to charlie but self.check does not--
        assert r.status_code == 403

    def test_it_handles_missing_channel(self):
        # Correct UUID but there is no channel for it:
        payload = {"channel": "6837d6ec-fc08-4da5-a67f-08a9ed1ccf62"}

        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)
        assert r.status_code == 400

    def test_it_handles_missing_check(self):
        # check- key has a correct UUID but there's no check object for it
        payload = {
            "channel": self.channel.code,
            "check-6837d6ec-fc08-4da5-a67f-08a9ed1ccf62": True
        }

        self.client.login(username="******", password="******")
        r = self.client.post("/integrations/", data=payload)
        assert r.status_code == 400
Esempio n. 47
0
 def test_it_handles_legacy_sms_json_value(self):
     c = Channel(kind="sms", value=json.dumps({"value": "+123123123"}))
     self.assertTrue(c.sms_notify_down)
     self.assertFalse(c.sms_notify_up)
Esempio n. 48
0
class SendTestNotificationTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()
        self.channel = Channel(kind="email", project=self.project)
        self.channel.email_verified = True
        self.channel.value = "*****@*****.**"
        self.channel.save()

        self.url = "/integrations/%s/test/" % self.channel.code

    def test_it_sends_test_email(self):

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, {}, follow=True)
        self.assertRedirects(r, self.channels_url)
        self.assertContains(r, "Test notification sent!")

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertTrue("X-Status-Url" in email.extra_headers)
        self.assertTrue("List-Unsubscribe" in email.extra_headers)

        # It should create a notification
        n = Notification.objects.get()
        self.assertEqual(n.channel, self.channel)
        self.assertEqual(n.error, "")

    def test_it_clears_channel_last_error(self):
        self.channel.last_error = "Something went wrong"
        self.channel.save()

        self.client.login(username="******", password="******")
        self.client.post(self.url, {})

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error, "")

    def test_it_sets_channel_last_error(self):
        self.channel.email_verified = False
        self.channel.save()

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, {}, follow=True)

        self.assertContains(r, "Could not send a test notification")
        self.assertContains(r, "Email not verified")

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error, "Email not verified")

    @patch("hc.api.transports.requests.request")
    def test_it_handles_webhooks_with_no_down_url(self, mock_get):
        mock_get.return_value.status_code = 200

        self.channel.kind = "webhook"
        self.channel.value = json.dumps({
            "method_down": "GET",
            "url_down": "",
            "body_down": "",
            "headers_down": {},
            "method_up": "GET",
            "url_up": "http://example-url",
            "body_up": "",
            "headers_up": {},
        })
        self.channel.save()

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, {}, follow=True)
        self.assertRedirects(r, self.channels_url)
        self.assertContains(r, "Test notification sent!")

    def test_it_handles_webhooks_with_no_urls(self):
        self.channel.kind = "webhook"
        self.channel.value = json.dumps({
            "method_down": "GET",
            "url_down": "",
            "body_down": "",
            "headers_down": {},
            "method_up": "GET",
            "url_up": "",
            "body_up": "",
            "headers_up": {},
        })
        self.channel.save()

        self.client.login(username="******", password="******")
        r = self.client.post(self.url, {}, follow=True)
        self.assertRedirects(r, self.channels_url)
        self.assertContains(r, "Could not send a test notification")

    def test_it_checks_channel_ownership(self):
        self.client.login(username="******", password="******")
        r = self.client.post(self.url, {}, follow=True)
        self.assertEqual(r.status_code, 404)
Esempio n. 49
0
 def test_it_handles_json_opsgenie_value(self):
     c = Channel(kind="opsgenie")
     c.value = json.dumps({"key": "abc", "region": "eu"})
     self.assertEqual(c.opsgenie_key, "abc")
     self.assertEqual(c.opsgenie_region, "eu")
Esempio n. 50
0
def add_pushover(request):
    if (settings.PUSHOVER_API_TOKEN is None
            or settings.PUSHOVER_SUBSCRIPTION_URL is None):
        raise Http404("pushover integration is not available")

    if not request.user.is_authenticated:
        ctx = {"page": "channels"}
        return render(request, "integrations/add_pushover.html", ctx)

    if request.method == "POST":
        # Initiate the subscription
        state = _prepare_state(request, "pushover")

        failure_url = settings.SITE_ROOT + reverse("hc-channels")
        success_url = (
            settings.SITE_ROOT + reverse("hc-add-pushover") + "?" +
            urlencode({
                "state": state,
                "prio": request.POST.get("po_priority", "0"),
                "prio_up": request.POST.get("po_priority_up", "0"),
            }))
        subscription_url = (settings.PUSHOVER_SUBSCRIPTION_URL + "?" +
                            urlencode({
                                "success": success_url,
                                "failure": failure_url
                            }))

        return redirect(subscription_url)

    # Handle successful subscriptions
    if "pushover_user_key" in request.GET:
        key = _get_validated_code(request, "pushover", "pushover_user_key")
        if key is None:
            return HttpResponseBadRequest()

        # Validate priority
        prio = request.GET.get("prio")
        if prio not in ("-2", "-1", "0", "1", "2"):
            return HttpResponseBadRequest()

        prio_up = request.GET.get("prio_up")
        if prio_up not in ("-2", "-1", "0", "1", "2"):
            return HttpResponseBadRequest()

        if request.GET.get("pushover_unsubscribed") == "1":
            # Unsubscription: delete all Pushover channels for this project
            Channel.objects.filter(project=request.project, kind="po").delete()
            return redirect("hc-channels")

        # Subscription
        channel = Channel(project=request.project, kind="po")
        channel.value = "%s|%s|%s" % (key, prio, prio_up)
        channel.save()
        channel.assign_all_checks()

        messages.success(request, "The Pushover integration has been added!")
        return redirect("hc-channels")

    # Show Integration Settings form
    ctx = {
        "page": "channels",
        "project": request.project,
        "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
        "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
    }
    return render(request, "integrations/add_pushover.html", ctx)
Esempio n. 51
0
class NotifyTestCase(BaseTestCase):
    def _setup_data(self, kind, value, status="down", email_verified=True):
        self.check = Check()
        self.check.status = status
        self.check.user = self.alice
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(user=self.alice)
        self.channel.kind = kind
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_webhook(self, mock_get):
        self._setup_data("webhook", "http://example")
        mock_get.return_value.status_code = 200

        self.channel.notify(self.check)
        mock_get.assert_called_with("get",
                                    u"http://example",
                                    headers={"User-Agent": "healthchecks.io"},
                                    timeout=5)

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_webhooks_handle_timeouts(self, mock_get):
        self._setup_data("webhook", "http://example")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
    def test_webhooks_handle_connection_errors(self, mock_get):
        self._setup_data("webhook", "http://example")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection failed")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_ignore_up_events(self, mock_get):
        self._setup_data("webhook", "http://example", status="up")
        self.channel.notify(self.check)

        self.assertFalse(mock_get.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_500(self, mock_get):
        self._setup_data("webhook", "http://example")
        mock_get.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables(self, mock_get):
        template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
        self._setup_data("webhook", template)
        self.check.name = "Hello World"
        self.check.tags = "foo bar"
        self.check.save()

        self.channel.notify(self.check)

        url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \
            % self.check.code

        args, kwargs = mock_get.call_args
        self.assertEqual(args[0], "get")
        self.assertEqual(args[1], url)
        self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
        self.assertEqual(kwargs["timeout"], 5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_post(self, mock_request):
        template = "http://example.com\n\nThe Time Is $NOW"
        self._setup_data("webhook", template)
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args
        self.assertEqual(args[0], "post")
        self.assertEqual(args[1], "http://example.com")

        # spaces should not have been urlencoded:
        payload = kwargs["data"].decode("utf-8")
        self.assertTrue(payload.startswith("The Time Is 2"))

    @patch("hc.api.transports.requests.request")
    def test_webhooks_dollarsign_escaping(self, mock_get):
        # If name or tag contains what looks like a variable reference,
        # that should be left alone:

        template = "http://host/$NAME"
        self._setup_data("webhook", template)
        self.check.name = "$TAG1"
        self.check.tags = "foo"
        self.check.save()

        self.channel.notify(self.check)

        url = u"http://host/%24TAG1"
        mock_get.assert_called_with("get",
                                    url,
                                    headers={"User-Agent": "healthchecks.io"},
                                    timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhook_fires_on_up_event(self, mock_get):
        self._setup_data("webhook", "http://foo\nhttp://bar", status="up")

        self.channel.notify(self.check)

        mock_get.assert_called_with("get",
                                    "http://bar",
                                    headers={"User-Agent": "healthchecks.io"},
                                    timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_unicode_post_body(self, mock_request):
        template = u"http://example.com\n\n(╯°□°)╯︵ ┻━┻"
        self._setup_data("webhook", template)
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args

        # unicode should be encoded into utf-8
        self.assertTrue(isinstance(kwargs["data"], binary_type))

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_json_value(self, mock_request):
        definition = {"url_down": "http://foo.com"}
        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io"}
        mock_request.assert_called_with("get",
                                        "http://foo.com",
                                        headers=headers,
                                        timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_json_up_event(self, mock_request):
        definition = {"url_up": "http://bar"}

        self._setup_data("webhook", json.dumps(definition), status="up")
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io"}
        mock_request.assert_called_with("get",
                                        "http://bar",
                                        headers=headers,
                                        timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_post_headers(self, mock_request):
        definition = {
            "url_down": "http://foo.com",
            "post_data": "data",
            "headers": {
                "Content-Type": "application/json"
            }
        }

        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {
            "User-Agent": "healthchecks.io",
            "Content-Type": "application/json"
        }
        mock_request.assert_called_with("post",
                                        "http://foo.com",
                                        data=b"data",
                                        headers=headers,
                                        timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_get_headers(self, mock_request):
        definition = {
            "url_down": "http://foo.com",
            "headers": {
                "Content-Type": "application/json"
            }
        }

        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {
            "User-Agent": "healthchecks.io",
            "Content-Type": "application/json"
        }
        mock_request.assert_called_with("get",
                                        "http://foo.com",
                                        headers=headers,
                                        timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_allow_user_agent_override(self, mock_request):
        definition = {
            "url_down": "http://foo.com",
            "headers": {
                "User-Agent": "My-Agent"
            }
        }

        self._setup_data("webhook", json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "My-Agent"}
        mock_request.assert_called_with("get",
                                        "http://foo.com",
                                        headers=headers,
                                        timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables_in_headers(self, mock_request):
        definition = {
            "url_down": "http://foo.com",
            "headers": {
                "X-Message": "$NAME is DOWN"
            }
        }

        self._setup_data("webhook", json.dumps(definition))
        self.check.name = "Foo"
        self.check.save()

        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
        mock_request.assert_called_with("get",
                                        "http://foo.com",
                                        headers=headers,
                                        timeout=5)

    def test_email(self):
        self._setup_data("email", "*****@*****.**")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertTrue("X-Bounce-Url" in email.extra_headers)

    def test_it_skips_unverified_email(self):
        self._setup_data("email", "*****@*****.**", email_verified=False)
        self.channel.notify(self.check)

        # If an email is not verified, it should be skipped over
        # without logging a notification:
        self.assertEqual(Notification.objects.count(), 0)
        self.assertEqual(len(mail.outbox), 0)

    @patch("hc.api.transports.requests.request")
    def test_pd(self, mock_post):
        self._setup_data("pd", "123")
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")
        self.assertEqual(payload["service_key"], "123")

    @patch("hc.api.transports.requests.request")
    def test_pd_complex(self, mock_post):
        self._setup_data("pd", json.dumps({"service_key": "456"}))
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")
        self.assertEqual(payload["service_key"], "456")

    @patch("hc.api.transports.requests.request")
    def test_pagertree(self, mock_post):
        self._setup_data("pagertree", "123")
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")

    @patch("hc.api.transports.requests.request")
    def test_slack(self, mock_post):
        self._setup_data("slack", "123")
        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["json"]
        attachment = payload["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "an hour ago")

    @patch("hc.api.transports.requests.request")
    def test_slack_with_complex_value(self, mock_post):
        v = json.dumps({"incoming_webhook": {"url": "123"}})
        self._setup_data("slack", v)
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        self.assertEqual(args[1], "123")

    @patch("hc.api.transports.requests.request")
    def test_slack_handles_500(self, mock_post):
        self._setup_data("slack", "123")
        mock_post.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_slack_handles_timeout(self, mock_post):
        self._setup_data("slack", "123")

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request")
    def test_slack_with_tabs_in_schedule(self, mock_post):
        self._setup_data("slack", "123")
        self.check.kind = "cron"
        self.check.schedule = "*\t* * * *"
        self.check.save()
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 1)
        self.assertTrue(mock_post.called)

    @patch("hc.api.transports.requests.request")
    def test_hipchat(self, mock_post):
        self._setup_data("hipchat", "123")
        mock_post.return_value.status_code = 204

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        args, kwargs = mock_post.call_args
        payload = kwargs["json"]
        self.assertIn("DOWN", payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_opsgenie(self, mock_post):
        self._setup_data("opsgenie", "123")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        args, kwargs = mock_post.call_args
        payload = kwargs["json"]
        self.assertIn("DOWN", payload["message"])

    @patch("hc.api.transports.requests.request")
    def test_pushover(self, mock_post):
        self._setup_data("po", "123|0")
        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.assertIn("DOWN", payload["title"])

    @patch("hc.api.transports.requests.request")
    def test_victorops(self, mock_post):
        self._setup_data("victorops", "123")
        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["json"]
        self.assertEqual(payload["message_type"], "CRITICAL")

    @patch("hc.api.transports.requests.request")
    def test_discord(self, mock_post):
        v = json.dumps({"webhook": {"url": "123"}})
        self._setup_data("discord", v)
        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["json"]
        attachment = payload["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "an hour ago")

    @patch("hc.api.transports.requests.request")
    def test_pushbullet(self, mock_post):
        self._setup_data("pushbullet", "fake-token")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        _, kwargs = mock_post.call_args
        self.assertEqual(kwargs["json"]["type"], "note")
        self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")

    @patch("hc.api.transports.requests.request")
    def test_telegram(self, mock_post):
        v = json.dumps({"id": 123})
        self._setup_data("telegram", v)
        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["json"]
        self.assertEqual(payload["chat_id"], 123)
        self.assertTrue("The check" in payload["text"])

    @patch("hc.api.transports.requests.request")
    def test_sms(self, mock_post):
        self._setup_data("sms", "+1234567890")
        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")
        self.assertFalse(u"\xa0" in payload["Body"])

        # sent SMS counter should go up
        self.profile.refresh_from_db()
        self.assertEqual(self.profile.sms_sent, 1)

    @patch("hc.api.transports.requests.request")
    def test_sms_limit(self, mock_post):
        # At limit already:
        self.profile.last_sms_date = now()
        self.profile.sms_sent = 50
        self.profile.save()

        self._setup_data("sms", "+1234567890")

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)

        n = Notification.objects.get()
        self.assertTrue("Monthly SMS limit exceeded" in n.error)

    @patch("hc.api.transports.requests.request")
    def test_sms_limit_reset(self, mock_post):
        # At limit, but also into a new month
        self.profile.sms_sent = 50
        self.profile.last_sms_date = now() - td(days=100)
        self.profile.save()

        self._setup_data("sms", "+1234567890")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertTrue(mock_post.called)
Esempio n. 52
0
def add_email(request):
    if request.method == "POST":
        form = AddEmailForm(request.POST)
        if form.is_valid():
            channel = Channel(project=request.project, kind="email")
            channel.value = json.dumps({
                "value": form.cleaned_data["value"],
                "up": form.cleaned_data["up"],
                "down": form.cleaned_data["down"],
            })
            channel.save()

            channel.assign_all_checks()

            is_own_email = form.cleaned_data["value"] == request.user.email
            if is_own_email or not settings.EMAIL_USE_VERIFICATION:
                # If user is subscribing *their own* address
                # we can skip the verification step.

                # Additionally, in self-hosted setting, administator has the
                # option to disable the email verification step altogether.

                channel.email_verified = True
                channel.save()
            else:
                channel.send_verify_link()

            return redirect("hc-channels")
    else:
        form = AddEmailForm()

    ctx = {
        "page": "channels",
        "project": request.project,
        "use_verification": settings.EMAIL_USE_VERIFICATION,
        "form": form,
    }
    return render(request, "integrations/add_email.html", ctx)
Esempio n. 53
0
class NotifyPdTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.name = "Foo"
        self.check.desc = "Description goes here"
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "pd"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_it_works(self, mock_post):
        self._setup_data("123")
        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["json"]
        self.assertEqual(payload["description"], "Foo is DOWN")
        self.assertEqual(payload["details"]["Description"],
                         "Description goes here")
        self.assertEqual(payload["event_type"], "trigger")
        self.assertEqual(payload["service_key"], "123")

    @patch("hc.api.transports.requests.request")
    def test_pd_complex(self, mock_post):
        self._setup_data(json.dumps({"service_key": "456"}))
        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["json"]
        self.assertEqual(payload["event_type"], "trigger")
        self.assertEqual(payload["service_key"], "456")

    @override_settings(PD_ENABLED=False)
    def test_it_requires_pd_enabled(self):
        self._setup_data("123")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "PagerDuty notifications are not enabled.")

    @patch("hc.api.transports.requests.request")
    def test_it_does_not_escape_description(self, mock_post):
        self._setup_data("123")
        self.check.name = "Foo & Bar"
        self.check.save()

        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["json"]
        self.assertEqual(payload["description"], "Foo & Bar is DOWN")
class NotifyWebhookTestCase(BaseTestCase):
    def _setup_data(self, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = "webhook"
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_webhook(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://example",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        mock_get.return_value.status_code = 200

        self.channel.notify(self.check)
        mock_get.assert_called_with(
            "get",
            "http://example",
            headers={"User-Agent": "healthchecks.io"},
            timeout=10,
        )

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_webhooks_handle_timeouts(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://example",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check)

        # The transport should have retried 3 times
        self.assertEqual(mock_get.call_count, 3)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

        self.channel.refresh_from_db()
        self.assertEqual(self.channel.last_error, "Connection timed out")

    @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
    def test_webhooks_handle_connection_errors(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://example",
            "body_down": "",
            "headers_down": {},
        }
        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check)

        # The transport should have retried 3 times
        self.assertEqual(mock_get.call_count, 3)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection failed")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_500(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://example",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        mock_get.return_value.status_code = 500

        self.channel.notify(self.check)

        # The transport should have retried 3 times
        self.assertEqual(mock_get.call_count, 3)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_webhooks_dont_retry_when_sending_test_notifications(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://example",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check, is_test=True)

        # is_test flag is set, the transport should not retry:
        self.assertEqual(mock_get.call_count, 1)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.check.name = "Hello World"
        self.check.tags = "foo bar"
        self.check.save()

        self.channel.notify(self.check)

        url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code

        args, kwargs = mock_get.call_args
        self.assertEqual(args[0], "get")
        self.assertEqual(args[1], url)
        self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
        self.assertEqual(kwargs["timeout"], 10)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_variable_variables(self, mock_get):
        definition = {
            "method_down": "GET",
            "url_down": "http://host/$$NAMETAG1",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.check.tags = "foo bar"
        self.check.save()

        self.channel.notify(self.check)

        # $$NAMETAG1 should *not* get transformed to "foo"
        args, kwargs = mock_get.call_args
        self.assertEqual(args[1], "http://host/$TAG1")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_post(self, mock_request):
        definition = {
            "method_down": "POST",
            "url_down": "http://example.com",
            "body_down": "The Time Is $NOW",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args
        self.assertEqual(args[0], "post")
        self.assertEqual(args[1], "http://example.com")

        # spaces should not have been urlencoded:
        payload = kwargs["data"].decode()
        self.assertTrue(payload.startswith("The Time Is 2"))

    @patch("hc.api.transports.requests.request")
    def test_webhooks_dollarsign_escaping(self, mock_get):
        # If name or tag contains what looks like a variable reference,
        # that should be left alone:
        definition = {
            "method_down": "GET",
            "url_down": "http://host/$NAME",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.check.name = "$TAG1"
        self.check.tags = "foo"
        self.check.save()

        self.channel.notify(self.check)

        url = "http://host/%24TAG1"
        mock_get.assert_called_with(
            "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=10
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_up_events(self, mock_get):
        definition = {
            "method_up": "GET",
            "url_up": "http://bar",
            "body_up": "",
            "headers_up": {},
        }
        self._setup_data(json.dumps(definition), status="up")

        self.channel.notify(self.check)

        mock_get.assert_called_with(
            "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=10
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_noop_up_events(self, mock_get):
        definition = {
            "method_up": "GET",
            "url_up": "",
            "body_up": "",
            "headers_up": {},
        }

        self._setup_data(json.dumps(definition), status="up")
        self.channel.notify(self.check)

        self.assertFalse(mock_get.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_unicode_post_body(self, mock_request):
        definition = {
            "method_down": "POST",
            "url_down": "http://foo.com",
            "body_down": "(╯°□°)╯︵ ┻━┻",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args

        # unicode should be encoded into utf-8
        self.assertIsInstance(kwargs["data"], bytes)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_post_headers(self, mock_request):
        definition = {
            "method_down": "POST",
            "url_down": "http://foo.com",
            "body_down": "data",
            "headers_down": {"Content-Type": "application/json"},
        }

        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
        mock_request.assert_called_with(
            "post", "http://foo.com", data=b"data", headers=headers, timeout=10
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_get_headers(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {"Content-Type": "application/json"},
        }

        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=10
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_allow_user_agent_override(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {"User-Agent": "My-Agent"},
        }

        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check)

        headers = {"User-Agent": "My-Agent"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=10
        )

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables_in_headers(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "body_down": "",
            "headers_down": {"X-Message": "$NAME is DOWN"},
        }

        self._setup_data(json.dumps(definition))
        self.check.name = "Foo"
        self.check.save()

        self.channel.notify(self.check)

        headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
        mock_request.assert_called_with(
            "get", "http://foo.com", headers=headers, timeout=10
        )

    @override_settings(WEBHOOKS_ENABLED=False)
    def test_it_requires_webhooks_enabled(self):
        definition = {
            "method_down": "GET",
            "url_down": "http://example",
            "body_down": "",
            "headers_down": {},
        }

        self._setup_data(json.dumps(definition))
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Webhook notifications are not enabled.")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_non_ascii_in_headers(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "headers_down": {"X-Foo": "bār"},
            "body_down": "",
        }

        self._setup_data(json.dumps(definition))
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args

        self.assertEqual(kwargs["headers"]["X-Foo"], "b&#257;r")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_handle_latin1_in_headers(self, mock_request):
        definition = {
            "method_down": "GET",
            "url_down": "http://foo.com",
            "headers_down": {"X-Foo": "½"},
            "body_down": "",
        }

        self._setup_data(json.dumps(definition))
        self.check.save()

        self.channel.notify(self.check)
        args, kwargs = mock_request.call_args

        self.assertEqual(kwargs["headers"]["X-Foo"], "½")
    def setUp(self):
        super(UpdateChannelNameTestCase, self).setUp()
        self.channel = Channel(kind="email", project=self.project)
        self.channel.save()

        self.url = "/integrations/%s/name/" % self.channel.code
Esempio n. 56
0
class NotifyTestCase(BaseTestCase):

    def _setup_data(self, kind, value, status="down", email_verified=True):
        self.check = Check()
        self.check.status = status
        self.check.user = self.alice
        self.check.save()

        self.channel = Channel(user=self.alice)
        self.channel.kind = kind
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_webhook(self, mock_get):
        self._setup_data("webhook", "http://example")
        mock_get.return_value.status_code = 200

        self.channel.notify(self.check)
        mock_get.assert_called_with(
            "get", u"http://example",
            headers={"User-Agent": "healthchecks.io"}, timeout=5)

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_webhooks_handle_timeouts(self, mock_get):
        self._setup_data("webhook", "http://example")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request")
    def test_webhooks_ignore_up_events(self, mock_get):
        self._setup_data("webhook", "http://example", status="up")
        self.channel.notify(self.check)

        self.assertFalse(mock_get.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_support_variables(self, mock_get):
        template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
        self._setup_data("webhook", template)
        self.check.name = "Hello World"
        self.check.tags = "foo bar"
        self.check.save()

        self.channel.notify(self.check)

        url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \
            % self.check.code

        mock_get.assert_called_with(
            "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhooks_dollarsign_escaping(self, mock_get):
        # If name or tag contains what looks like a variable reference,
        # that should be left alone:

        template = "http://host/$NAME"
        self._setup_data("webhook", template)
        self.check.name = "$TAG1"
        self.check.tags = "foo"
        self.check.save()

        self.channel.notify(self.check)

        url = u"http://host/%24TAG1"
        mock_get.assert_called_with(
            "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5)

    @patch("hc.api.transports.requests.request")
    def test_webhook_fires_on_up_event(self, mock_get):
        self._setup_data("webhook", "http://foo\nhttp://bar", status="up")

        self.channel.notify(self.check)

        mock_get.assert_called_with(
            "get", "http://bar", headers={"User-Agent": "healthchecks.io"},
            timeout=5)

    def test_email(self):
        self._setup_data("email", "*****@*****.**")
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

    def test_it_skips_unverified_email(self):
        self._setup_data("email", "*****@*****.**", email_verified=False)
        self.channel.notify(self.check)

        assert Notification.objects.count() == 1
        n = Notification.objects.first()
        self.assertEqual(n.error, "Email not verified")
        self.assertEqual(len(mail.outbox), 0)

    @override_settings(USE_PAYMENTS=True)
    def test_email_contains_upgrade_notice(self):
        self._setup_data("email", "*****@*****.**", status="up")
        self.profile.team_access_allowed = False
        self.profile.save()

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        # Check is up, payments are enabled, and the user does not have team
        # access: the email should contain upgrade note
        message = mail.outbox[0]
        html, _ = message.alternatives[0]
        assert "/pricing/" in html

    @patch("hc.api.transports.requests.request")
    def test_pd(self, mock_post):
        self._setup_data("pd", "123")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        json = kwargs["json"]
        self.assertEqual(json["event_type"], "trigger")

    @patch("hc.api.transports.requests.request")
    def test_slack(self, mock_post):
        self._setup_data("slack", "123")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        json = kwargs["json"]
        attachment = json["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "Never")

    @patch("hc.api.transports.requests.request")
    def test_slack_with_complex_value(self, mock_post):
        v = json.dumps({"incoming_webhook": {"url": "123"}})
        self._setup_data("slack", v)
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        self.assertEqual(args[1], "123")

    @patch("hc.api.transports.requests.request")
    def test_slack_handles_500(self, mock_post):
        self._setup_data("slack", "123")
        mock_post.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Received status code 500")

    @patch("hc.api.transports.requests.request", side_effect=Timeout)
    def test_slack_handles_timeout(self, mock_post):
        self._setup_data("slack", "123")

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection timed out")

    @patch("hc.api.transports.requests.request")
    def test_hipchat(self, mock_post):
        self._setup_data("hipchat", "123")
        mock_post.return_value.status_code = 204

        self.channel.notify(self.check)
        n = Notification.objects.first()
        self.assertEqual(n.error, "")

        args, kwargs = mock_post.call_args
        json = kwargs["json"]
        self.assertIn("DOWN", json["message"])

    @patch("hc.api.transports.requests.request")
    def test_pushover(self, mock_post):
        self._setup_data("po", "123|0")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        json = kwargs["data"]
        self.assertIn("DOWN", json["title"])

    @patch("hc.api.transports.requests.request")
    def test_victorops(self, mock_post):
        self._setup_data("victorops", "123")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        json = kwargs["json"]
        self.assertEqual(json["message_type"], "CRITICAL")

    ### Test that the web hooks handle connection errors and error 500s

    @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
    def test_webhook_connection_error(self, mock_post):
        self._setup_data("webhook", "http://example")
        mock_post.return_value.status_code = 500

        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Connection failed")
class CreateCheckTestCase(BaseTestCase):
    URL = "/api/v1/checks/"

    def setUp(self):
        super(CreateCheckTestCase, self).setUp()

    def post(self, data, expected_error=None):
        r = self.client.post(self.URL,
                             json.dumps(data),
                             content_type="application/json")

        if expected_error:
            self.assertEqual(r.status_code, 400)
            ### Assert that the expected error is the response error

            self.assertEqual(r.json()['error'], expected_error)

        return r

    def test_it_works(self):
        r = self.post({
            "api_key": "abc",
            "name": "Foo",
            "tags": "bar,baz",
            "timeout": 3600,
            "grace": 60
        })

        self.assertEqual(r.status_code, 201)

        doc = r.json()
        assert "ping_url" in doc
        self.assertEqual(doc["name"], "Foo")
        self.assertEqual(doc["tags"], "bar,baz")

        ### Assert the expected last_ping and n_pings values
        self.assertEqual(doc['n_pings'], 0)
        self.assertEqual(doc['last_ping'], None)

        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_accepts_api_key_in_header(self):
        payload = json.dumps({"name": "Foo"})

        ### Make the post request and get the response
        # r = {'status_code': 201} ### This is just a placeholder variable

        r = self.client.post(self.URL,
                             payload,
                             content_type="application/json",
                             HTTP_X_API_KEY="abc")

        self.assertEqual(r.status_code, 201)

    def test_it_handles_missing_request_body(self):
        ### Make the post request with a missing body and get the response
        # r = {'status_code': 400, 'error': "wrong api_key"} ### This is just a placeholder variable

        r = self.post({})

        self.assertEqual(r.status_code, 400)
        self.assertEqual(r.json()['error'], "wrong api_key")

    def test_it_handles_invalid_json(self):
        ### Make the post request with invalid json data type
        # r = {'status_code': 400, 'error': "could not parse request body"} ### This is just a placeholder variable
        payload = {"name": "", "api_key": "abc"}  #without json.dumps()
        r = self.client.post(self.URL,
                             payload,
                             content_type="application/json",
                             HTTP_X_API_KEY="abc")

        self.assertEqual(r.status_code, 400)
        self.assertEqual(r.json()["error"], "could not parse request body")

    def test_it_rejects_wrong_api_key(self):
        self.post({"api_key": "wrong"}, expected_error="wrong api_key")

    def test_it_rejects_non_number_timeout(self):
        self.post({
            "api_key": "abc",
            "timeout": "oops"
        },
                  expected_error="timeout is not a number")

    def test_it_rejects_non_string_name(self):
        self.post({
            "api_key": "abc",
            "name": False
        },
                  expected_error="name is not a string")

    ### Test for the assignment of channels
    def test_assigns_channels(self):

        self.channel = Channel(user=self.alice)
        self.channel.kind = "webhook"
        self.channel.value = "http://example.com"
        self.channel.email_verified = True
        self.channel.save()

        r = self.post({
            "api_key": "abc",
            "name": "Foo",
            "tags": "bar,baz",
            "timeout": 3600,
            "grace": 60,
            "channels": "*"
        })

        check_channel = [
            channel for channel in Check.objects.get().channel_set.all()
        ][0]

        self.assertEqual(check_channel.value, "http://example.com")

    ### Test for the 'timeout is too small' and 'timeout is too large' errors

    # test timeout is too small
    def test_timeout_is_too_small(self):
        r = self.post({
            "api_key": "abc",
            "name": "Foo",
            "tags": "bar,baz",
            "timeout": 50,
            "grace": 60,
            "channels": "*"
        })

        self.assertEqual(r.json()['error'], "timeout is too small")

    # test timeout is too large
    def test_timeout_is_too_large(self):
        r = self.post({
            "api_key": "abc",
            "name": "Foo",
            "tags": "bar,baz",
            "timeout": 604801,
            "grace": 60,
            "channels": "*"
        })

        self.assertEqual(r.json()['error'], "timeout is too large")
Esempio n. 58
0
class NotifySignalTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        self.check = Check(project=self.project)
        self.check.name = "Daily Backup"
        self.check.status = "down"
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        payload = {"value": "+123456789", "up": True, "down": True}
        self.channel = Channel(project=self.project)
        self.channel.kind = "signal"
        self.channel.value = json.dumps(payload)
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.dbus")
    def test_it_works(self, mock_bus):
        self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "")

        args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args
        message, attachments, recipients = args[-1]

        self.assertIn("is DOWN", message)
        self.assertEqual(recipients, ["+123456789"])

        # Only one check in the project, so there should be no note about
        # other checks:
        self.assertNotIn("All the other checks are up.", message)

    @patch("hc.api.transports.dbus")
    def test_it_obeys_down_flag(self, mock_bus):
        payload = {"value": "+123456789", "up": True, "down": False}
        self.channel.value = json.dumps(payload)
        self.channel.save()

        self.channel.notify(self.check)

        # This channel should not notify on "down" events:
        self.assertEqual(Notification.objects.count(), 0)

        self.assertFalse(mock_bus.SystemBus.called)

    @patch("hc.api.transports.dbus")
    def test_it_requires_signal_cli_enabled(self, mock_bus):
        with override_settings(SIGNAL_CLI_ENABLED=False):
            self.channel.notify(self.check)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Signal notifications are not enabled")

        self.assertFalse(mock_bus.SystemBus.called)

    @patch("hc.api.transports.dbus")
    def test_it_does_not_escape_special_characters(self, mock_bus):
        self.check.name = "Foo & Bar"
        self.check.save()

        self.channel.notify(self.check)

        args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args
        message, attachments, recipients = args[-1]
        self.assertIn("Foo & Bar", message)

    @override_settings(SECRET_KEY="test-secret")
    @patch("hc.api.transports.dbus")
    def test_it_obeys_rate_limit(self, mock_bus):
        # "2862..." is sha1("+123456789test-secret")
        obj = TokenBucket(
            value="signal-2862991ccaa15c8856e7ee0abaf3448fb3c292e0")
        obj.tokens = 0
        obj.save()

        self.channel.notify(self.check)
        n = Notification.objects.get()
        self.assertEqual(n.error, "Rate limit exceeded")

        self.assertFalse(mock_bus.SysemBus.called)

    @patch("hc.api.transports.dbus")
    def test_it_shows_all_other_checks_up_note(self, mock_bus):
        other = Check(project=self.project)
        other.name = "Foobar"
        other.status = "up"
        other.last_ping = now() - td(minutes=61)
        other.save()

        self.channel.notify(self.check)

        args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args
        message, attachments, recipients = args[-1]
        self.assertIn("All the other checks are up.", message)

    @patch("hc.api.transports.dbus")
    def test_it_lists_other_down_checks(self, mock_bus):
        other = Check(project=self.project)
        other.name = "Foobar"
        other.status = "down"
        other.last_ping = now() - td(minutes=61)
        other.save()

        self.channel.notify(self.check)

        args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args
        message, attachments, recipients = args[-1]
        self.assertIn("The following checks are also down", message)
        self.assertIn("Foobar", message)

    @patch("hc.api.transports.dbus")
    def test_it_does_not_show_more_than_10_other_checks(self, mock_bus):
        for i in range(0, 11):
            other = Check(project=self.project)
            other.name = f"Foobar #{i}"
            other.status = "down"
            other.last_ping = now() - td(minutes=61)
            other.save()

        self.channel.notify(self.check)

        args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args
        message, attachments, recipients = args[-1]
        self.assertNotIn("Foobar", message)
        self.assertIn("11 other checks are also down.", message)
Esempio n. 59
0
class NotifyTestCase(BaseTestCase):
    def _setup_data(self, kind, value, status="down", email_verified=True):
        self.check = Check(project=self.project)
        self.check.status = status
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        self.channel = Channel(project=self.project)
        self.channel.kind = kind
        self.channel.value = value
        self.channel.email_verified = email_verified
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_pagerteam(self, mock_post):
        self._setup_data("pagerteam", "123")

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_hipchat(self, mock_post):
        self._setup_data("hipchat", "123")

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)
        self.assertEqual(Notification.objects.count(), 0)

    @patch("hc.api.transports.requests.request")
    def test_discord(self, mock_post):
        v = json.dumps({"webhook": {"url": "123"}})
        self._setup_data("discord", v)
        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["json"]
        attachment = payload["attachments"][0]
        fields = {f["title"]: f["value"] for f in attachment["fields"]}
        self.assertEqual(fields["Last Ping"], "an hour ago")

    @patch("hc.api.transports.requests.request")
    def test_discord_rewrites_discordapp_com(self, mock_post):
        v = json.dumps({"webhook": {"url": "https://discordapp.com/foo"}})
        self._setup_data("discord", v)
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        args, kwargs = mock_post.call_args
        url = args[1]

        # discordapp.com is deprecated. For existing webhook URLs, wwe should
        # rewrite discordapp.com to discord.com:
        self.assertEqual(url, "https://discord.com/foo/slack")

    @patch("hc.api.transports.requests.request")
    def test_pushbullet(self, mock_post):
        self._setup_data("pushbullet", "fake-token")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        assert Notification.objects.count() == 1

        _, kwargs = mock_post.call_args
        self.assertEqual(kwargs["json"]["type"], "note")
        self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")

    @patch("hc.api.transports.requests.request")
    def test_call(self, mock_post):
        self.profile.call_limit = 1
        self.profile.save()

        value = {"label": "foo", "value": "+1234567890"}
        self._setup_data("call", json.dumps(value))
        self.check.last_ping = now() - td(hours=2)

        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertEqual(payload["To"], "+1234567890")

        n = Notification.objects.get()
        callback_path = f"/api/v1/notifications/{n.code}/status"
        self.assertTrue(payload["StatusCallback"].endswith(callback_path))

    @patch("hc.api.transports.requests.request")
    def test_call_limit(self, mock_post):
        # At limit already:
        self.profile.call_limit = 50
        self.profile.last_call_date = now()
        self.profile.calls_sent = 50
        self.profile.save()

        definition = {"value": "+1234567890"}
        self._setup_data("call", json.dumps(definition))

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)

        n = Notification.objects.get()
        self.assertTrue("Monthly phone call limit exceeded" in n.error)

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertEqual(email.subject, "Monthly Phone Call Limit Reached")

    @patch("hc.api.transports.requests.request")
    def test_call_limit_reset(self, mock_post):
        # At limit, but also into a new month
        self.profile.call_limit = 50
        self.profile.calls_sent = 50
        self.profile.last_call_date = now() - td(days=100)
        self.profile.save()

        self._setup_data("call", "+1234567890")
        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)
        self.assertTrue(mock_post.called)

    def test_not_implimented(self):
        self._setup_data("webhook", "http://example")
        self.channel.kind = "invalid"

        with self.assertRaises(NotImplementedError):
            self.channel.notify(self.check)

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=True)
    def test_shell(self, mock_system):
        definition = {"cmd_down": "logger hello", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))
        mock_system.return_value = 0

        self.channel.notify(self.check)
        mock_system.assert_called_with("logger hello")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=True)
    def test_shell_handles_nonzero_exit_code(self, mock_system):
        definition = {"cmd_down": "logger hello", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))
        mock_system.return_value = 123

        self.channel.notify(self.check)
        n = Notification.objects.get()
        self.assertEqual(n.error, "Command returned exit code 123")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=True)
    def test_shell_supports_variables(self, mock_system):
        definition = {
            "cmd_down": "logger $NAME is $STATUS ($TAG1)",
            "cmd_up": ""
        }
        self._setup_data("shell", json.dumps(definition))
        mock_system.return_value = 0

        self.check.name = "Database"
        self.check.tags = "foo bar"
        self.check.save()
        self.channel.notify(self.check)

        mock_system.assert_called_with("logger Database is down (foo)")

    @patch("hc.api.transports.os.system")
    @override_settings(SHELL_ENABLED=False)
    def test_shell_disabled(self, mock_system):
        definition = {"cmd_down": "logger hello", "cmd_up": ""}
        self._setup_data("shell", json.dumps(definition))

        self.channel.notify(self.check)
        self.assertFalse(mock_system.called)

        n = Notification.objects.get()
        self.assertEqual(n.error, "Shell commands are not enabled")
class NotifyWhatsAppTestCase(BaseTestCase):
    def _setup_data(self, notify_up=True, notify_down=True):
        self.check = Check(project=self.project)
        self.check.status = "down"
        self.check.last_ping = now() - td(minutes=61)
        self.check.save()

        definition = {
            "value": "+1234567890",
            "up": notify_up,
            "down": notify_down
        }

        self.channel = Channel(project=self.project, kind="whatsapp")
        self.channel.value = json.dumps(definition)
        self.channel.save()
        self.channel.checks.add(self.check)

    @patch("hc.api.transports.requests.request")
    def test_it_works(self, mock_post):
        mock_post.return_value.status_code = 200
        self._setup_data()

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertEqual(payload["To"], "whatsapp:+1234567890")

        n = Notification.objects.get()
        callback_path = f"/api/v1/notifications/{n.code}/status"
        self.assertTrue(payload["StatusCallback"].endswith(callback_path))

        # sent SMS counter should go up
        self.profile.refresh_from_db()
        self.assertEqual(self.profile.sms_sent, 1)

    @patch("hc.api.transports.requests.request")
    def test_it_obeys_up_down_flags(self, mock_post):
        self._setup_data(notify_down=False)
        self.check.last_ping = now() - td(hours=2)

        self.channel.notify(self.check)
        self.assertEqual(Notification.objects.count(), 0)

        self.assertFalse(mock_post.called)

    @patch("hc.api.transports.requests.request")
    def test_it_enforces_limit(self, mock_post):
        # At limit already:
        self.profile.last_sms_date = now()
        self.profile.sms_sent = 50
        self.profile.save()

        self._setup_data()

        self.channel.notify(self.check)
        self.assertFalse(mock_post.called)

        n = Notification.objects.get()
        self.assertTrue("Monthly message limit exceeded" in n.error)

        # And email should have been sent
        self.assertEqual(len(mail.outbox), 1)

        email = mail.outbox[0]
        self.assertEqual(email.to[0], "*****@*****.**")
        self.assertEqual(email.subject, "Monthly WhatsApp Limit Reached")

    @patch("hc.api.transports.requests.request")
    def test_it_does_not_escape_special_characters(self, mock_post):
        self._setup_data()
        self.check.name = "Foo > Bar & Co"

        mock_post.return_value.status_code = 200

        self.channel.notify(self.check)

        args, kwargs = mock_post.call_args
        payload = kwargs["data"]
        self.assertIn("Foo > Bar & Co", payload["Body"])