Coverage for src/lexigram/auth/exceptions.py: 86%

104 statements  

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

1"""Exception hierarchy for Lexigram Auth. 

2 

3All exceptions are organized by inheritance level: 

41. Re-imports from lexigram-contracts (base classes, aliased into this hierarchy) 

52. Auth root exception 

63. Authentication exceptions (credentials, tokens, account) 

74. Authorization exceptions 

85. Verification exceptions 

96. Registration/conflict exceptions 

10""" 

11 

12from __future__ import annotations 

13 

14from typing import Any 

15 

16from lexigram.contracts.auth.exceptions import AuthError as ContractsAuthError 

17from lexigram.contracts.auth.exceptions import VerificationError 

18from lexigram.contracts.exceptions import ( 

19 AuthenticationError as LexigramAuthenticationError, 

20) 

21from lexigram.contracts.exceptions import ( 

22 AuthorizationError as LexigramAuthorizationError, 

23) 

24from lexigram.contracts.exceptions import ( 

25 ConflictError, 

26) 

27from lexigram.contracts.exceptions import ( 

28 NotFoundError as LexigramNotFoundError, 

29) 

30 

31 

32class AuthError(ContractsAuthError): 

33 """Base exception for all auth errors.""" 

34 

35 _code = "LEX_ERR_AUTH_004" 

36 

37 

38class AuthenticationError(LexigramAuthenticationError, AuthError): 

39 """Raised when authentication fails.""" 

40 

41 _code = "LEX_ERR_AUTH_005" 

42 

43 

44class AuthorizationError(LexigramAuthorizationError, AuthError): 

45 """Raised when user lacks required permissions.""" 

46 

47 _code = "LEX_ERR_AUTH_006" 

48 

49 

50class InvalidCredentialsError(AuthenticationError): 

51 """Raised when credentials are invalid.""" 

52 

53 _code = "LEX_ERR_AUTH_007" 

54 

55 def __init__(self, message: str = "Invalid credentials", **kwargs: Any): 

56 super().__init__(message, **kwargs) 

57 

58 

59class AccountLockedError(AuthenticationError): 

60 """Raised when an account is locked due to too many failed login attempts. 

61 

62 Accounts are locked automatically after ``LockoutConfig.max_failed_attempts`` 

63 consecutive failures within ``LockoutConfig.lockout_duration_seconds``. 

64 The lock is lifted automatically once the observation window has passed. 

65 """ 

66 

67 _code = "LEX_ERR_AUTH_008" 

68 

69 def __init__(self, email: str = "", **kwargs: Any): 

70 msg = ( 

71 f"Account locked due to too many failed login attempts: {email}" 

72 if email 

73 else "Account locked due to too many failed login attempts" 

74 ) 

75 super().__init__(msg, **kwargs) 

76 

77 

78class UserNotFoundError(LexigramNotFoundError, AuthError): 

79 """Raised when user is not found.""" 

80 

81 _code = "LEX_ERR_AUTH_009" 

82 

83 def __init__(self, identifier: str, **kwargs: Any): 

84 super().__init__( 

85 f"User not found: {identifier}", 

86 **kwargs, 

87 ) 

88 

89 

90class TokenError(InvalidCredentialsError): 

91 """Base exception for token-related errors.""" 

92 

93 _code = "LEX_ERR_AUTH_010" 

94 

95 

96class InvalidTokenError(TokenError): 

97 """Raised when a token is malformed or invalid.""" 

98 

99 _code = "LEX_ERR_AUTH_011" 

100 

101 

102class TokenExpiredError(TokenError): 

103 """Raised when a token has expired.""" 

104 

105 _code = "LEX_ERR_AUTH_012" 

106 

107 def __init__( 

108 self, 

109 message: str = "Token has expired", 

110 expiration_time: str | None = None, 

111 **kwargs: Any, 

112 ) -> None: 

113 details = kwargs.get("details", {}) 

114 if expiration_time: 

115 details["expiration_time"] = expiration_time 

116 kwargs["details"] = details 

117 super().__init__(message, **kwargs) 

118 

119 

120class TokenBlacklistedError(TokenError): 

121 """Token has been explicitly revoked.""" 

122 

123 _code = "LEX_ERR_AUTH_013" 

124 

125 def __init__( 

126 self, 

127 message: str = "Token has been revoked", 

128 **kwargs: Any, 

129 ) -> None: 

130 super().__init__(message, **kwargs) 

131 

132 

133class TokenInvalidError(TokenError): 

134 """Token is structurally invalid or has wrong type.""" 

135 

136 _code = "LEX_ERR_AUTH_014" 

137 

138 def __init__( 

139 self, 

140 message: str = "Token is invalid", 

141 reason: str | None = None, 

142 **kwargs: Any, 

143 ) -> None: 

144 details = kwargs.get("details", {}) 

145 if reason: 

146 details["reason"] = reason 

