Пример #1
0
def handle_email(request):
    if request.method != "POST":
        return HttpResponseBadRequest()

    events = json.loads(request.POST["mandrill_events"])
    for event in events:
        for recipient_address, recipient_name in event["msg"]["to"]:
            code, domain = recipient_address.split("@")
            try:
                check = Check.objects.get(code=code)
            except ValueError:
                continue
            except Check.DoesNotExist:
                continue

            check.n_pings = F("n_pings") + 1
            check.last_ping = timezone.now()
            if check.status == "new":
                check.status = "up"

            check.save()

            ping = Ping(owner=check)
            ping.scheme = "email"
            ping.save()

    response = HttpResponse("OK")
    return response
Пример #2
0
    def setUp(self):
        super(LogTestCase, self).setUp()
        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()
Пример #3
0
    def setUp(self):
        super(LogTestCase, self).setUp()
        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()
Пример #4
0
def handle_email(request):
    if request.method != "POST":
        return HttpResponseBadRequest()

    events = json.loads(request.POST["mandrill_events"])
    for event in events:
        for recipient_address, recipient_name in event["msg"]["to"]:
            code, domain = recipient_address.split("@")
            try:
                check = Check.objects.get(code=code)
            except ValueError:
                continue
            except Check.DoesNotExist:
                continue

            check.n_pings = F("n_pings") + 1
            check.last_ping = timezone.now()
            if check.status == "new":
                check.status = "up"

            check.save()

            ping = Ping(owner=check)
            ping.scheme = "email"
            ping.save()

    response = HttpResponse("OK")
    return response
Пример #5
0
    def setUp(self):
        self.alice = User(username="******")
        self.alice.set_password("password")
        self.alice.save()

        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()
Пример #6
0
class GetPingsTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()

        self.a1 = Check(project=self.project, name="Alice 1")
        self.a1.timeout = td(seconds=3600)
        self.a1.grace = td(seconds=900)
        self.a1.n_pings = 1
        self.a1.status = "new"
        self.a1.tags = "a1-tag a1-additional-tag"
        self.a1.desc = "This is description"
        self.a1.save()

        self.ping = Ping(owner=self.a1)
        self.ping.n = 1
        self.ping.remote_addr = "1.2.3.4"
        self.ping.scheme = "https"
        self.ping.method = "get"
        self.ping.ua = "foo-agent"
        self.ping.save()

        self.url = "/api/v1/checks/%s/pings/" % self.a1.code

    def get(self, api_key="X" * 32):
        return self.csrf_client.get(self.url, HTTP_X_API_KEY=api_key)

    def test_it_works(self):
        r = self.get()
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r["Access-Control-Allow-Origin"], "*")

        doc = r.json()
        self.assertEqual(len(doc["pings"]), 1)

        ping = doc["pings"][0]
        self.assertEqual(ping["n"], 1)
        self.assertEqual(ping["remote_addr"], "1.2.3.4")
        self.assertEqual(ping["scheme"], "https")
        self.assertEqual(ping["method"], "get")
        self.assertEqual(ping["ua"], "foo-agent")

    def test_readonly_key_is_not_allowed(self):
        self.project.api_key_readonly = "R" * 32
        self.project.save()

        r = self.get(api_key=self.project.api_key_readonly)
        self.assertEqual(r.status_code, 401)

    def test_it_rejects_post(self):
        r = self.csrf_client.post(self.url, HTTP_X_API_KEY="X" * 32)
        self.assertEqual(r.status_code, 405)

    def test_it_handles_missing_api_key(self):
        r = self.client.get(self.url)
        self.assertContains(r, "missing api key", status_code=401)
