Repository: jazzband/django-axes

Issue:
Not sending lockout signal
The library doesn't send the lockout signal. Tried both with decorator and middleware.
Debugging check_request, it does not pass through it when locking out the user, maybe the control

``` python
if is_already_locked(request):
```

in watch_login prevents the signal sending by returning directly?
Or am I missing something else?



Discussion:
I proposed a patch that triggers the correct actions, but somehow it does not pass the Travis CI build.
If someone has thoughts about it it would be welcome, since I'd like to use this library in a project, but the signal's spawn is mandatory for my needs.
https://github.com/zoten/django-axes/commit/f3f93712867c8d175d8494c91823b5a197a8649f

+1 for this issue - code never hits the signal send line because of the is_already_locked check above it, and the fact that
    failures > FAILURE_LIMIT 
is never True: failures gets capped at 5 with FAILURE_LIMIT = 5, and is_already_locked() prevents reaching this logic a 2nd time.  

it would be really nice to have access to this lockout signal to be able to customize site behavior at the moment of user lockout (e.g. notify admin or record suspicious activity).

@zoten Where is your PR? I don't see it.

@aclark4life I think it has been rejected because it seemed to break some tests, which I hadn't the time to fix.
You can still find the one-line-diff at
https://github.com/zoten/django-axes/commit/f3f93712867c8d175d8494c91823b5a197a8649f

Jep, this issue would also be important for me.


Resolving patch:
diff --git a/axes/decorators.py b/axes/decorators.py
--- a/axes/decorators.py
+++ b/axes/decorators.py
@@ -428,7 +428,7 @@ def check_request(request, login_unsuccessful):
     user_lockable = is_user_lockable(request)
     # no matter what, we want to lock them out if they're past the number of
     # attempts allowed, unless the user is set to notlockable
-    if failures > FAILURE_LIMIT and LOCK_OUT_AT_FAILURE and user_lockable:
+    if failures >= FAILURE_LIMIT and LOCK_OUT_AT_FAILURE and user_lockable:
         # We log them out in case they actually managed to enter the correct
         # password
         if hasattr(request, 'user') and request.user.is_authenticated():


Test patch:
diff --git a/axes/tests.py b/axes/tests.py
--- a/axes/tests.py
+++ b/axes/tests.py
@@ -10,6 +10,7 @@
 from axes.decorators import COOLOFF_TIME
 from axes.decorators import FAILURE_LIMIT
 from axes.models import AccessLog
+from axes.signals import user_locked_out
 from axes.utils import reset
 
 
@@ -58,7 +59,7 @@ 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):
+        for i in range(1, FAILURE_LIMIT):  # test until one try before the limit
             response = self._login()
             # Check if we are in the same login page
             self.assertContains(response, self.LOGIN_FORM_KEY)
@@ -72,7 +73,7 @@ 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):
+        for i in range(1, FAILURE_LIMIT):
             response = self._login()
             # Check if we are in the same login page
             self.assertContains(response, self.LOGIN_FORM_KEY)
@@ -158,3 +159,27 @@ def test_reset_all(self):
 
         # Make a login attempt again
         self.test_valid_login()
+
+    def test_send_lockout_signal(self):
+        """Test if the lockout signal is emitted
+        """
+        class Scope(object): pass  # this "hack" is needed so we don't have to use global variables or python3 features
+        scope = Scope()
+        scope.signal_received = 0
+
+        def signal_handler(request, username, ip_address, *args, **kwargs):
+            scope.signal_received += 1
+            self.assertIsNotNone(request)
+
+        # Connect signal handler
+        user_locked_out.connect(signal_handler)
+
+        # Make a lockout
+        self.test_failure_limit_once()
+        self.assertEquals(scope.signal_received, 1)
+
+        reset()
+
+        # Make another lockout
+        self.test_failure_limit_once()
+        self.assertEquals(scope.signal_received, 2)