fix: establish read-only audit transaction before identity check
This commit is contained in:
125
tests/test_blif_flow_v2_readonly_transaction_order.py
Normal file
125
tests/test_blif_flow_v2_readonly_transaction_order.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from contextlib import nullcontext
|
||||
|
||||
import pytest
|
||||
|
||||
import scripts.simulate_blif_flow_v2 as simulator
|
||||
|
||||
|
||||
class _Result:
|
||||
def __init__(self, *, identity=None, rows=()):
|
||||
self._identity = identity
|
||||
self._rows = rows
|
||||
|
||||
def one(self):
|
||||
return self._identity
|
||||
|
||||
def mappings(self):
|
||||
return self._rows
|
||||
|
||||
|
||||
class _Connection:
|
||||
def __init__(self, database="clientflow", user="clientflow"):
|
||||
self.database = database
|
||||
self.user = user
|
||||
self.read_only = False
|
||||
self.statements = []
|
||||
self.identity_observations = []
|
||||
|
||||
def execution_options(self, **_kwargs):
|
||||
return self
|
||||
|
||||
def execute(self, statement):
|
||||
sql = " ".join(str(statement).split())
|
||||
self.statements.append(sql)
|
||||
upper = sql.upper()
|
||||
if upper == "BEGIN READ ONLY":
|
||||
self.read_only = True
|
||||
return _Result()
|
||||
if upper == "ROLLBACK":
|
||||
self.read_only = False
|
||||
return _Result()
|
||||
if upper.startswith(("INSERT ", "UPDATE ", "DELETE ")):
|
||||
if self.read_only:
|
||||
raise RuntimeError("cannot execute write in a read-only transaction")
|
||||
return _Result()
|
||||
if "CURRENT_DATABASE()" in upper:
|
||||
identity = (self.database, self.user, "on" if self.read_only else "off")
|
||||
self.identity_observations.append(identity)
|
||||
return _Result(identity=identity)
|
||||
return _Result(rows=())
|
||||
|
||||
|
||||
class _Engine:
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def connect(self):
|
||||
return nullcontext(self.connection)
|
||||
|
||||
|
||||
def _load(monkeypatch, connection, **kwargs):
|
||||
monkeypatch.setattr(simulator, "engine", _Engine(connection))
|
||||
return simulator._load(**kwargs)
|
||||
|
||||
|
||||
def test_strict_load_establishes_read_only_before_same_connection_identity_and_reads(monkeypatch):
|
||||
connection = _Connection()
|
||||
assert connection.read_only is False # normal fresh-connection state
|
||||
|
||||
result = _load(
|
||||
monkeypatch, connection, expected_database="clientflow",
|
||||
expected_user="clientflow", require_read_only=True,
|
||||
)
|
||||
|
||||
assert connection.statements[0] == "BEGIN READ ONLY"
|
||||
assert "CURRENT_DATABASE()" in connection.statements[1].upper()
|
||||
assert connection.identity_observations == [("clientflow", "clientflow", "on")]
|
||||
assert result["identity"]["transaction_read_only"] == "on"
|
||||
assert connection.statements[2].upper().startswith("SELECT O.*")
|
||||
assert connection.statements[-1] == "ROLLBACK"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("database", "user", "expected_database", "expected_user"),
|
||||
[
|
||||
("wrong", "clientflow", "clientflow", "clientflow"),
|
||||
("clientflow", "wrong", "clientflow", "clientflow"),
|
||||
],
|
||||
)
|
||||
def test_strict_load_refuses_wrong_identity_before_factual_reads(
|
||||
monkeypatch, database, user, expected_database, expected_user,
|
||||
):
|
||||
connection = _Connection(database=database, user=user)
|
||||
with pytest.raises(RuntimeError, match="unexpected database identity"):
|
||||
_load(
|
||||
monkeypatch, connection, expected_database=expected_database,
|
||||
expected_user=expected_user, require_read_only=True,
|
||||
)
|
||||
|
||||
assert connection.statements[0] == "BEGIN READ ONLY"
|
||||
assert len([sql for sql in connection.statements if sql.upper().startswith("SELECT")]) == 1
|
||||
assert connection.statements[-1] == "ROLLBACK"
|
||||
|
||||
|
||||
def test_strict_factual_transaction_rejects_writes():
|
||||
connection = _Connection()
|
||||
connection.execute("BEGIN READ ONLY")
|
||||
with pytest.raises(RuntimeError, match="read-only transaction"):
|
||||
connection.execute("UPDATE opportunities SET stage = 'forbidden'")
|
||||
assert connection.read_only is True
|
||||
|
||||
|
||||
def test_non_strict_load_preserves_identity_then_read_only_collection_order(monkeypatch):
|
||||
connection = _Connection(database="clientflow_codex_test", user="clientflow_codex_test")
|
||||
result = _load(
|
||||
monkeypatch, connection, expected_database="clientflow_codex_test",
|
||||
expected_user="clientflow_codex_test", require_read_only=False,
|
||||
)
|
||||
|
||||
assert "CURRENT_DATABASE()" in connection.statements[0].upper()
|
||||
assert connection.identity_observations == [
|
||||
("clientflow_codex_test", "clientflow_codex_test", "off")
|
||||
]
|
||||
assert connection.statements[1] == "BEGIN READ ONLY"
|
||||
assert result["identity"]["transaction_read_only"] == "off"
|
||||
assert connection.statements[-1] == "ROLLBACK"
|
||||
Reference in New Issue
Block a user