Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

145 lines
5.0 KiB
Python

"""Tests for per-user account lockout."""
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
from local_deep_research.security.account_lockout import AccountLockoutManager
class TestAccountLockout:
"""Unit tests for AccountLockoutManager."""
def _make_manager(self, threshold=3, lockout_minutes=15):
return AccountLockoutManager(
threshold=threshold, lockout_minutes=lockout_minutes
)
def test_not_locked_initially(self):
mgr = self._make_manager()
assert mgr.is_locked("alice") is False
def test_not_locked_below_threshold(self):
mgr = self._make_manager(threshold=3)
mgr.record_failure("alice")
mgr.record_failure("alice")
assert mgr.is_locked("alice") is False
def test_locked_at_threshold(self):
mgr = self._make_manager(threshold=3)
for _ in range(3):
mgr.record_failure("alice")
assert mgr.is_locked("alice") is True
def test_success_resets_lockout(self):
mgr = self._make_manager(threshold=3)
for _ in range(3):
mgr.record_failure("alice")
assert mgr.is_locked("alice") is True
mgr.record_success("alice")
assert mgr.is_locked("alice") is False
def test_counter_resets_after_success(self):
mgr = self._make_manager(threshold=10)
for _ in range(9):
mgr.record_failure("alice")
mgr.record_success("alice")
# After reset, 9 more failures should not lock (threshold is 10)
for _ in range(9):
mgr.record_failure("alice")
assert mgr.is_locked("alice") is False
def test_independent_users(self):
mgr = self._make_manager(threshold=3)
for _ in range(3):
mgr.record_failure("alice")
assert mgr.is_locked("alice") is True
assert mgr.is_locked("bob") is False
def test_success_on_nonexistent_user_is_noop(self):
mgr = self._make_manager()
# Should not raise
mgr.record_success("nonexistent")
assert mgr.is_locked("nonexistent") is False
def test_lockout_expires_after_duration(self):
mgr = self._make_manager(threshold=3, lockout_minutes=15)
now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
with patch(
"local_deep_research.security.account_lockout.datetime"
) as mock_dt:
mock_dt.now.return_value = now
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
for _ in range(3):
mgr.record_failure("alice")
# Still locked 14 minutes later
with patch(
"local_deep_research.security.account_lockout.datetime"
) as mock_dt:
mock_dt.now.return_value = now + timedelta(minutes=14)
assert mgr.is_locked("alice") is True
# Unlocked at exactly 15 minutes
with patch(
"local_deep_research.security.account_lockout.datetime"
) as mock_dt:
mock_dt.now.return_value = now + timedelta(minutes=15)
assert mgr.is_locked("alice") is False
def test_eviction_removes_unlocked_entries(self):
"""Eviction should remove unlocked/expired entries, not blanket clear."""
mgr = self._make_manager(threshold=5)
mgr._MAX_STATE_ENTRIES = 100
now = datetime.now(timezone.utc)
# Add unlocked entries (count < threshold, no locked_until)
for i in range(90):
mgr._state[f"unlocked_{i}"] = {"count": 1, "locked_until": None}
# Add expired locked entries
for i in range(10):
mgr._state[f"expired_{i}"] = {
"count": 5,
"locked_until": now - timedelta(minutes=1),
}
# Add actively locked entries
for i in range(5):
mgr._state[f"locked_{i}"] = {
"count": 5,
"locked_until": now + timedelta(minutes=10),
}
assert len(mgr._state) == 105
# Trigger eviction via record_failure
mgr.record_failure("attacker")
# Actively locked entries should survive
for i in range(5):
assert f"locked_{i}" in mgr._state
# New entry should exist
assert "attacker" in mgr._state
# Unlocked/expired entries should be gone
assert not any(k.startswith("unlocked_") for k in mgr._state)
assert not any(k.startswith("expired_") for k in mgr._state)
def test_blanket_clear_as_last_resort(self):
"""If eviction can't reduce below limit, blanket clear."""
mgr = self._make_manager(threshold=3)
mgr._MAX_STATE_ENTRIES = 10
now = datetime.now(timezone.utc)
# Fill with actively locked entries (can't be evicted)
for i in range(15):
mgr._state[f"locked_{i}"] = {
"count": 5,
"locked_until": now + timedelta(hours=1),
}
mgr.record_failure("attacker")
# Should have blanket-cleared then added new entry
assert len(mgr._state) == 1
assert "attacker" in mgr._state