first pass at some unit tests, still not working

This commit is contained in:
Ken Cochrane 2014-12-30 20:17:15 -05:00
parent 03488469f5
commit 6786499ba1
7 changed files with 271 additions and 10 deletions

View file

@ -131,3 +131,13 @@ ELSE
EXEC
END
```
Running Tests
=============
Tests can be run, after you clone the repository and having django installed,
like:
```
$ PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test defender --settings=defender.test_settings
```

View file

@ -1,7 +1,7 @@
import logging
import socket
import redis
from redis import StrictRedis
from django.conf import settings
from django.http import HttpResponse
from django.http import HttpResponseRedirect
@ -50,7 +50,7 @@ VERBOSE = getattr(settings, 'DEFENDER_VERBOSE', True)
ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. "
"Note that both fields are case-sensitive.")
redis_server = redis.StrictRedis(
redis_server = StrictRedis(
host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD)
log = logging.getLogger(__name__)
@ -198,12 +198,16 @@ def record_failed_attempt(ip, username):
return True
def reset_failed_attempts(ip, username):
""" reset the failed attempts for these ip's and usernames """
redis_server.delete(get_ip_attempt_cache_key(ip))
redis_server.delete(get_username_attempt_cache_key(username))
redis_server.delete(get_username_blocked_cache_key(username))
redis_server.delete(get_ip_blocked_cache_key(ip))
def reset_failed_attempts(ip=None, username=None):
""" reset the failed attempts for these ip's and usernames
TODO: run all commands in one redis transaction
"""
if ip:
redis_server.delete(get_ip_attempt_cache_key(ip))
redis_server.delete(get_ip_blocked_cache_key(ip))
if username:
redis_server.delete(get_username_attempt_cache_key(username))
redis_server.delete(get_username_blocked_cache_key(username))
def lockout_response(request):

35
defender/middleware.py Normal file
View file

@ -0,0 +1,35 @@
from django.conf import settings
from django.contrib.auth import views as auth_views
from defender.decorators import watch_login
class FailedLoginMiddleware(object):
def __init__(self, *args, **kwargs):
super(FailedLoginMiddleware, self).__init__(*args, **kwargs)
# watch the auth login
auth_views.login = watch_login(auth_views.login)
class ViewDecoratorMiddleware(object):
"""
When the django_axes middleware is installed, by default it watches the
django.auth.views.login.
This middleware allows adding protection to other views without the need
to change any urls or dectorate them manually.
Add this middleware to your MIDDLEWARE settings after
`defender.middleware.FailedLoginMiddleware` and before the django
flatpages middleware.
"""
watched_logins = getattr(
settings, 'DEFENDER_PROTECTED_LOGINS', (
'/accounts/login/',
)
)
def process_view(self, request, view_func, view_args, view_kwargs):
if request.path in self.watched_logins:
return watch_login(view_func)(request, *view_args, **view_kwargs)
return None

45
defender/test_settings.py Normal file
View file

@ -0,0 +1,45 @@
import django
if django.VERSION[:2] >= (1, 3):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
else:
DATABASE_ENGINE = 'sqlite3'
SITE_ID = 1
REDIS_HOST = 'localhost'
REDIS_PORT = '1234'
REDIS_PASSWORD = 'mypassword'
REDIS_DB = 1
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'defender.middleware.FailedLoginMiddleware'
)
ROOT_URLCONF = 'defender.test_urls'
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'defender',
]
SECRET_KEY = 'too-secret-for-test'
LOGIN_REDIRECT_URL = '/admin'
AXES_LOGIN_FAILURE_LIMIT = 10
from datetime import timedelta
AXES_COOLOFF_TIME = timedelta(seconds=2)

7
defender/test_urls.py Normal file
View file

@ -0,0 +1,7 @@
from django.conf.urls import patterns, include
from django.contrib import admin
urlpatterns = patterns(
'',
(r'^admin/', include(admin.site.urls)),
)

160
defender/tests.py Normal file
View file

@ -0,0 +1,160 @@
import random
import string
import time
from mock import patch
import mockredis
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.urlresolvers import NoReverseMatch
from django.core.urlresolvers import reverse
from defender.decorators import (
COOLOFF_TIME, FAILURE_LIMIT, reset_failed_attempts)
# Django >= 1.7 compatibility
try:
ADMIN_LOGIN_URL = reverse('admin:login')
LOGIN_FORM_KEY = '<form action="/admin/login/" method="post" \
id="login-form">'
except NoReverseMatch:
ADMIN_LOGIN_URL = reverse('admin:index')
LOGIN_FORM_KEY = 'this_is_the_login_form'
class AccessAttemptTest(TestCase):
"""Test case using custom settings for testing
"""
VALID_USERNAME = 'valid'
LOCKED_MESSAGE = 'Account locked: too many login attempts.'
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def the_test(self):
from redis import StrictRedis
print(StrictRedis)
def the_test2(self):
from redis import StrictRedis
dir(StrictRedis)
print(StrictRedis)
def _get_random_str(self):
""" Returns a random str """
chars = string.ascii_uppercase + string.digits
return ''.join(random.choice(chars) for x in range(20))
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def _login(self, is_valid=False, user_agent='test-browser'):
"""Login a user. A valid credential is used when is_valid is True,
otherwise it will use a random string to make a failed login.
"""
username = self.VALID_USERNAME if is_valid else self._get_random_str()
response = self.client.post(ADMIN_LOGIN_URL, {
'username': username,
'password': username,
'this_is_the_login_form': 1,
}, HTTP_USER_AGENT=user_agent)
return response
def setUp(self):
"""Create a valid user for login
"""
self.user = User.objects.create_superuser(
username=self.VALID_USERNAME,
email='test@example.com',
password=self.VALID_USERNAME,
)
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_failure_limit_once(self):
"""Tests the login lock trying to login one more time
than failure limit
"""
for i in range(0, FAILURE_LIMIT):
response = self._login()
# Check if we are in the same login page
self.assertContains(response, LOGIN_FORM_KEY)
# So, we shouldn't have gotten a lock-out yet.
# But we should get one now
response = self._login()
self.assertContains(response, self.LOCKED_MESSAGE)
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_failure_limit_many(self):
"""Tests the login lock trying to login a lot of times more
than failure limit
"""
for i in range(0, FAILURE_LIMIT):
response = self._login()
# Check if we are in the same login page
self.assertContains(response, LOGIN_FORM_KEY)
# So, we shouldn't have gotten a lock-out yet.
# But we should get one now
for i in range(0, random.randrange(1, 10)):
# try to log in a bunch of times
response = self._login()
self.assertContains(response, self.LOCKED_MESSAGE)
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_valid_login(self):
"""Tests a valid login for a real username
"""
response = self._login(is_valid=True)
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_cooling_off(self):
"""Tests if the cooling time allows a user to login
"""
self.test_failure_limit_once()
# Wait for the cooling off period
time.sleep(COOLOFF_TIME.total_seconds())
# It should be possible to login again, make sure it is.
self.test_valid_login()
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_cooling_off_for_trusted_user(self):
"""Test the cooling time for a trusted user
"""
# Try the cooling off time
self.test_cooling_off()
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_long_user_agent_valid(self):
"""Tests if can handle a long user agent
"""
long_user_agent = 'ie6' * 1024
response = self._login(is_valid=True, user_agent=long_user_agent)
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_long_user_agent_not_valid(self):
"""Tests if can handle a long user agent with failure
"""
long_user_agent = 'ie6' * 1024
for i in range(0, FAILURE_LIMIT + 1):
response = self._login(user_agent=long_user_agent)
self.assertContains(response, self.LOCKED_MESSAGE)
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
def test_reset_ip(self):
"""Tests if can reset an ip address
"""
# Make a lockout
self.test_failure_limit_once()
# Reset the ip so we can try again
reset_failed_attempts(ip='127.0.0.1')
# Make a login attempt again
self.test_valid_login()

View file

@ -30,6 +30,6 @@ setup(name='django-defender',
author_email='kencochrane@gmail.com',
license='Apache 2',
packages=['defender'],
install_requires=['django==1.6.7', 'redis==2.10.3', 'hiredis==0.1.4', ],
install_requires=['django==1.6.8', 'redis==2.10.3', 'hiredis==0.1.4', ],
tests_require=['mock', 'mockredispy'],
)