Files
siftlode/backend/tests/test_refusal_sites.py
T
peter f8a8e905da fix(i18n): read refusals along a closed list of shapes, report the rest
The scan followed many shapes and called the rest unread, but each shape it learned opened a silent
branch: a parameter rebound in the body, a second visit of a followed helper, an `import app.x`, a name
bound twice or under an `if`, a spread or a store that replaces the code, a detail rebuilt from its
variable, a None reason. It now reads only what the app writes and reports everything else: a
branch the app never takes is gone (an `or`, a default, a base class's attribute, string and union
annotations, a bare module constant), a spread is clean only from an untouched `**kwargs` no call puts
a `code` in, and the detail's variable may only grow by `|=` or `["reason"]`. The real tree reads
exactly as before; every exit and every kept branch has a self-test that a mutant of it fails.

The detail test also fails a dict without a `code`, a JSONResponse sentence and a detail-less
HTTPException; the two that are no refusal (the setup gate's 503, an unmatched api path's 404) are
listed with why. The contract's assertion that compared the reason map with itself is gone.
2026-10-31 18:45:02 +01:00

656 lines
23 KiB
Python

"""The refusal scan (`refusal_sites.py`) on small sources: each shape on its closed list is followed to its
values, and each exit from that list is reported, unread or as an unread code. A scan that silently drops a
shape is a guard green over nothing, so every exit is pinned here, not only the paths the app takes. Pure:
no app import, no database."""
import textwrap
from tests.refusal_sites import scan, scan_app
def _scan(**files: str) -> dict:
return scan(
{f"{name.replace('__', '/')}.py": textwrap.dedent(src) for name, src in files.items()}
)
def _code(result: dict, code: str) -> dict:
return result["codes"][code]
def _unread(result: dict, code: str) -> list[str]:
return [u.split(": ", 1)[1] for u in _code(result, code)["unread"]]
REFUSAL = """
from fastapi import HTTPException
def refused(status: int, reason: str, **params: object) -> HTTPException:
return HTTPException(status_code=status, detail={"code": "refused", "reason": reason, **params})
"""
# The same helper without a `**params` spread: what a call passes it is about the reason alone.
PLAIN = """
from fastapi import HTTPException
def refused(status: int, reason: str) -> HTTPException:
return HTTPException(status_code=status, detail={"code": "refused", "reason": reason})
"""
def test_a_literal_wherever_it_sits_in_the_dict():
r = _scan(
a="""
def f(x):
raise HTTPException(409, {"code": "clip", "limit_s": 5, "reason": "too_long"})
def g(x):
raise HTTPException(409, {"reason": "before", "code": "clip"})
def h(x):
raise HTTPException(409, {
"code": "clip", # a comment between
"reason": "after_comment",
})
def i(x):
raise HTTPException(409, detail=dict(code="clip", reason="by_call"))
"""
)
assert _code(r, "clip")["reasons"] == ["after_comment", "before", "by_call", "too_long"]
assert _code(r, "clip")["unread"] == []
def test_both_arms_of_a_conditional_and_a_none_arm_makes_the_site_reasonless():
r = _scan(
a="""
def f(x):
raise HTTPException(409, {"code": "edit", "reason": "a" if x else "b"})
def g(x):
raise HTTPException(409, {"code": "playlist", "reason": "lit" if x else None})
def h(x):
raise HTTPException(409, {"code": "busy", "reason": None})
"""
)
assert _code(r, "edit")["reasons"] == ["a", "b"] and _code(r, "edit")["reasonless"] == []
assert _code(r, "playlist")["reasons"] == ["lit"]
assert _code(r, "playlist")["reasonless"] == ["a.py:5"]
assert _code(r, "busy")["reasonless"] == ["a.py:7"]
def test_what_the_list_does_not_hold_is_unread():
r = _scan(
a="""
def f(x, svc):
raise HTTPException(409, {"code": "edit", "reason": x.why or "c"})
def g(svc):
raise HTTPException(409, {"code": "edit", "reason": svc.reason()})
def h():
raise HTTPException(409, {"code": "edit", "reason": 5})
def i(e):
raise HTTPException(409, {"code": "edit", "reason": f"x{e}"})
"""
)
assert _unread(r, "edit") == ["x.why or 'c'", "svc.reason()", "5", "f'x{e}'"]
assert _code(r, "edit")["reasons"] == []
def test_a_helper_is_followed_to_every_call_however_it_is_written():
r = _scan(
refusal=REFUSAL,
routes__a="""
from http import HTTPStatus
from app import refusal
from app.refusal import refused
def f(status):
raise refused(status, "by_name_status")
def g():
raise refused(HTTPStatus.CONFLICT, reason="by_keyword")
def h():
raise refusal.refused(409, "qualified")
def i(x):
raise refused(409, "a" if x else "second_arm")
""",
)
assert _code(r, "refused")["reasons"] == [
"a",
"by_keyword",
"by_name_status",
"qualified",
"second_arm",
]
assert _code(r, "refused")["unread"] == []
def test_a_call_the_helper_cannot_be_followed_through_is_unread():
r = _scan(
refusal=PLAIN,
routes__a="""
from app.refusal import refused
def f(args, kw):
pick = refused
raise refused(*args)
def g(kw):
raise refused(409, **kw)
def h():
raise refused(409)
""",
routes__b="""
import app.refusal
def f():
raise app.refusal.refused(409, "dotted")
""",
routes__c="""
from ..refusal import refused
def f():
raise refused(409, "relative")
""",
)
unread = _unread(r, "refused")
assert len(unread) == 6
assert sum("the helper used as a value" in u for u in unread) == 1
assert sum("a reference the scan cannot resolve" in u for u in unread) == 2
assert sum("does not pass reason" in u for u in unread) == 2 # `**kw`, and nothing at all
assert any(u == "refused(*args)" for u in unread)
assert _code(r, "refused")["reasons"] == []
def test_a_default_is_not_followed_a_call_must_pass_the_reason():
r = _scan(
routes___common="""
def not_found(what: str = "generic"):
return HTTPException(404, {"code": "not_found", "reason": what})
def get_or_404(db, id_, *, what: str = "generic"):
raise not_found(what)
""",
routes__a="""
from app.routes._common import get_or_404, not_found
def f(db):
get_or_404(db, 1, what="user")
get_or_404(db, 2)
raise not_found("debug_session")
def g():
raise not_found()
""",
)
assert _code(r, "not_found")["reasons"] == ["debug_session", "user"]
assert [u.endswith("(does not pass what)") for u in _unread(r, "not_found")] == [True, True]
def test_a_positional_only_reason_passed_by_name_is_not_bound_to_it():
r = _scan(
a="""
def deny(reason: str, /, **params):
return HTTPException(409, {"code": "playlist", "reason": reason})
def f():
raise deny("by_position")
def g():
raise deny(reason="lands_in_params")
"""
)
assert _code(r, "playlist")["reasons"] == ["by_position"]
assert _unread(r, "playlist") == ["deny(reason='lands_in_params') (does not pass reason)"]
def test_a_helper_reaching_two_sites_gives_both_its_reasons_and_one_reached_again_is_unread():
r = _scan(
a="""
def deny(reason: str, admin):
if admin:
return HTTPException(409, {"code": "activate", "reason": reason})
return HTTPException(409, {"code": "playlist", "reason": reason})
def again(reason: str, n):
if n:
return again(reason, n - 1)
return HTTPException(409, {"code": "edit", "reason": reason})
def f():
raise deny("foo", True)
def g():
raise again("bar", 3)
"""
)
assert _code(r, "activate")["reasons"] == ["foo"]
assert _code(r, "playlist")["reasons"] == ["foo"]
assert _code(r, "edit")["reasons"] == ["bar"]
assert any("reached again while it is followed" in u for u in _unread(r, "edit"))
def test_a_parameter_its_body_binds_again_is_unread():
r = _scan(
a="""
from typing import Literal
def refused(status, reason: str = ""):
reason = reason or "fallback"
return HTTPException(status, {"code": "refused", "reason": reason})
def typed(reason: Literal["a"]):
if not reason:
reason = "b"
return HTTPException(409, {"code": "edit", "reason": reason})
def f():
raise refused(409, "x")
"""
)
assert _code(r, "refused")["reasons"] == []
assert _unread(r, "refused") == ["reason (a parameter its body binds again)"]
assert _unread(r, "edit") == ["reason (a parameter its body binds again)"]
def test_a_local_is_followed_only_through_plain_assignments():
r = _scan(
a="""
from typing import Literal
def f(x):
why = "first"
if x:
why = "second" if x > 1 else "third"
raise HTTPException(409, {"code": "edit", "reason": why})
def g(svc):
hit, why = svc.fetch()
raise HTTPException(409, {"code": "playlist", "reason": why})
def h(items):
for why in items:
pass
raise HTTPException(409, {"code": "playlist", "reason": why})
def i():
why: Literal["typed"] = pick()
raise HTTPException(409, {"code": "playlist", "reason": why})
def j(items):
LABELS[why] = "NotAReason"
raise HTTPException(409, {"code": "playlist", "reason": why})
"""
)
assert _code(r, "edit")["reasons"] == ["first", "second", "third"]
assert _code(r, "playlist")["reasons"] == []
assert _unread(r, "playlist") == [
"why",
"why",
"why",
"why",
] # unpacked, a loop, annotated, unbound
def test_a_call_answers_with_its_literal_return_type():
r = _scan(
a="""
from typing import Literal
def why(x) -> Literal["quota", "gone"]:
return "quota"
def how(x) -> str:
return "x"
def f(x):
raise HTTPException(409, {"code": "youtube", "reason": why(x)})
def g(x):
raise HTTPException(409, {"code": "youtube", "reason": how(x)})
"""
)
assert _code(r, "youtube")["reasons"] == ["gone", "quota"]
assert _unread(r, "youtube") == ["how(x)"]
def test_a_module_constant_is_read_only_as_mod_name_and_only_when_bound_once():
r = _scan(
urlguard="""
REASON_MALFORMED = "malformed"
TWICE = "a"
TWICE = "b"
if FLAG:
LATER = "c"
""",
routes__a="""
from app import urlguard
BARE = "bare"
def f():
raise HTTPException(409, {"code": "url_blocked", "reason": urlguard.REASON_MALFORMED})
def g():
raise HTTPException(409, {"code": "url_blocked", "reason": urlguard.TWICE})
def h():
raise HTTPException(409, {"code": "url_blocked", "reason": urlguard.LATER})
def i():
raise HTTPException(409, {"code": "url_blocked", "reason": BARE})
""",
)
assert _code(r, "url_blocked")["reasons"] == ["malformed"]
assert _unread(r, "url_blocked") == ["urlguard.TWICE", "urlguard.LATER", "BARE"]
def test_a_name_imported_from_two_places_or_under_an_if_is_not_looked_up():
r = _scan(
m1='R = Literal["one"]',
m2='R = Literal["two"]',
a="""
def f():
from app.m1 import R
def g():
from app.m2 import R
def h(reason: R):
return HTTPException(409, {"code": "edit", "reason": reason})
""",
b="""
if FLAG:
from app.m1 import R
def h(reason: R):
return HTTPException(409, {"code": "playlist", "reason": reason})
""",
c="""
from app.m1 import R
def h(reason: R):
return HTTPException(409, {"code": "clip", "reason": reason})
""",
)
assert _code(r, "edit")["reasons"] == [] and _unread(r, "edit") == ["reason"]
assert _code(r, "playlist")["reasons"] == [] and _unread(r, "playlist") == ["reason"]
assert _code(r, "clip")["reasons"] == ["one"]
def test_a_caught_exception_answers_with_the_literal_its_class_declares_on_self():
r = _scan(
errors="""
from typing import Literal
ClipReason = Literal["bad_range", "too_short"]
EditReason = Literal["not_media"]
class ClipError(Exception):
def __init__(self, reason: ClipReason, **params: float):
self.reason: ClipReason = reason
self.params = params
class EditError(Exception):
reason: EditReason
class Untyped(Exception):
def __init__(self, reason: str):
self.reason = reason
class Narrower(ClipError):
pass
""",
routes__a="""
from app import errors
def f():
try:
pass
except errors.ClipError as e:
raise HTTPException(422, {"code": "clip", "reason": e.reason})
except (errors.ClipError, errors.EditError) as e:
raise HTTPException(422, {"code": "clip", "reason": e.reason})
except errors.EditError as e:
raise HTTPException(422, {"code": "clip", "reason": e.reason})
except errors.Untyped as e:
raise HTTPException(422, {"code": "clip", "reason": e.reason})
except errors.Narrower as e:
raise HTTPException(422, {"code": "clip", "reason": e.reason})
except errors.ClipError as e:
raise HTTPException(422, {"code": "clip", "reason": e})
""",
)
assert _code(r, "clip")["reasons"] == ["bad_range", "too_short"]
assert _unread(r, "clip") == [
"e.reason", # a tuple of classes
"e.reason (EditError.reason has no Literal type)", # declared on the class body only
"e.reason (Untyped.reason has no Literal type)",
"e.reason (Narrower.reason has no Literal type)", # a subclass: not looked up the bases
"e", # the exception itself
]
def test_a_parameter_answers_with_its_literal_or_its_class_and_any_other_type_is_unread():
r = _scan(
a="""
from typing import Literal
Why = Literal["one", "two"]
class Err(Exception):
def __init__(self, reason: Literal["three"]):
self.reason: Literal["three"] = reason
def typed(reason: Why):
return HTTPException(409, {"code": "playlist", "reason": reason})
def inline(reason: Literal["four"]):
return HTTPException(409, {"code": "playlist", "reason": reason})
def by_class(exc: Err):
return HTTPException(409, {"code": "playlist", "reason": exc.reason})
def rebound(exc: Err):
exc = other()
return HTTPException(409, {"code": "playlist", "reason": exc.reason})
def optional(reason: Literal["five"] | None = None):
return HTTPException(409, {"code": "playlist", "reason": reason})
def quoted(reason: "Why"):
return HTTPException(409, {"code": "playlist", "reason": reason})
def number(reason: int):
return HTTPException(409, {"code": "playlist", "reason": reason})
class C:
def method(self, reason: str):
return HTTPException(409, {"code": "playlist", "reason": reason})
"""
)
assert _code(r, "playlist")["reasons"] == ["four", "one", "three", "two"]
unread = _unread(r, "playlist")
# a parameter bound again, `| None`, a quoted annotation, `int`
assert unread[:4] == ["exc.reason", "reason", "reason", "reason"]
assert "method(reason) is not a module-level helper" in unread[4]
assert len(unread) == 5
def test_a_str_helper_nobody_calls_sends_nothing():
r = _scan(
a="""
def helper(reason: str):
return HTTPException(409, {"code": "playlist", "reason": reason})
"""
)
assert _code(r, "playlist") == {"reasons": [], "unread": [], "sites": 1, "reasonless": []}
def test_a_map_lookup_is_its_values_and_its_keys_must_be_exactly_what_the_index_can_be():
src = """
from typing import Literal
SettingReason = Literal["too_low", "too_high"]
class InvalidSetting(ValueError):
def __init__(self, key: str, reason: SettingReason, **params: object):
self.reason: SettingReason = reason
self.params = params
_MAP: dict[SettingReason, str] = {{{keys}}}
def refused(status: int, reason: str, **params: object):
return HTTPException(status, {{"code": "refused", "reason": reason, **params}})
def setting_refused(exc: InvalidSetting):
return refused(400, _MAP[{index}], **exc.params)
"""
both = '"too_low": "setting_too_low", "too_high": "setting_too_high"'
whole = _scan(a=src.format(keys=both, index="exc.reason"))
assert _code(whole, "refused")["reasons"] == ["setting_too_high", "setting_too_low"]
assert whole["errors"] == [] and whole["unreadCodes"] == []
for keys, index in [
('"too_low": "setting_too_low"', "exc.reason"), # a key short
(both + ', "gone": "setting_gone"', "exc.reason"), # a key no index can be
(both, "exc.key"), # an index the scan cannot read
]:
r = _scan(a=src.format(keys=keys, index=index))
assert len(r["errors"]) == 1 and "not what its index can be" in r["errors"][0]
def test_a_lookup_in_anything_but_a_module_dict_of_literal_keys_is_unread():
r = _scan(
a="""
BUILT = make()
NUMBERED = {1: "one"}
def f(table, k):
raise HTTPException(409, {"code": "edit", "reason": table[k]})
def g(k):
raise HTTPException(409, {"code": "edit", "reason": BUILT[k]})
def h(k):
raise HTTPException(409, {"code": "edit", "reason": NUMBERED[k]})
"""
)
assert _unread(r, "edit") == ["table[k]", "BUILT[k]", "NUMBERED[k]"]
assert r["errors"] == []
def test_a_code_must_be_one_literal():
r = _scan(
a="""
BUSY = "busy"
def f(code):
raise HTTPException(409, {"code": "busy"})
def g(code):
raise HTTPException(409, {"code": code})
def h():
raise HTTPException(409, {"code": BUSY})
def i(x):
raise HTTPException(409, {"code": "a" if x else "b"})
def j(x):
raise HTTPException(409, {"code": "a" if x else None})
def k(key):
raise HTTPException(409, {"code": "edit", key: "late"})
"""
)
assert r["codes"]["busy"]["reasonless"] == ["a.py:4"]
assert set(r["codes"]) == {"busy"}
assert len(r["unreadCodes"]) == 5
assert r["unreadCodes"][-1].endswith("(a key the scan cannot read)")
def test_a_spread_is_clean_only_from_an_untouched_kwargs_no_call_puts_a_code_in():
def unread_codes(helper: str, call: str) -> list[str]:
return _scan(a=helper + "\n" + f"def caller(x, more):\n raise {call}\n")["unreadCodes"]
helper = """
def deny(status, reason: str, **params):
return HTTPException(status, {"code": "refused", "reason": reason, **params})
"""
assert unread_codes(helper, 'deny(409, "a", limit=3)') == []
assert unread_codes(helper, 'deny(409, "a", code=x)') # the spread puts its own code
assert unread_codes(helper, 'deny(409, "a", **more)') # a spread nothing vouches for
assert unread_codes(helper + "pick = deny\n", 'deny(409, "a")') # passed around
posonly = helper.replace("reason: str,", "reason: str, /,") # `reason=` lands in params
assert unread_codes(posonly, 'deny(409, "a")')
mutated = helper.replace(" return", ' params["reason"] = "zz"\n return')
assert unread_codes(mutated, 'deny(409, "a")')
renamed = helper.replace("**params})", "**{k.lstrip('_'): v for k, v in params.items()}})")
assert unread_codes(renamed, 'deny(409, "a")')
filtered = helper.replace("**params})", "**{k: v for k, v in params.items() if v}})")
assert unread_codes(filtered, 'deny(409, "a")') == []
def test_a_caught_exceptions_params_are_clean_only_when_kept_from_its_init():
def unread_codes(errors: str, construct: str = 'Err("a", limit=1)') -> list[str]:
return _scan(
errors=errors,
a=f"""
from app.errors import Err
def f():
raise {construct}
def g():
try:
pass
except Err as e:
raise HTTPException(422, {{"code": "clip", "reason": e.reason, **e.params}})
""",
)["unreadCodes"]
kept = """
from typing import Literal
class Err(Exception):
def __init__(self, reason: Literal["a"], **params: float):
super().__init__(f"{reason} {params}")
self.reason: Literal["a"] = reason
self.params = params
"""
assert unread_codes(kept) == []
assert unread_codes(kept, 'Err("a", code="other")')
assert unread_codes(kept + "\n class Sub(Err):\n pass\n")
assert unread_codes(kept.replace("self.params = params", "self.params = {'code': 'x'}"))
assert unread_codes(kept + "\n def more(self):\n self.params = {}\n")
def test_a_detail_built_up_in_steps_is_read_through_its_variable():
r = _scan(
a="""
def f(status: int, reason: str, user=None, **extra):
detail: dict = {"code": "login", "reason": "invalid"}
if user is not None:
detail |= {"email": user.email, "reason": "suspended"}
detail |= {k: v for k, v in extra.items() if v is not None}
detail["email"] = "x"
return HTTPException(status, detail)
def g(x):
detail = {"code": "login"} if x else {"code": "login", "reason": "other"}
detail["reason"] = "late"
return HTTPException(401, detail=detail)
"""
)
assert r["unreadCodes"] == []
assert _code(r, "login")["reasons"] == ["invalid", "late", "other", "suspended"]
def test_a_detail_changed_or_sent_any_other_way_is_an_unread_code():
changes = [
"detail.update(more)",
"detail |= more",
'detail |= {"code": "other"}',
'detail["code"] = "other"',
'del detail["reason"]',
"detail[key] = more",
'detail = {**detail, "reason": "b"}',
"detail = rebuilt()",
"copy = detail",
"add_extra(detail)",
"return detail",
'return {**detail, "reason": "b"}',
'return dict(detail, reason="b")',
]
for change in changes:
r = _scan(
a=f"""
def f(key, more):
detail = {{"code": "login", "reason": "a"}}
{change}
raise HTTPException(401, detail)
"""
)
assert r["unreadCodes"], change
assert "login" not in r["codes"], change
held = _scan(a='class C:\n detail = {"code": "login"}\n')
assert (
held["unreadCodes"]
and "a detail put where the scan cannot follow it" in held["unreadCodes"][0]
)
def test_the_data_files_are_excluded_by_name_only():
r = _scan(
plex__tmdb='def f():\n return {"code": "HU", "name": "Hungary"}',
plex__other='def f():\n return {"code": "HU"}',
)
assert r["excluded"] == {"plex/tmdb.py": 1}
assert "HU" in r["codes"]
def test_the_app_scans():
"""The real tree, on the interpreter the backend runs: every refusal it holds is read (the
frontend's contract judges what is read)."""
result = scan_app()
assert {"refused", "not_found", "login", "clip"} <= set(result["codes"])
assert result["errors"] == [] and result["unreadCodes"] == []