"""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"] == []