mirror of
https://github.com/jazzband/django-axes.git
synced 2026-05-09 08:04:44 +00:00
The old architecture used exceptions in the signal handler which prevented transactions from running smoothly and signal handlers from running after Axes handlers. The new architecture changes the request approach to request flagging and moves the exception handling into the middleware call method. This allows users to more flexibly run their own signal handlers and optionally use the Axes middleware if they want to do so. Fixes #440 Fixes #442
177 lines
5.2 KiB
Python
177 lines
5.2 KiB
Python
from random import choice
|
|
from string import ascii_letters, digits
|
|
from time import sleep
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.http import HttpRequest
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from django.utils.timezone import now
|
|
|
|
from axes.utils import reset
|
|
from axes.conf import settings
|
|
from axes.helpers import (
|
|
get_cache,
|
|
get_client_http_accept,
|
|
get_client_ip_address,
|
|
get_client_path_info,
|
|
get_client_user_agent,
|
|
get_cool_off,
|
|
get_credentials,
|
|
)
|
|
from axes.models import AccessAttempt
|
|
|
|
|
|
class AxesTestCase(TestCase):
|
|
"""
|
|
Test case using custom settings for testing.
|
|
"""
|
|
|
|
VALID_USERNAME = 'axes-valid-username'
|
|
VALID_PASSWORD = 'axes-valid-password'
|
|
VALID_EMAIL = 'axes-valid-email@example.com'
|
|
VALID_USER_AGENT = 'axes-user-agent'
|
|
VALID_IP_ADDRESS = '127.0.0.1'
|
|
|
|
INVALID_USERNAME = 'axes-invalid-username'
|
|
INVALID_PASSWORD = 'axes-invalid-password'
|
|
INVALID_EMAIL = 'axes-invalid-email@example.com'
|
|
|
|
LOCKED_MESSAGE = 'Account locked: too many login attempts.'
|
|
LOGOUT_MESSAGE = 'Logged out'
|
|
LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
|
|
|
|
STATUS_SUCCESS = 200
|
|
ALLOWED = 302
|
|
BLOCKED = 403
|
|
|
|
def setUp(self):
|
|
"""
|
|
Create a valid user for login.
|
|
"""
|
|
|
|
self.username = self.VALID_USERNAME
|
|
self.password = self.VALID_PASSWORD
|
|
self.email = self.VALID_EMAIL
|
|
|
|
self.ip_address = self.VALID_IP_ADDRESS
|
|
self.user_agent = self.VALID_USER_AGENT
|
|
self.path_info = reverse('admin:login')
|
|
|
|
self.user = get_user_model().objects.create_superuser(
|
|
username=self.username,
|
|
password=self.password,
|
|
email=self.email,
|
|
)
|
|
|
|
self.request = HttpRequest()
|
|
self.request.method = 'POST'
|
|
self.request.META['REMOTE_ADDR'] = self.ip_address
|
|
self.request.META['HTTP_USER_AGENT'] = self.user_agent
|
|
self.request.META['PATH_INFO'] = self.path_info
|
|
|
|
self.request.axes_attempt_time = now()
|
|
self.request.axes_ip_address = get_client_ip_address(self.request)
|
|
self.request.axes_user_agent = get_client_user_agent(self.request)
|
|
self.request.axes_path_info = get_client_path_info(self.request)
|
|
self.request.axes_http_accept = get_client_http_accept(self.request)
|
|
|
|
self.credentials = get_credentials(self.username)
|
|
|
|
def tearDown(self):
|
|
get_cache().clear()
|
|
|
|
def get_kwargs_with_defaults(self, **kwargs):
|
|
defaults = {
|
|
'user_agent': self.user_agent,
|
|
'ip_address': self.ip_address,
|
|
'username': self.username,
|
|
'failures_since_start': 1,
|
|
}
|
|
|
|
defaults.update(kwargs)
|
|
return defaults
|
|
|
|
def create_attempt(self, **kwargs):
|
|
return AccessAttempt.objects.create(**self.get_kwargs_with_defaults(**kwargs))
|
|
|
|
def reset(self, ip=None, username=None):
|
|
return reset(ip, username)
|
|
|
|
def login(self, is_valid_username=False, is_valid_password=False, **kwargs):
|
|
"""
|
|
Login a user.
|
|
|
|
A valid credential is used when is_valid_username is True,
|
|
otherwise it will use a random string to make a failed login.
|
|
"""
|
|
|
|
if is_valid_username:
|
|
username = self.VALID_USERNAME
|
|
else:
|
|
username = ''.join(
|
|
choice(ascii_letters + digits)
|
|
for _ in range(10)
|
|
)
|
|
|
|
if is_valid_password:
|
|
password = self.VALID_PASSWORD
|
|
else:
|
|
password = self.INVALID_PASSWORD
|
|
|
|
post_data = {
|
|
'username': username,
|
|
'password': password,
|
|
**kwargs
|
|
}
|
|
|
|
return self.client.post(
|
|
reverse('admin:login'),
|
|
post_data,
|
|
REMOTE_ADDR=self.ip_address,
|
|
HTTP_USER_AGENT=self.user_agent,
|
|
)
|
|
|
|
def logout(self):
|
|
return self.client.post(
|
|
reverse('admin:logout'),
|
|
REMOTE_ADDR=self.ip_address,
|
|
HTTP_USER_AGENT=self.user_agent,
|
|
)
|
|
|
|
def check_login(self):
|
|
response = self.login(is_valid_username=True, is_valid_password=True)
|
|
self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=self.ALLOWED, html=True)
|
|
|
|
def almost_lockout(self):
|
|
for _ in range(1, settings.AXES_FAILURE_LIMIT):
|
|
response = self.login()
|
|
self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
|
|
|
|
def lockout(self):
|
|
self.almost_lockout()
|
|
return self.login()
|
|
|
|
def check_lockout(self):
|
|
response = self.lockout()
|
|
self.assertContains(response, self.LOCKED_MESSAGE, status_code=self.BLOCKED)
|
|
|
|
def cool_off(self):
|
|
sleep(get_cool_off().total_seconds())
|
|
|
|
def check_logout(self):
|
|
response = self.logout()
|
|
self.assertContains(response, self.LOGOUT_MESSAGE, status_code=self.STATUS_SUCCESS)
|
|
|
|
def check_handler(self):
|
|
"""
|
|
Check a handler and its basic functionality with lockouts, cool offs, login, and logout.
|
|
|
|
This is a check that is intended to successfully run for each and every new handler.
|
|
"""
|
|
|
|
self.check_lockout()
|
|
self.cool_off()
|
|
self.check_login()
|
|
self.check_logout()
|
|
|