| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 |
- """登录失败锁定策略纯逻辑测试(不连接数据库)。"""
- from __future__ import annotations
- from datetime import datetime, timedelta, timezone
- from dms.models import User
- from dms.services.login_lockout import (
- LOGIN_LOCK_DURATION,
- LOGIN_MAX_FAILURES,
- apply_failed_attempt,
- clear_failed_attempts,
- is_locked,
- )
- # 与 login_lockout._utc_now 同口径取当前 UTC;在每个用例内调用(而非模块级常量),
- # 避免长批次下收集与执行的时间差让 ±1 秒边界断言失效。
- def _now() -> datetime:
- return datetime.now(timezone.utc).replace(tzinfo=None)
- def _user(*, failed_login_count=0, locked_until=None) -> User:
- user = User(username="u1", password_hash="x", real_name="用户一")
- user.failed_login_count = failed_login_count
- user.locked_until = locked_until
- return user
- def test_failures_below_threshold_do_not_lock():
- user = _user()
- for _ in range(LOGIN_MAX_FAILURES - 1):
- apply_failed_attempt(user, now=_now())
- assert user.failed_login_count == LOGIN_MAX_FAILURES - 1
- assert user.locked_until is None
- assert not is_locked(user)
- def test_fifth_consecutive_failure_locks_and_resets_count():
- now = _now()
- user = _user(failed_login_count=LOGIN_MAX_FAILURES - 1)
- apply_failed_attempt(user, now=now)
- assert user.locked_until == now + LOGIN_LOCK_DURATION
- assert user.failed_login_count == 0
- assert is_locked(user)
- def test_lock_window_is_fifteen_minutes():
- assert LOGIN_MAX_FAILURES == 5
- assert LOGIN_LOCK_DURATION == timedelta(minutes=15)
- def test_is_locked_expired_or_absent_returns_false():
- now = _now()
- assert not is_locked(_user(locked_until=None))
- assert not is_locked(_user(locked_until=now - timedelta(seconds=1)))
- assert is_locked(_user(locked_until=now + timedelta(seconds=1)))
- def test_clear_failed_attempts_zeroes_state():
- user = _user(failed_login_count=3, locked_until=_now() + timedelta(minutes=5))
- clear_failed_attempts(user)
- assert user.failed_login_count == 0
- assert user.locked_until is None
- def test_count_resets_after_lock_expiry_window():
- """锁定到期后视为未锁定,重新计数从零开始。"""
- user = _user(locked_until=_now() - timedelta(minutes=1))
- assert not is_locked(user)
- apply_failed_attempt(user, now=_now())
- assert user.failed_login_count == 1
|