Пример #7
0
def ping(request, code):
    try:
        check = Check.objects.get(code=code)
    except Check.DoesNotExist:
        return HttpResponseBadRequest()

    check.last_ping = timezone.now()
    if check.status == "new":
        check.status = "up"

    check.save()

    ping = Ping(owner=check)
    headers = request.META
    ping.remote_addr = headers.get("HTTP_X_REAL_IP", headers["REMOTE_ADDR"])
    ping.scheme = headers.get("HTTP_X_SCHEME", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.body = request.body
    ping.save()

    response = HttpResponse("OK")
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #8
0
def ping(request, code):
    try:
        check = Check.objects.get(code=code)
    except Check.DoesNotExist:
        return HttpResponseBadRequest()

    check.n_pings = F("n_pings") + 1
    check.last_ping = timezone.now()
    if check.status in ("new", "paused"):
        check.status = "up"
    if check.runs_too_often() == 'over':
        check.status = "over"

    check.save()
    check.refresh_from_db()
    # check from the db send mail to user on jobs that are running too often
    check.runs_too_often()

    ping = Ping(owner=check)
    headers = request.META
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()

    response = HttpResponse("OK" + check.status)
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #9
0
def ping(request, code):
    check = get_object_or_404(Check, code=code)

    check.n_pings = F("n_pings") + 1
    check.last_ping = timezone.now()
    check.last_ping_body = request.body[:10000]
    check.alert_after = check.get_alert_after()
    if check.status in ("new", "paused"):
        check.status = "up"

    check.save()
    check.refresh_from_db()

    ping = Ping(owner=check)
    headers = request.META
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()

    response = HttpResponse("OK")
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #10
0
    def setUp(self):
        super(LogTestCase, self).setUp()
        self.alice = User(username="******", email="*****@*****.**")
        self.alice.set_password("password")
        self.alice.save()

        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()
Пример #11
0
    def setUp(self):
        super(LogTestCase, self).setUp()
        self.alice = User(username="******", email="*****@*****.**")
        self.alice.set_password("password")
        self.alice.save()

        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()
    def test_it_works(self):
        p = Ping(owner=self.check)
        p.created = now()
        p.save()

        n = Notification.objects.create(owner=self.check, channel=self.channel)
        n.created = p.created - td(minutes=1)
        n.save()

        output = Command().handle()
        self.assertIn("Pruned 1 notifications", output)
        self.assertFalse(Notification.objects.exists())
Пример #13
0
def ping(request, code):
    try:
        check = Check.objects.get(code=code)
    except Check.DoesNotExist:
        return HttpResponseBadRequest()

    check.n_pings = F("n_pings") + 1
    check.last_ping = timezone.now()
    if check.status == "new":
        check.status = "up"

    check.save()
    check.refresh_from_db()

    ping = Ping(owner=check)
    headers = request.META
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()

    response = HttpResponse("OK")
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #14
0
    def setUp(self):
        super().setUp()

        self.a1 = Check(project=self.project, name="Alice 1")
        self.a1.timeout = td(seconds=3600)
        self.a1.grace = td(seconds=900)
        self.a1.n_pings = 1
        self.a1.status = "new"
        self.a1.tags = "a1-tag a1-additional-tag"
        self.a1.desc = "This is description"
        self.a1.save()

        self.ping = Ping(owner=self.a1)
        self.ping.n = 1
        self.ping.remote_addr = "1.2.3.4"
        self.ping.scheme = "https"
        self.ping.method = "get"
        self.ping.ua = "foo-agent"
        self.ping.save()

        self.url = "/api/v1/checks/%s/pings/" % self.a1.code
Пример #15
0
def ping(request, code):
    try:
        check = Check.objects.get(code=code)
    except Check.DoesNotExist:
        return HttpResponseBadRequest()

    check.n_pings = F("n_pings") + 1
    if check.last_ping:
        now = timezone.now()
        reverse_grace = check.timeout - check.grace
        if reverse_grace <= td(seconds=0):
            too_early = now
        else:
            too_early = check.last_ping + reverse_grace
        if now <= too_early:
            check.often = True
        else:
            check.often = False

    check.last_ping = timezone.now()
    if check.status in ("new", "paused"):
        check.status = "up"

    check.save()
    check.refresh_from_db()

    if check.often:
        check.send_alert()

    ping = Ping(owner=check)
    headers = request.META
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()

    response = HttpResponse("OK")
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #16
0
    def setUp(self):
        super(LogTestCase, self).setUp()
        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()

        # Older MySQL versions don't store microseconds. This makes sure
        # the ping is older than any notifications we may create later:
        ping.created = "2000-01-01T00:00:00+00:00"
        ping.save()
Пример #17
0
    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)
Пример #18
0
def log(request, code):
    check = get_object_or_404(Check, code=code)
    if check.user != request.user:
        return HttpResponseForbidden()

    profile = Profile.objects.for_user(request.user)
    limit = profile.ping_log_limit
    pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]

    pings = list(pings.iterator())
    # oldest-to-newest order will be more convenient for adding
    # "not received" placeholders:
    pings.reverse()

    # Add a dummy ping object at the end. We iterate over *pairs* of pings
    # and don't want to handle a special case of a check with a single ping.
    pings.append(Ping(created=timezone.now()))

    # Now go through pings, calculate time gaps, and decorate
    # the pings list for convenient use in template
    wrapped = []

    early = False
    for older, newer in pairwise(pings):
        wrapped.append({"ping": older, "early": early})

        # Fill in "missed ping" placeholders:
        expected_date = older.created + check.timeout
        n_blanks = 0
        while expected_date + check.grace < newer.created and n_blanks < 10:
            wrapped.append({"placeholder_date": expected_date})
            expected_date = expected_date + check.timeout
            n_blanks += 1

        # Prepare early flag for next ping to come
        early = older.created + check.timeout > newer.created + check.grace

    reached_limit = len(pings) > limit

    wrapped.reverse()
    ctx = {
        "check": check,
        "pings": wrapped,
        "num_pings": len(pings),
        "limit": limit,
        "show_limit_notice": reached_limit and settings.USE_PAYMENTS
    }

    return render(request, "front/log.html", ctx)