147 kwargs["details"] = details 

148 super().__init__(message, **kwargs) 

149 

150 

151class TokenAudienceError(TokenError): 

152 """Token audience claim does not match expected.""" 

153 

154 _code = "LEX_ERR_AUTH_015" 

155 

156 def __init__( 

157 self, 

158 message: str = "Token audience mismatch", 

159 expected: str | None = None, 

160 actual: str | None = None, 

161 **kwargs: Any, 

162 ) -> None: 

163 details = kwargs.get("details", {}) 

164 if expected: 

165 details["expected"] = expected 

166 if actual: 

167 details["actual"] = actual 

168 kwargs["details"] = details 

169 super().__init__(message, **kwargs) 

170 

171 

172class TokenNotFoundError(TokenError): 

173 """Token record does not exist.""" 

174 

175 _code = "LEX_ERR_AUTH_016" 

176 

177 def __init__( 

178 self, 

179 message: str = "Token not found", 

180 token_id: str | None = None, 

181 **kwargs: Any, 

182 ) -> None: 

183 details = kwargs.get("details", {}) 

184 if token_id: 

185 details["token_id"] = token_id 

186 kwargs["details"] = details 

187 super().__init__(message, **kwargs) 

188 

189 

190class InvalidAudienceError(TokenError): 

191 """Raised when a token audience is invalid.""" 

192 

193 _code = "LEX_ERR_AUTH_017" 

194 

195 

196class InvalidScopeError(TokenError): 

197 """Raised when a token lacks required scope.""" 

198 

199 _code = "LEX_ERR_AUTH_018" 

200 

201 

202class BlacklistedTokenError(TokenError): 

203 """Raised when a token has been blacklisted.""" 

204 

205 _code = "LEX_ERR_AUTH_019" 

206 

207 

208class TokenExpiredVerificationError(VerificationError): 

209 """Account verification has expired.""" 

210 

211 _code = "LEX_ERR_AUTH_020" 

212 

213 def __init__( 

214 self, 

215 message: str = "Verification has expired", 

216 user_id: str | None = None, 

217 **kwargs: Any, 

218 ) -> None: 

219 details = kwargs.get("details", {}) 

220 if user_id: 

221 details["user_id"] = user_id 

222 kwargs["details"] = details 

223 super().__init__(message, **kwargs) 

224 

225 

226class AlreadyVerifiedError(VerificationError): 

227 """Account is already verified.""" 

228 

229 _code = "LEX_ERR_AUTH_021" 

230 

231 def __init__( 

232 self, 

233 message: str = "Account is already verified", 

234 user_id: str | None = None, 

235 **kwargs: Any, 

236 ) -> None: 

237 details = kwargs.get("details", {}) 

238 if user_id: 

239 details["user_id"] = user_id 

240 kwargs["details"] = details 

241 super().__init__(message, **kwargs) 

242 

243 

244class EmailExistsError(AuthError, ConflictError): 

245 """Raised when email is already taken.""" 

246 

247 _code = "LEX_ERR_AUTH_022" 

248 

249 

250class UsernameExistsError(AuthError, ConflictError): 

251 """Raised when username is already taken.""" 

252 

253 _code = "LEX_ERR_AUTH_023" 

254 

255 

256class PasswordPolicyError(AuthError): 

257 """Raised when password doesn't meet requirements.""" 

258 

259 _code = "LEX_ERR_AUTH_024" 

260 

261 

262class OAuth2Error(AuthError): 

263 """Base exception for OAuth2 errors.""" 

264 

265 _code = "LEX_ERR_AUTH_025" 

266 

267 

268class SessionNotFoundError(LexigramNotFoundError, AuthError): 

269 """Raised when a session cannot be found in the store.""" 

270 

271 _code = "LEX_ERR_AUTH_026" 

272 

273 def __init__(self, session_id: str, **kwargs: Any): 

274 super().__init__( 

275 f"Session not found: {session_id}", 

276 **kwargs, 

277 ) 

278 

279 

280__all__ = [ 

281 "AccountLockedError", 

282 "AlreadyVerifiedError", 

283 "AuthError", 

284 "AuthenticationError", 

285 "AuthorizationError", 

286 "BlacklistedTokenError", 

287 "EmailExistsError", 

288 "InvalidAudienceError", 

289 "InvalidCredentialsError", 

290 "InvalidScopeError", 

291 "InvalidTokenError", 

292 "OAuth2Error", 

293 "PasswordPolicyError", 

294 "SessionNotFoundError", 

295 "TokenAudienceError", 

296 "TokenBlacklistedError", 

297 "TokenError", 

298 "TokenExpiredError", 

299 "TokenExpiredVerificationError", 

300 "TokenInvalidError", 

301 "TokenNotFoundError", 

302 "UserNotFoundError", 

303 "UsernameExistsError", 

304 "VerificationError", 

305]