Coverage for src/lexigram/auth/admin/handlers/failed_logins.py: 100%

19 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 00:58 +0800

1"""Failed logins widget handler.""" 

2 

3from __future__ import annotations 

4 

5from lexigram.auth.services.activity_tracker import AuthActivityTracker 

6from lexigram.contracts.admin import Stat, StatContent, Tone, WidgetParams 

7from lexigram.contracts.admin.errors import AdminError 

8from lexigram.result import Ok, Result 

9 

10 

11class FailedLoginsWidgetHandler: 

12 """Handler for the failed logins widget. 

13 

14 Args: 

15 tracker: injected AuthActivityTracker. 

16 """ 

17 

18 def __init__(self, tracker: AuthActivityTracker) -> None: 

19 """Initialize handler with the activity tracker. 

20 

21 Args: 

22 tracker: AuthActivityTracker for login failure tracking. 

23 """ 

24 self._tracker = tracker 

25 

26 async def get_data(self, params: WidgetParams) -> Result[StatContent, AdminError]: 

27 """Fetch failed login statistics. 

28 

29 Mirrors the widget template: a single ``is_elevated`` boundary gates 

30 the danger tone (the template's ``{% if is_elevated %}`` badge), with 

31 neutral styling otherwise. No multi-tier threshold ladder exists. 

32 

33 Infrastructure failures propagate as exceptions. 

34 

35 Args: 

36 params: Widget parameters (includes time_window_minutes). 

37 

38 Returns: 

39 Result containing StatContent or AdminError. 

40 """ 

41 window = getattr(params, "time_window_minutes", 30) 

42 count, unique_ips = self._tracker.failed_login_summary( 

43 window_minutes=int(window or 30) 

44 ) 

45 return Ok( 

46 self._build_content( 

47 count=count, unique_ips=unique_ips, is_elevated=count > 0 

48 ) 

49 ) 

50 

51 def _build_content( 

52 self, count: int, unique_ips: int, is_elevated: bool 

53 ) -> StatContent: 

54 """Build the StatContent mirroring the widget template. 

55 

56 Args: 

57 count: Number of failed login attempts over the window. 

58 unique_ips: Number of unique source IPs. 

59 is_elevated: Whether the count is above the elevated threshold. 

60 

61 Returns: 

62 StatContent with a danger tone when elevated and neutral styling 

63 otherwise, plus a conditional unique-IP stat. 

64 """ 

65 tone = Tone.DANGER if is_elevated else Tone.DEFAULT 

66 stats: list[Stat] = [ 

67 Stat(label="Failed Logins (1 hour)", value=str(count), tone=tone), 

68 ] 

69 if unique_ips > 0: 

70 stats.append(Stat(label="Unique IPs", value=str(unique_ips))) 

71 return StatContent(stats=tuple(stats)) 

72 

73 

74__all__ = ["FailedLoginsWidgetHandler"]