mmap-worker / tests /test_auth_lockout.py
jugalgajjar's picture
phase 3
bfa4570
Raw
History Blame Contribute Delete
2.2 kB
"""Lockout module — exercises the failure-silent behaviour without Redis.
We only test the public-facing shape (silent-fail returns False/0) because
the real counter logic needs a live Redis; that's covered in integration.
"""
from unittest.mock import AsyncMock, patch
import pytest
from redis.exceptions import RedisError
from app.auth import lockout
class TestLockoutFailsSilent:
@pytest.mark.asyncio
async def test_is_locked_out_returns_false_when_redis_unreachable(self):
async def boom(*_, **__):
raise RedisError("connection refused")
with patch.object(lockout, "_client") as mock_ctx:
mock_ctx.return_value.__aenter__ = AsyncMock(side_effect=boom)
mock_ctx.return_value.__aexit__ = AsyncMock(return_value=False)
locked, ttl = await lockout.is_locked_out("alice@example.com")
assert locked is False
assert ttl == 0
@pytest.mark.asyncio
async def test_record_failed_login_returns_zero_when_redis_unreachable(self):
async def boom(*_, **__):
raise RedisError("connection refused")
with patch.object(lockout, "_client") as mock_ctx:
mock_ctx.return_value.__aenter__ = AsyncMock(side_effect=boom)
mock_ctx.return_value.__aexit__ = AsyncMock(return_value=False)
count = await lockout.record_failed_login("alice@example.com")
assert count == 0
@pytest.mark.asyncio
async def test_clear_failed_logins_returns_none_when_redis_unreachable(self):
async def boom(*_, **__):
raise RedisError("connection refused")
with patch.object(lockout, "_client") as mock_ctx:
mock_ctx.return_value.__aenter__ = AsyncMock(side_effect=boom)
mock_ctx.return_value.__aexit__ = AsyncMock(return_value=False)
# Just ensure it doesn't raise.
await lockout.clear_failed_logins("alice@example.com")
class TestKeyNamespacing:
def test_email_is_lowercased(self):
assert lockout._key("Alice@Example.com") == "auth:lockout:alice@example.com"
def test_key_prefix(self):
assert lockout._key("a@b.com").startswith("auth:lockout:")