Files
turnstone/tests/test_storage_registry.py
T
Patrick Buckley 205e7818f8 Fix/codeql quality findings (#299)
* fix: replace empty except blocks with diagnostic logging

Add log.debug/warning to 7 bare except-pass blocks that silenced
failures in security-relevant or operationally-important paths:
- Channel route lookup, CLI policy evaluation, OIDC JWKS fetch,
  prompt policy loading, plan file write, routing override, username
  resolution.

Plan write now reports failure to user instead of falsely claiming
"Plan saved."

* fix: replace assert-with-side-effect and narrow BaseException catch

- Convert 4 assert isinstance() to explicit TypeError raises — assertions
  are stripped under python -O, removing runtime type checks
- Narrow except BaseException to except Exception in fallback handler —
  KeyboardInterrupt/SystemExit should not record as health failures
- Plan write failure now reports error to user instead of "Plan saved"

* fix: wire up toast error type and remove useless conditional

- showToast() now accepts optional type param ("error") with red border
  styling — 3 call sites were passing "error" that was silently ignored
- Remove always-true if (q) guard after early-return on empty query

* fix: remove unreachable return None after return self._judge

* fix: parenthesize multi-line string concatenations in dev_parts list

Explicit parens make intentional concatenation unambiguous to static
analysis (CodeQL implicit-string-concatenation-in-list rule).

* fix: remove constant-true filter in test mock — return list directly

* fix: extract side-effecting calls from assert in tests

store.delete() and mgr.close() have side effects that would be
stripped under python -O. Assign to variable first, then assert.

* fix: remove unused local variables in tests

Drop assignments to unused workstream/variable references created
solely for side effects. Use _ for unused tuple unpacking.

* fix: use admin.prompt_policies permission for prompt policy endpoints

All 5 prompt-policy endpoints (list, create, get, update, delete)
were checking admin.policies (the tool-policy permission) instead of
admin.prompt_policies. This caused a mismatch with the admin UI which
gates the tab on admin.prompt_policies — users could see the tab but
get 403, or reach the endpoint but never see the tab.

* fix: use caplog instead of capsys for structlog warning assertion

structlog output goes through the logging system, not stdout/stderr.

* fix: address review — remove dead isinstance, module-level import, unnecessary lambdas

- session.py: remove unreachable isinstance check (has_batch already
  validates raw_edits is a list)
- cli.py: move logging import to module level
- test_workstream.py: replace lambda wid: FakeUI(wid) with FakeUI
2026-04-04 17:53:20 -07:00

129 lines
4.5 KiB
Python

"""Tests for the storage backend registry."""
from unittest.mock import patch
import pytest
import sqlalchemy as sa
from turnstone.core.storage import (
StorageUnavailableError,
get_storage,
init_storage,
reset_storage,
)
from turnstone.core.storage._postgresql import PostgreSQLBackend
from turnstone.core.storage._sqlite import SQLiteBackend
@pytest.fixture(autouse=True)
def _clean_registry():
"""Reset the storage registry before and after each test."""
reset_storage()
yield
reset_storage()
class TestInitStorage:
def test_sqlite_default(self, tmp_path):
backend = init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
assert isinstance(backend, SQLiteBackend)
def test_unknown_backend_raises(self):
with pytest.raises(ValueError, match="Unknown storage backend"):
init_storage("mongodb")
def test_postgresql_requires_url(self):
with pytest.raises(ValueError, match="requires a connection URL"):
init_storage("postgresql")
class TestGetStorage:
def test_auto_init(self, tmp_path, monkeypatch):
"""get_storage() auto-initializes with SQLite if not yet initialized."""
monkeypatch.chdir(tmp_path)
storage = get_storage()
assert isinstance(storage, SQLiteBackend)
def test_returns_same_instance(self, tmp_path):
init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
s1 = get_storage()
s2 = get_storage()
assert s1 is s2
class TestResetStorage:
def test_reset_clears_singleton(self, tmp_path):
init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
s1 = get_storage()
reset_storage()
# After reset, get_storage() auto-inits a new instance
init_storage("sqlite", path=str(tmp_path / "test2.db"), run_migrations=False)
s2 = get_storage()
assert s1 is not s2
class TestConnUnavailableLogging:
"""Test that _conn() deduplicates DB unavailable/restored logging."""
def _make_backend(self, tmp_path):
"""Create a minimal SQLite backend for testing _conn()."""
from turnstone.core.storage._sqlite import SQLiteBackend
return SQLiteBackend(str(tmp_path / "test.db"), create_tables=True)
def test_logs_unavailable_once(self, tmp_path, caplog: pytest.LogCaptureFixture) -> None:
backend = self._make_backend(tmp_path)
with patch.object(backend, "_engine") as mock_engine:
mock_engine.connect.side_effect = sa.exc.OperationalError(
"conn", {}, Exception("refused")
)
for _ in range(3):
with pytest.raises(StorageUnavailableError), backend._conn():
pass # pragma: no cover
unavailable_msgs = [r for r in caplog.records if "database.unavailable" in r.message]
assert len(unavailable_msgs) == 1
def test_logs_restored_on_recovery(self, tmp_path, caplog: pytest.LogCaptureFixture) -> None:
import logging
caplog.set_level(logging.INFO)
backend = self._make_backend(tmp_path)
# Simulate outage
with patch.object(backend, "_engine") as mock_engine:
mock_engine.connect.side_effect = sa.exc.OperationalError(
"conn", {}, Exception("refused")
)
with pytest.raises(StorageUnavailableError), backend._conn():
pass # pragma: no cover
assert backend._db_unavailable is True
# Real connection — should log restored
caplog.clear()
with backend._conn():
pass
restored_msgs = [r for r in caplog.records if "database.connection_restored" in r.message]
assert len(restored_msgs) == 1
assert backend._db_unavailable is False
def test_postgresql_conn_raises_storage_unavailable(self) -> None:
import threading
backend = PostgreSQLBackend.__new__(PostgreSQLBackend)
backend._db_unavailable = False
backend._db_unavailable_lock = threading.Lock()
def _raise_op_error():
raise sa.exc.OperationalError("conn", {}, Exception("refused"))
mock_engine = type(
"E",
(),
{
"connect": staticmethod(_raise_op_error),
"url": sa.engine.make_url("postgresql://user:pass@localhost/db"),
},
)()
backend._engine = mock_engine
with pytest.raises(StorageUnavailableError), backend._conn():
pass # pragma: no cover
assert backend._db_unavailable is True