@ -1,6 +1,7 @@ | |||||
from django.conf import settings | from django.conf import settings | ||||
from django.core import mail | from django.core import mail | ||||
from hc.api.models import Check | |||||
from django.test.utils import override_settings | |||||
from hc.api.models import Check, TokenBucket | |||||
from hc.test import BaseTestCase | from hc.test import BaseTestCase | ||||
@ -32,6 +33,36 @@ class LoginTestCase(BaseTestCase): | |||||
body = mail.outbox[0].body | body = mail.outbox[0].body | ||||
self.assertTrue("/?next=/integrations/add_slack/" in body) | self.assertTrue("/?next=/integrations/add_slack/" in body) | ||||
@override_settings(SECRET_KEY="test-secret") | |||||
def test_it_rate_limits_emails(self): | |||||
# "d60d..." is sha1("[email protected]") | |||||
obj = TokenBucket(value="em-d60db3b2343e713a4de3e92d4eb417e4f05f06ab") | |||||
obj.tokens = 0 | |||||
obj.save() | |||||
form = {"identity": "[email protected]"} | |||||
r = self.client.post("/accounts/login/", form) | |||||
self.assertContains(r, "Too Many Requests") | |||||
# No email should have been sent | |||||
self.assertEqual(len(mail.outbox), 0) | |||||
@override_settings(SECRET_KEY="test-secret") | |||||
def test_it_rate_limits_ips(self): | |||||
# 4b84.... is sha1("127.0.0.1test-secret") | |||||
obj = TokenBucket(value="ip-4b84b15bff6ee5796152495a230e45e3d7e947d9") | |||||
obj.tokens = 0 | |||||
obj.save() | |||||
form = {"identity": "[email protected]"} | |||||
r = self.client.post("/accounts/login/", form) | |||||
self.assertContains(r, "Too Many Requests") | |||||
# No email should have been sent | |||||
self.assertEqual(len(mail.outbox), 0) | |||||
def test_it_pops_bad_link_from_session(self): | def test_it_pops_bad_link_from_session(self): | ||||
self.client.session["bad_link"] = True | self.client.session["bad_link"] = True | ||||
self.client.get("/accounts/login/") | self.client.get("/accounts/login/") | ||||
@ -0,0 +1,22 @@ | |||||
# Generated by Django 2.2 on 2019-04-25 12:46 | |||||
from django.db import migrations, models | |||||
class Migration(migrations.Migration): | |||||
dependencies = [ | |||||
('api', '0059_auto_20190314_1744'), | |||||
] | |||||
operations = [ | |||||
migrations.CreateModel( | |||||
name='TokenBucket', | |||||
fields=[ | |||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
('value', models.CharField(max_length=80, unique=True)), | |||||
('tokens', models.FloatField(default=1.0)), | |||||
('updated', models.DateTimeField(auto_now_add=True)), | |||||
], | |||||
), | |||||
] |
@ -0,0 +1,47 @@ | |||||
from datetime import timedelta as td | |||||
from django.test.utils import override_settings | |||||
from django.utils.timezone import now | |||||
from hc.api.models import TokenBucket | |||||
from hc.test import BaseTestCase | |||||
# This is sha1("[email protected]" + "test-secred") | |||||
ALICE_HASH = "d60db3b2343e713a4de3e92d4eb417e4f05f06ab" | |||||
@override_settings(SECRET_KEY="test-secret") | |||||
class TokenBucketTestCase(BaseTestCase): | |||||
def test_it_works(self): | |||||
r = TokenBucket.authorize_login_email("[email protected]") | |||||
self.assertTrue(r) | |||||
obj = TokenBucket.objects.get() | |||||
self.assertEqual(obj.tokens, 0.95) | |||||
self.assertEqual(obj.value, "em-" + ALICE_HASH) | |||||
def test_it_handles_insufficient_tokens(self): | |||||
TokenBucket.objects.create(value="em-" + ALICE_HASH, tokens=0.04) | |||||
r = TokenBucket.authorize_login_email("[email protected]") | |||||
self.assertFalse(r) | |||||
def test_it_tops_up(self): | |||||
obj = TokenBucket(value="em-" + ALICE_HASH) | |||||
obj.tokens = 0 | |||||
obj.updated = now() - td(minutes=30) | |||||
obj.save() | |||||
r = TokenBucket.authorize_login_email("[email protected]") | |||||
self.assertTrue(r) | |||||
obj.refresh_from_db() | |||||
self.assertAlmostEqual(obj.tokens, 0.45, places=5) | |||||
def test_it_normalizes_email(self): | |||||
emails = ("[email protected]", "[email protected]") | |||||
for email in emails: | |||||
TokenBucket.authorize_login_email(email) | |||||
self.assertEqual(TokenBucket.objects.count(), 1) |
@ -0,0 +1,14 @@ | |||||
{% extends "base.html" %} | |||||
{% block content %} | |||||
<div class="row"> | |||||
<div class="col-sm-6 col-sm-offset-3"> | |||||
<div class="hc-dialog text-center"> | |||||
<h1>Too Many Requests</h1> | |||||
<div class="dialog-body"> | |||||
<p>Please try again later.</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{% endblock %} |