Files
zeekay 53cd11b0a5 hanzo-iam: one decoder, so a wrong path names itself
hanzo.id serves its sign-in SPA on every unmatched path, so a path IAM
does not serve answers 200 text/html. Forty-four call sites went straight
to .json() on the strength of the status code and died inside it with a
byte offset naming neither the path nor the reason. oauth was the one
module that asked, in a guard of its own.

routes.decode is that question, once, beside the unwrap that answers the
envelope one: the wire decision, then the envelope decision, each with a
name. A test walks the syntax tree and fails if any module decodes a body
on its own.

141 pass across the package and the two consumers that build on it.
2026-08-23 12:57:04 -07:00

68 lines
2.2 KiB
Python

"""One wire decision: is the body JSON, and did IAM answer at all?
hanzo.id serves its sign-in SPA on every unmatched path, so a wrong path answers
200 text/html. A decoder that trusts the status code dies inside .json() with a
byte offset that names neither the path nor the reason.
"""
import ast
from pathlib import Path
import httpx
import pytest
from hanzo_iam import routes
def response(body: bytes, content_type: str, status: int = 200) -> httpx.Response:
request = httpx.Request("GET", "https://hanzo.id/v1/iam/nope")
return httpx.Response(status, content=body, headers={"content-type": content_type}, request=request)
def test_json_decodes():
assert routes.decode(response(b'{"users": []}', "application/json")) == {"users": []}
def test_the_sign_in_page_names_the_path():
with pytest.raises(routes.IAMError) as caught:
routes.decode(response(b"<!doctype html><title>Sign in</title>", "text/html"))
message = str(caught.value)
assert "/v1/iam/nope" in message
assert "text/html" in message
assert "not a refusal" in message
def test_a_body_with_no_content_type_is_not_guessed():
with pytest.raises(routes.IAMError):
routes.decode(response(b"{}", ""))
def test_unparseable_json_names_the_url():
with pytest.raises(routes.IAMError) as caught:
routes.decode(response(b"{not json", "application/json"))
assert "hanzo.id" in str(caught.value)
def test_nothing_decodes_a_body_on_its_own():
"""Every module asks routes.decode.
routes owns the decision; oauth carries the same guard for the login flow,
which runs before a client exists. A mention in a comment is not a call, so
this reads the syntax tree rather than the text.
"""
package = Path(__file__).resolve().parent.parent / "hanzo_iam"
loose = set()
for f in package.glob("*.py"):
if f.name in {"routes.py", "oauth.py"}:
continue
for node in ast.walk(ast.parse(f.read_text())):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "json"
and not node.args
):
loose.add(f.name)
assert not loose, loose