fix(i18n): read every refusal from the backend's syntax tree, not by regex
The contract read Python with regexes, and each guard added for a missed shape had shapes of its own
it missed: a reason after another key, a status passed by name, a second `e.reason` handler with the
same text, a pin naming a type nothing checked.
backend/tests/refusal_sites.py (stdlib `ast`, run by the contract on the host's Python) reads every
`{"code": ...}` detail and follows each reason to its values: literals, both arms of a conditional,
module constants, the `Literal` on the parameter or attribute it travels in (on the class an `except`
catches), every call of a `str` helper with its defaults, a map's values with its keys held to the
index's type. What it cannot follow fails the contract, unless no screen shows that code; no pins.
The reason attributes are typed with their Literal, so a subclass or an assignment cannot widen
them; InvalidSetting takes a SettingReason, and the settings map is held to it (a reason the map
lacked was a 500). The per-family reverse checks are one check by prefix (the admin actions
included), and the leaf-key filter applies only to the suffixed sign-in family.
This commit is contained in:
@@ -78,7 +78,7 @@ class ClipError(Exception):
|
||||
message names (the cap itself, so the client never says a stale one)."""
|
||||
|
||||
def __init__(self, reason: ClipReason, **params: float):
|
||||
self.reason = reason
|
||||
self.reason: ClipReason = reason
|
||||
self.params = params
|
||||
super().__init__(reason)
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ class InvalidCookies(ValueError):
|
||||
|
||||
def __init__(self, reason: CookieReason):
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
self.reason: CookieReason = reason
|
||||
|
||||
|
||||
def validate_cookies(raw: str) -> str:
|
||||
|
||||
@@ -197,7 +197,7 @@ class FolderError(Exception):
|
||||
|
||||
def __init__(self, reason: FolderReason) -> None:
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
self.reason: FolderReason = reason
|
||||
|
||||
|
||||
def own_folder(db: Session, user_id: int, folder_id: int, *, hold: bool = True) -> FileFolder:
|
||||
|
||||
@@ -60,7 +60,7 @@ class QuotaExceeded(Exception):
|
||||
"""Raised by check_enqueue; `reason` names the cap it hit (an i18n key on the client)."""
|
||||
|
||||
def __init__(self, reason: QuotaReason, limit: int, current: int):
|
||||
self.reason = reason
|
||||
self.reason: QuotaReason = reason
|
||||
self.limit = limit
|
||||
self.current = current
|
||||
super().__init__(f"download quota exceeded: {reason} ({current}/{limit})")
|
||||
|
||||
@@ -49,7 +49,7 @@ class EditError(Exception):
|
||||
"""Bad edit request (empty spec / source not ready / not media); `reason` names the sentence."""
|
||||
|
||||
def __init__(self, reason: EditReason):
|
||||
self.reason = reason
|
||||
self.reason: EditReason = reason
|
||||
super().__init__(reason)
|
||||
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class BlockedUrl(Exception):
|
||||
|
||||
def __init__(self, reason: UrlBlockedReason):
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
self.reason: UrlBlockedReason = reason
|
||||
|
||||
|
||||
def _is_forbidden(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user