Coverage for src/lexigram/auth/types.py: 99%

67 statements  

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

1"""Type definitions for Lexigram Auth.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from enum import StrEnum 

7from typing import TYPE_CHECKING, Any 

8 

9from lexigram.auth.models import AuthToken 

10from lexigram.auth.models.user import User 

11from lexigram.contracts.auth.roles import RoleDefinition 

12from lexigram.domain import DomainModel 

13from lexigram.validation import Field 

14 

15if TYPE_CHECKING: 

16 from lexigram.contracts.core import HealthStatus 

17 

18 

19class AuthStatus(StrEnum): 

20 """Authentication status values.""" 

21 

22 AUTHENTICATED = "authenticated" 

23 UNAUTHENTICATED = "unauthenticated" 

24 TOKEN_EXPIRED = "token_expired" # noqa: S105 # status name, not a credential 

25 TOKEN_INVALID = "token_invalid" # noqa: S105 # status name, not a credential 

26 USER_INACTIVE = "user_inactive" 

27 USER_NOT_VERIFIED = "user_not_verified" 

28 

29 

30class TokenType(StrEnum): 

31 """Token type values.""" 

32 

33 BEARER = "Bearer" 

34 BASIC = "Basic" 

35 API_KEY = "ApiKey" 

36 

37 

38class UserStatus(StrEnum): 

39 """User account status values.""" 

40 

41 ACTIVE = "active" 

42 INACTIVE = "inactive" 

43 SUSPENDED = "suspended" 

44 PENDING_VERIFICATION = "pending_verification" 

45 DELETED = "deleted" 

46 

47 

48@dataclass(init=False) 

49class GuardContext(DomainModel): 

50 """Context passed to authorization guards.""" 

51 

52 user: User | None = Field(default=None, description="Authenticated user") 

53 request: Any | None = Field(default=None, description="Raw request object") 

54 request_context_user_id: str | None = Field( 

55 default=None, 

56 description="Authenticated user ID from the normalized request context", 

57 ) 

58 route: str | None = Field(default=None, description="Target route name") 

59 method: str | None = Field(default=None, description="HTTP method") 

60 path: str | None = Field(default=None, description="Request path") 

61 headers: dict[str, str] = Field(default_factory=dict, description="Request headers") 

62 params: dict[str, Any] = Field(default_factory=dict, description="Route parameters") 

63 

64 

65@dataclass(init=False) 

66class AuthResult(DomainModel): 

67 """Result of an authentication attempt.""" 

68 

69 success: bool = Field(description="Whether authentication succeeded") 

70 status: AuthStatus = Field(description="Status code of the attempt") 

71 user: User | None = Field(default=None, description="Authenticated user instance") 

72 token: AuthToken | None = Field(default=None, description="Generated auth token") 

73 message: str | None = Field( 

74 default=None, description="Optional error/status message" 

75 ) 

76 

77 

78@dataclass(init=False) 

79class OAuth2UserInfo(DomainModel): 

80 """User information returned by an OAuth2 provider.""" 

81 

82 provider: str = Field(description="OAuth2 provider name") 

83 provider_user_id: str = Field(description="Provider-specific user ID") 

84 email: str | None = Field(default=None, description="User email") 

85 email_verified: bool = Field( 

86 default=False, 

87 description="Whether the IdP verified the email address", 

88 ) 

89 username: str | None = Field(default=None, description="Provider username") 

90 name: str | None = Field(default=None, description="Display name") 

91 avatar_url: str | None = Field(default=None, description="Profile image URL") 

92 raw_data: dict[str, Any] | None = Field( 

93 default=None, description="Original provider response" 

94 ) 

95 

96 def __post_init__(self) -> None: 

97 """Populate ``name`` from ``username`` when ``name`` is not supplied.""" 

98 if self.username and not self.name: 

99 self.name = self.username 

100 

101 

102@dataclass(init=False) 

103class AuthHealthResult(DomainModel): 

104 """Health check result for auth components.""" 

105 

106 status: HealthStatus = Field(description="Overall component health status") 

107 message: str = Field(description="Health status message") 

108 users_count: int = Field(default=0, description="Number of users in store") 

109 components: dict[str, dict[str, Any]] = Field( 

110 default_factory=dict, description="Sub-component health details" 

111 ) 

112 

113 

114@dataclass(frozen=True) 

115class TokenPair: 

116 """A minimal access + refresh token pair. 

117 

118 Returned by rotation operations (e.g. 

119 :meth:`~lexigram.auth.authn.jwt.JWTTokenManager.refresh_with_rotation`) 

120 as a lightweight, immutable carrier for the two new token strings. 

121 

122 Attributes: 

123 access: The newly issued access token JWT string. 

124 refresh: The newly issued refresh token JWT string. 

125 """ 

126 

127 access: str 

128 refresh: str 

129 

130 

131__all__ = [ 

132 "AuthHealthResult", 

133 "AuthResult", 

134 "AuthStatus", 

135 "AuthToken", 

136 "GuardContext", 

137 "OAuth2UserInfo", 

138 "RoleDefinition", 

139 "TokenPair", 

140 "TokenType", 

141 "User", 

142 "UserStatus", 

143]