"""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"