"""No refusal answers in a hand-written English sentence. The UI is bilingual and translates a structured `detail` (`{code, reason, …}`), but a string detail it can only print as it came — so ~150 of them reached the Hungarian UI verbatim, in the blocking error dialog and on every inline surface that shows `HttpError.detail`. `app/refusal.py` is the family they became; this test is what keeps a new one from being written the old way. Pure: it reads the sources. """ import ast import pathlib APP = pathlib.Path(__file__).resolve().parents[1] / "app" def _is_text(node: ast.AST) -> bool: """A detail that is a sentence: a literal, an f-string, `str(exc)`, or `"…" + x`.""" if isinstance(node, ast.Constant) and isinstance(node.value, str): return True if isinstance(node, ast.JoinedStr): return True if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "str": return True if isinstance(node, ast.BoolOp): return any(_is_text(v) for v in node.values) if isinstance(node, ast.BinOp): return _is_text(node.left) or _is_text(node.right) return False def test_no_http_exception_carries_a_sentence(): offenders = [] for path in sorted(APP.rglob("*.py")): for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): if not ( isinstance(node, ast.Call) and getattr(node.func, "id", getattr(node.func, "attr", "")) == "HTTPException" ): continue kw = {k.arg: k.value for k in node.keywords} detail = kw.get("detail", node.args[1] if len(node.args) > 1 else None) if detail is not None and _is_text(detail): offenders.append(f"{path.relative_to(APP).as_posix()}:{node.lineno}") assert offenders == [], ( "a refusal with an English sentence for a detail — raise `refused(status, reason)` " f"(app/refusal.py) and name the reason in both locales: {offenders}" ) def test_the_scan_sees_a_sentence_when_there_is_one(): """A guard that matches nothing is green over nothing.""" sample = ast.parse('HTTPException(status_code=400, detail=f"bad {x}")').body[0].value assert _is_text(sample.keywords[1].value) assert not _is_text(ast.parse('{"code": "refused", "reason": "x"}').body[0].value) # Answers that are no refusal of the app's, by the function they are sent from: the client shows a # detail only when it is an object (`lib/api/core.ts`), so these read as its generic line, and no screen # needs more from them. NO_REFUSAL = { ("main.py", "setup_gate"): "the 503 before setup: the setup screen is what the user sees", ("main.py", "spa_fallback"): "a bare 404 for an api path no route matches: no screen calls one", } def _detail_offenders(source: str, path: str) -> list[str]: """Each detail the client could not say: a dict without a `code`, a sentence, or none at all. `HTTPException(status, detail)` and `JSONResponse({"detail": …})`; a variable or a call is the refusal scan's to read (`refusal_sites.py`, which follows each `code` dict).""" tree = ast.parse(source) parents = {child: node for node in ast.walk(tree) for child in ast.iter_child_nodes(node)} out = [] for node in ast.walk(tree): name = isinstance(node, ast.Call) and getattr( node.func, "id", getattr(node.func, "attr", "") ) if name == "HTTPException": kw = {k.arg: k.value for k in node.keywords} detail = kw.get("detail", node.args[1] if len(node.args) > 1 else None) elif name == "JSONResponse": content = {k.arg: k.value for k in node.keywords}.get( "content", node.args[0] if node.args else None ) if not isinstance(content, ast.Dict) or not any( isinstance(k, ast.Constant) and k.value == "detail" for k in content.keys ): continue # an answer, not a refusal detail = next( v for k, v in zip(content.keys, content.values, strict=True) if isinstance(k, ast.Constant) and k.value == "detail" ) else: continue fn = parents.get(node) while fn is not None and not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)): fn = parents.get(fn) if (path, getattr(fn, "name", None)) in NO_REFUSAL: continue keys = ( [k.value for k in detail.keys if isinstance(k, ast.Constant)] if isinstance(detail, ast.Dict) else [k.arg for k in detail.keywords] if isinstance(detail, ast.Call) and getattr(detail.func, "id", "") == "dict" else None ) if detail is None or _is_text(detail) or (keys is not None and "code" not in keys): out.append(f"{path}:{node.lineno}") return out def test_every_detail_is_one_the_client_can_say(): offenders = [ o for p in sorted(APP.rglob("*.py")) for o in _detail_offenders(p.read_text(encoding="utf-8"), p.relative_to(APP).as_posix()) ] assert offenders == [], ( "a refusal the client can only answer with its generic line — send `{code, reason}` " f"(`refused(status, reason)`), or list it in NO_REFUSAL with why: {offenders}" ) def test_the_detail_scan_sees_each_shape_the_client_cannot_say(): src = """ def f(x): raise HTTPException(409, detail={"reason": "view_name_taken"}) def g(x): raise HTTPException(409, dict(reason="taken")) def h(x): raise HTTPException(status_code=404) def i(x): return JSONResponse({"detail": "This instance isn't set up yet."}, status_code=503) def j(x): return JSONResponse(content={"detail": {"reason": "x"}}, status_code=409) def ok(x): raise HTTPException(409, {"code": "refused", "reason": "x"}) raise HTTPException(409, detail(x)) return JSONResponse({"detail": {"code": "too_large"}}, status_code=413) return JSONResponse({"ok": True}) def setup_gate(x): return JSONResponse({"detail": "listed"}, status_code=503) """ assert _detail_offenders(src, "main.py") == [f"main.py:{n}" for n in (3, 5, 7, 9, 11)]