Files
ThothII/harness/tests/test_rest_client.py
T
marcopan eb3bde90e2 test(harness): L0 testcontainers + L1 contract tests for ported db/mschema/rest (A9, spec §1)
Ports the leaf data-layer modules and validates them:
- mschema/ (models, eligibility, merge, render), db/ (connection, sampling,
  introspect, fetch_ca), rest/client.py -- renamed psdwp3->nsp, verbatim.
- L0 (testcontainers, real Postgres): db connection read-only enforcement
  (psd_ro cannot CREATE/INSERT), introspect against a known schema (tables,
  columns, types, comments, FKs, enum, composite PK), sampling most-frequent
  values + truncation reporting. 15 tests, ~4s.
- L1 (fake data): rest/client RPC contract (mocked transport -- X-API-Key
  header, payloads, base_url slash handling, HTTP/network error surfacing),
  mschema/render 3 formats (markdown, mschema-text, schema-dict) +
  eligibility rules (wide_text excluded, short_text/numeric/enum/temporal/
  boolean eligible, annotation override wins). 25 tests.

pyproject registers l0/l2 markers + addopts '-m not l2' (L2 opt-in).

Deferred to their dependency-porting tasks: test_rrf.py (search needs
vectorstore, B3) and the 11 CLI contract tests (need _guards/session, wired
when each command lands). 'Not assumed reliable' now has real teeth for the
data layer; CLI/search contracts follow.
2026-06-26 22:53:08 +02:00

116 lines
4.0 KiB
Python

"""L1: rest/client RPC contract (mocked transport — no network).
Verifies the ported RestClient: each rpc carries the X-API-Key header, the right
payload, base_url slash handling, and HTTP/network errors surface as RestError.
Ported (renamed psdwp3->nsp) from ChironeWp3/tests/test_rest_client.py.
"""
import pytest
import requests
from nsp.config import RestConfig
from nsp.rest.client import RestClient, RestError
class FakeResponse:
def __init__(self, payload=None, status=200, text=None):
self._payload = payload
self.status_code = status
self.text = text if text is not None else ("" if payload is None else "json")
@property
def ok(self):
return self.status_code < 400
def json(self):
if self._payload is None:
raise ValueError("no json")
return self._payload
def _client(timeout=30):
return RestClient(RestConfig(base_url="https://h/dwh/", api_key="dwh_k", timeout=timeout))
def _capture(monkeypatch, response):
calls = []
def fake_post(url, json=None, headers=None, timeout=None, verify=None):
calls.append({"url": url, "json": json, "headers": headers,
"timeout": timeout, "verify": verify})
return response
monkeypatch.setattr("nsp.rest.client.requests.post", fake_post)
return calls
def test_run_query_payload_and_rows(monkeypatch):
calls = _capture(monkeypatch, FakeResponse([{"x": 1}]))
rows = _client().run_query("SELECT 1 AS x")
assert rows == [{"x": 1}]
c = calls[0]
assert c["url"] == "https://h/dwh/rpc/run_query"
assert c["json"] == {"query_text": "SELECT 1 AS x"}
assert c["headers"]["X-API-Key"] == "dwh_k"
assert c["timeout"] == 30
def test_explain_query_maps_lines(monkeypatch):
plan = [
{"line": "Aggregate (cost=3747.24..3747.25 rows=1 width=8)"},
{"line": " -> Seq Scan on dim_patient (cost=0.00..3712.97 rows=13706 width=0)"},
]
_capture(monkeypatch, FakeResponse(plan))
lines = _client().explain_query("SELECT count(*) FROM dim_patient")
assert lines == [
"Aggregate (cost=3747.24..3747.25 rows=1 width=8)",
" -> Seq Scan on dim_patient (cost=0.00..3712.97 rows=13706 width=0)",
]
def test_ping(monkeypatch):
calls = _capture(monkeypatch, FakeResponse({"db_connected": True, "role": "postgres"}))
out = _client().ping()
assert out["db_connected"] is True
assert calls[0]["url"] == "https://h/dwh/rpc/ping"
assert calls[0]["json"] == {}
def test_top_values_payload(monkeypatch):
calls = _capture(monkeypatch, FakeResponse([{"value": "MI", "count": 13737}]))
out = _client().top_values("datawarehouse", "dim_patient", "provincia", 1000)
assert out == [{"value": "MI", "count": 13737}]
assert calls[0]["json"] == {
"schema_name": "datawarehouse",
"table_name": "dim_patient",
"column_name": "provincia",
"max_values": 1000,
}
def test_validate_select_ok_and_write(monkeypatch):
_capture(monkeypatch, FakeResponse(status=204))
assert _client().validate_select("SELECT 1") is True
_capture(monkeypatch, FakeResponse({"message": "Only SELECT / WITH statements are allowed"}, status=400))
assert _client().validate_select("DELETE FROM t") is False
def test_http_error_surfaces_message(monkeypatch):
_capture(monkeypatch, FakeResponse({"message": "boom"}, status=400))
with pytest.raises(RestError, match="boom"):
_client().run_query("SELECT bad")
def test_network_error_actionable(monkeypatch):
def boom(*a, **k):
raise requests.ConnectionError("refused")
monkeypatch.setattr("nsp.rest.client.requests.post", boom)
with pytest.raises(RestError, match="raggiungibile"):
_client().ping()
def test_base_url_without_trailing_slash(monkeypatch):
calls = _capture(monkeypatch, FakeResponse({"db_connected": True}))
RestClient(RestConfig(base_url="https://h/dwh", api_key="k")).ping()
assert calls[0]["url"] == "https://h/dwh/rpc/ping"