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.
57 lines
1.8 KiB
Python
57 lines
1.8 KiB
Python
"""L0: db/connection read-only enforcement against real Postgres (testcontainers).
|
|
|
|
The ported read-only contract: the psd_ro role can SELECT but not write, and
|
|
can_create_in_schema / writable_tables reflect that. This is where 'ported code
|
|
is not assumed reliable' gains real teeth for the data layer.
|
|
"""
|
|
import pytest
|
|
from sqlalchemy import create_engine, text
|
|
|
|
from nsp.db.connection import can_create_in_schema, make_engine, ping, writable_tables
|
|
|
|
pytestmark = [pytest.mark.l0]
|
|
|
|
|
|
def test_ping_succeeds_on_read_only_role(ro_url):
|
|
engine = create_engine(ro_url)
|
|
try:
|
|
ping(engine) # SELECT 1 — must not raise
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_read_only_role_cannot_create_in_schema(ro_url):
|
|
engine = create_engine(ro_url)
|
|
try:
|
|
# psd_ro has USAGE + SELECT only, not CREATE on the dw schema.
|
|
assert can_create_in_schema(engine, "dw") is False
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_writable_tables_empty_for_read_only_role(ro_url):
|
|
engine = create_engine(ro_url)
|
|
try:
|
|
tables = writable_tables(engine, "dw")
|
|
assert tables == [] # read-only role has no INSERT/UPDATE/DELETE grants
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_read_only_role_cannot_insert(ro_url):
|
|
"""The hard guarantee: a write attempt raises (enforced by Postgres, surfaced
|
|
by our engine)."""
|
|
engine = create_engine(ro_url)
|
|
try:
|
|
with pytest.raises(Exception):
|
|
with engine.begin() as conn:
|
|
conn.execute(text('INSERT INTO dw.dim_pazienti VALUES (999, %s, %s)'),
|
|
("test", "test"))
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_admin_engine_can_create_in_schema(admin_engine):
|
|
# Sanity: the admin (table owner) CAN create — confirms the test harness itself.
|
|
assert can_create_in_schema(admin_engine, "dw") is True
|