Пример #19
0
def ping(request, code):
    try:
        check = Check.objects.get(code=code)
    except Check.DoesNotExist:
        return HttpResponseBadRequest()

    # num_pings= Ping.objects.filter(owner=check).filter(created__range=(check.prev_ping,deadline))
    # import pdb; pdb.set_trace()
    # if len(num_pings) > 1:
    if check.last_ping:
        deadline = check.last_ping + check.timeout - check.grace
        if timezone.now() > check.last_ping and timezone.now() < deadline:
            check.often = True
        else:
            check.often = False

    check.n_pings = F("n_pings") + 1
    check.last_ping = timezone.now()
    if check.status in ("new", "paused"):
        check.status = "up"

    check.save()
    check.refresh_from_db()
    if check.often:
        check.often_alert()
    ping = Ping(owner=check)
    headers = request.META
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()

    response = HttpResponse("OK")
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #20
0
def ping(request, code):
    try:
        check = Check.objects.get(code=code)
    except Check.DoesNotExist:
        return HttpResponseBadRequest()

    if check.status in ("new", "paused"):
        check.status = "up"

    # only confirm if a check is too_often if the check is not in down-wise statuses
    if check.status not in ("down"):
        if check.running_too_often():
            check.status = "too often"
        else:
            check.status = "up"

    now = timezone.now()
    check.n_pings = F("n_pings") + 1
    check.last_ping = now
    # store expected time for next ping
    check.next_ping = now + check.timeout

    check.save()
    check.refresh_from_db()

    ping = Ping(owner=check)
    headers = request.META
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()

    response = HttpResponse("OK")
    response["Access-Control-Allow-Origin"] = "*"
    return response
Пример #21
0
def _ping(headers, check):
    check.n_pings = F("n_pings") + 1
    check.last_ping = timezone.now()
    if check.status == "new":
        check.status = "up"

    check.save()
    check.refresh_from_db()

    ping = Ping(owner=check)
    ping.n = check.n_pings
    remote_addr = headers.get("HTTP_X_FORWARDED_FOR", headers["REMOTE_ADDR"])
    ping.remote_addr = remote_addr.split(",")[0]
    ping.scheme = headers.get("HTTP_X_FORWARDED_PROTO", "http")
    ping.method = headers["REQUEST_METHOD"]
    # If User-Agent is longer than 200 characters, truncate it:
    ping.ua = headers.get("HTTP_USER_AGENT", "")[:200]
    ping.save()
    return
Пример #22
0
    def setUp(self):
        super(LogTestCase, self).setUp()
        self.check = Check(user=self.alice)
        self.check.save()

        ping = Ping(owner=self.check)
        ping.save()

        # Older MySQL versions don't store microseconds. This makes sure
        # the ping is older than any notifications we may create later:
        ping.created = "2000-01-01T00:00:00+00:00"
        ping.save()
Пример #23
0
class NotifyEmailTestCase(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")
Пример #24
0
class NotifyEmailTestCase(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.n = 1
        self.ping.remote_addr = "1.2.3.4"
        self.ping.body_raw = b"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_it_works(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.assertEqual(email.extra_headers["X-Status-Url"], n.status_url())
        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)

        # Check's code must not be in the html
        self.assertNotIn(str(self.check.code), html)

        # Check's code must not be in the plain text body
        self.assertNotIn(str(self.check.code), email.body)

    def test_it_displays_body(self):
        self.ping.body = "Body Line 1\nBody Line 2"
        self.ping.body_raw = None
        self.ping.save()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        html = email.alternatives[0][0]
        self.assertIn("Line 1<br>Line2", html)

    @override_settings(S3_BUCKET="test-bucket")
    @patch("hc.api.models.get_object")
    def test_it_loads_body_from_object_storage(self, get_object):
        get_object.return_value = b"Body Line 1\nBody Line 2"

        self.ping.object_size = 1000
        self.ping.body_raw = None
        self.ping.save()

        self.channel.notify(self.check)

        email = mail.outbox[0]
        html = email.alternatives[0][0]
        self.assertIn("Line 1<br>Line2", html)

        args, kwargs = get_object.call_args
        code, n = args
        self.assertEqual(code, self.check.code)
        self.assertEqual(n, 1)

    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")

    @override_settings(S3_BUCKET="test-bucket")
    @patch("hc.api.models.get_object")
    def test_it_handles_pending_body(self, get_object):
        get_object.return_value = None

        self.ping.object_size = 1000
        self.ping.body_raw = None
        self.ping.save()

        with patch("hc.api.transports.time.sleep"):
            self.channel.notify(self.check)

        email = mail.outbox[0]
        html = email.alternatives[0][0]
        self.assertIn("The request body data is being processed", html)