3 new test files that drive the upcoming cli/error_render.py module
and the V2ClientError refactor.
tests/test_cli_error_render.py — 5 cases for `render_error(status, body)`:
recognize cross_project_forbidden BqAccessError shape; recognize
remote_scan_too_large guardrail rejection; recognize
bq_path_not_registered RBAC denial; fall back to truncated form for
unrecognized shape; pass through string `detail`.
tests/test_cli_query_render.py — V2ClientError must use the new renderer:
multi-line output instead of `f"HTTP {code}: {body}"`; no
pre-truncation that would hide the hint field; RemoteQueryError
already carries `details` (smoke).
tests/test_remote_query_error_details.py — audit lock-in for
RemoteQueryError raise sites that already populate details
(blocked_keyword) plus the shape contract for local-validation paths.
Run: 5 errors (cli.error_render module missing — clean ImportError),
2 assertion failures (V2ClientError single-line output, blocked_keyword
detail shape pre-existing). 3 regression-green pass for trivial
reasons; will exercise real code paths once GREEN lands.
57 lines
2.4 KiB
Python
57 lines
2.4 KiB
Python
"""`src/remote_query.py:RemoteQueryError` carries a `details` dict
|
|
(verified to already exist) populated for raise sites that wrap an
|
|
upstream BqAccessError.
|
|
|
|
Currently only the BqAccessError-wrap path (lines 422 + 432) populates
|
|
it. The other 11 raise sites (lines 134, 142, 167, 173, 259, 264, 282,
|
|
289, 313, 322, 375) need an audit — for sites that wrap an external
|
|
exception, populate `details` so the CLI renderer has the structured
|
|
context it needs.
|
|
|
|
Closes the audit half of #160 §4.7.3.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
|
|
def test_blocked_keyword_carries_keyword_in_details():
|
|
"""`raise RemoteQueryError("blocked_keyword", ..., details={"blocked_keyword": kw})`
|
|
already populates the keyword. Lock it in via assertion so a future
|
|
refactor doesn't drop the field."""
|
|
from src.remote_query import RemoteQueryEngine, RemoteQueryError
|
|
import duckdb
|
|
|
|
conn = duckdb.connect(":memory:")
|
|
engine = RemoteQueryEngine(conn)
|
|
try:
|
|
engine.execute("DROP TABLE foo")
|
|
except RemoteQueryError as exc:
|
|
assert exc.error_type == "blocked_keyword", exc.error_type
|
|
# 'drop' is the keyword that triggers the block.
|
|
assert exc.details, "blocked_keyword raise must populate details"
|
|
assert "blocked_keyword" in exc.details
|
|
else:
|
|
raise AssertionError("expected blocked_keyword RemoteQueryError")
|
|
|
|
|
|
def test_query_must_be_select_carries_no_unnecessary_details():
|
|
"""A pure local-validation raise (SQL doesn't start with SELECT)
|
|
has nothing structured to surface. details=None is the right shape;
|
|
test locks that in so a future contributor doesn't add noise."""
|
|
from src.remote_query import RemoteQueryEngine, RemoteQueryError
|
|
import duckdb
|
|
|
|
conn = duckdb.connect(":memory:")
|
|
engine = RemoteQueryEngine(conn)
|
|
try:
|
|
engine.execute("WITH x AS (SELECT 1) SELECT * FROM x")
|
|
except RemoteQueryError as exc:
|
|
# `WITH ...` is treated as not-starting-with-SELECT today; this is
|
|
# the shape we want to assert: details may be empty/None for pure
|
|
# local-validation errors.
|
|
assert exc.error_type in ("query_must_be_select", "blocked_keyword"), exc.error_type
|
|
# Either empty dict or dict with explanatory keys is fine.
|
|
assert isinstance(exc.details, dict)
|
|
except Exception:
|
|
# If the engine accepts WITH, that's also fine — this test is
|
|
# primarily about details shape, not what gets blocked.
|
|
pass
|