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.
656 lines
23 KiB
Python
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"] == []
|