from __future__ import annotations from pathlib import Path import pytest from trading.domain.database import ( HybridRow, PostgreSQLBackend, SQLiteBackend, configured_backend, validate_database_configuration, ) from trading.domain.schema import migration_plan class FakeCursor: def __init__(self, connection: "FakeRawConnection") -> None: self.connection = connection self.rowcount = 1 self.description = [("first",), ("second",)] self._rows = [("a", 2)] def execute(self, sql, params=(), **kwargs): self.connection.calls.append((sql, tuple(params), dict(kwargs))) return self def fetchone(self): return self._rows[0] if self._rows else None def fetchall(self): return list(self._rows) def __iter__(self): return iter(self._rows) class FakeRawConnection: def __init__(self) -> None: self.calls: list[tuple[str, tuple[object, ...], dict[str, object]]] = [] self.commits = 0 self.rollbacks = 0 self.closed = False def cursor(self): return FakeCursor(self) def commit(self): self.commits += 1 def rollback(self): self.rollbacks += 1 def close(self): self.closed = True def test_postgres_backend_is_lazy_redacted_and_repository_compatible(monkeypatch): captured = {} raw = FakeRawConnection() def connector(dsn, **kwargs): captured["dsn"] = dsn captured.update(kwargs) return raw secret_dsn = "postgresql://operator:super-secret@db.internal/hermes?sslmode=require" backend = PostgreSQLBackend(secret_dsn, connector=connector) assert "super-secret" not in repr(backend) assert backend.supports_advisory_locks() is True connection = backend.connect() assert captured["dsn"] == secret_dsn assert captured["autocommit"] is False assert captured["application_name"] == "hermesface-v5" cursor = connection.execute("SELECT a,b FROM t WHERE id=?", (7,)) assert raw.calls[-1][0] == "SELECT a,b FROM t WHERE id=%s" row = cursor.fetchone() assert isinstance(row, HybridRow) assert row[0] == "a" and row["second"] == 2 and dict(row) == {"first": "a", "second": 2} connection.execute("BEGIN IMMEDIATE") assert raw.calls[-1][0] == "BEGIN" connection.execute("INSERT OR IGNORE INTO roles(role_id) VALUES (?)", ("r",)) assert raw.calls[-1][0] == "INSERT INTO roles(role_id) VALUES (%s) ON CONFLICT DO NOTHING" connection.executescript("CREATE TABLE demo(id TEXT);") assert raw.calls[-1][2] == {"prepare": False} connection.commit() connection.rollback() connection.close() assert raw.commits == 1 and raw.rollbacks == 1 and raw.closed is True def test_postgres_connection_errors_do_not_echo_dsn(): def failing_connector(*args, **kwargs): raise RuntimeError("driver echoed postgresql://user:secret@example/db") backend = PostgreSQLBackend("postgresql://user:secret@example/db", connector=failing_connector) with pytest.raises(RuntimeError, match="PostgreSQL connection failed") as exc_info: backend.connect() assert "secret" not in str(exc_info.value) def test_sqlite_is_paper_only_and_memory_database_is_rejected(tmp_path, monkeypatch): monkeypatch.setenv("TRADING_MODE", "testnet") with pytest.raises(RuntimeError, match="restricted to isolated Paper"): SQLiteBackend(tmp_path / "db.sqlite3").connect() monkeypatch.setenv("TRADING_MODE", "paper") monkeypatch.setenv("HERMES_DATABASE_URL", "sqlite:///:memory:") with pytest.raises(ValueError, match="in-memory SQLite"): configured_backend() def test_backend_selection_requires_postgres_for_nonpaper(monkeypatch): monkeypatch.delenv("HERMES_DATABASE_URL", raising=False) monkeypatch.setenv("TRADING_MODE", "live") with pytest.raises(RuntimeError, match="requires PostgreSQL"): configured_backend() ok, messages = validate_database_configuration() assert ok is False and "requires PostgreSQL" in " ".join(messages) monkeypatch.setenv("HERMES_DATABASE_URL", "postgresql://db/hermes?sslmode=require") backend = configured_backend() assert isinstance(backend, PostgreSQLBackend) ok, messages = validate_database_configuration() assert ok is True and "PostgreSQL architecture selected" in " ".join(messages) def test_dialect_migration_plans_are_immutable_and_postgres_native(): sqlite_plan = migration_plan("sqlite") postgres_plan = migration_plan("postgresql") assert len(sqlite_plan) >= 10 assert [migration.version for migration in postgres_plan] == [ "001_baseline", "002_operational_indexes", "003_scoped_lifecycle_metadata", "004_position_events", "005_execution_audit_session", "006_rate_limit_audit_integrity", "007_audit_append_only", "008_advisory_outcomes", ] assert all(len(migration.sha256) == 64 for migration in sqlite_plan + postgres_plan) sql = postgres_plan[0].path.read_text(encoding="utf-8").upper() for forbidden in ("AUTOINCREMENT", "PRAGMA", "RANDOMBLOB", "BEGIN IMMEDIATE", "INSERT OR"): assert forbidden not in sql for required in ( "CREATE TABLE IF NOT EXISTS USERS", "CREATE TABLE IF NOT EXISTS ANALYSIS_REQUESTS", "CREATE TABLE IF NOT EXISTS EXECUTION_HISTORY", "CREATE TABLE IF NOT EXISTS POSITIONS", "CREATE TABLE IF NOT EXISTS TRADING_MODE_POLICIES", ): assert required in sql