c054675167
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / build (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 38s
CI / security (pull_request) Successful in 40s
CI / integration_tests (pull_request) Successful in 4m5s
CI / unit_tests (pull_request) Successful in 4m56s
CI / coverage (pull_request) Successful in 5m13s
CI / docker (pull_request) Successful in 1m10s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 19s
CI / build (push) Successful in 17s
CI / security (push) Successful in 37s
CI / typecheck (push) Successful in 40s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m59s
CI / integration_tests (push) Successful in 3m24s
CI / docker (push) Successful in 40s
CI / coverage (push) Successful in 5m14s
CI / benchmark-publish (push) Successful in 17m55s
CI / benchmark-regression (pull_request) Successful in 34m15s
Implement database resource types (postgres, mysql, sqlite, duckdb) with connection args, auth handling, and transaction-based sandbox strategy using BEGIN/ROLLBACK/COMMIT wrappers. Key changes: - Add DatabaseResourceHandler with 4 database type definitions - Implement TransactionSandbox for transaction_rollback strategy - Wire TransactionSandbox into SandboxFactory - Register database types in bootstrap_builtin_types - Add connection validation with credential masking - Add Behave BDD tests, Robot integration tests, ASV benchmarks ISSUES CLOSED: #342
191 lines
6.0 KiB
Python
191 lines
6.0 KiB
Python
"""Helper utilities for database resource Robot smoke tests.
|
|
|
|
Each command prints a single ``<command>-ok`` token on success so the
|
|
calling Robot test can assert on ``stdout``. Only uses sqlite3 (stdlib)
|
|
for concrete database testing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
|
|
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
|
|
from cleveragents.infrastructure.sandbox.transaction_sandbox import (
|
|
TransactionSandbox,
|
|
)
|
|
from cleveragents.resource.handlers.database import (
|
|
DATABASE_TYPE_DEFS,
|
|
DATABASE_TYPE_NAMES,
|
|
DatabaseResourceHandler,
|
|
resolve_connection_args,
|
|
validate_connection,
|
|
)
|
|
from cleveragents.resource.handlers.protocol import ResourceHandler
|
|
from cleveragents.shared.redaction import mask_database_url
|
|
|
|
|
|
def _protocol_check() -> None:
|
|
"""Verify DatabaseResourceHandler satisfies ResourceHandler."""
|
|
handler = DatabaseResourceHandler()
|
|
assert isinstance(handler, ResourceHandler), (
|
|
"DatabaseResourceHandler not a ResourceHandler"
|
|
)
|
|
print("protocol-check-ok")
|
|
|
|
|
|
def _type_defs() -> None:
|
|
"""Verify all 4 database type definitions are present."""
|
|
names = {td["name"] for td in DATABASE_TYPE_DEFS}
|
|
for expected in ("postgres", "mysql", "sqlite", "duckdb"):
|
|
assert expected in names, f"Missing type def: {expected}"
|
|
assert names == DATABASE_TYPE_NAMES
|
|
print("type-defs-ok")
|
|
|
|
|
|
def _sqlite_validate() -> None:
|
|
"""Validate SQLite connection with a temp file."""
|
|
fd, path = tempfile.mkstemp(suffix=".db")
|
|
os.close(fd)
|
|
try:
|
|
success, msg = validate_connection("sqlite", {"path": path})
|
|
assert success, f"SQLite validation failed: {msg}"
|
|
print("sqlite-validate-ok")
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
def _tx_commit() -> None:
|
|
"""Test transaction sandbox commit persists data."""
|
|
fd, path = tempfile.mkstemp(suffix=".db")
|
|
os.close(fd)
|
|
try:
|
|
# Setup table
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Use sandbox
|
|
sandbox = TransactionSandbox(resource_id="robot-tx-1", original_path=path)
|
|
sandbox.create(plan_id="PLAN-ROBOT-COMMIT")
|
|
sandbox.execute("INSERT INTO items (id, name) VALUES (1, 'committed')")
|
|
result = sandbox.commit("robot commit")
|
|
assert result.success
|
|
sandbox.cleanup()
|
|
|
|
# Verify data persisted
|
|
conn = sqlite3.connect(path)
|
|
rows = conn.execute("SELECT name FROM items WHERE id = 1").fetchall()
|
|
conn.close()
|
|
assert len(rows) == 1
|
|
assert rows[0][0] == "committed"
|
|
print("tx-commit-ok")
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
def _tx_rollback() -> None:
|
|
"""Test transaction sandbox rollback discards data."""
|
|
fd, path = tempfile.mkstemp(suffix=".db")
|
|
os.close(fd)
|
|
try:
|
|
# Setup table
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Use sandbox
|
|
sandbox = TransactionSandbox(resource_id="robot-tx-2", original_path=path)
|
|
sandbox.create(plan_id="PLAN-ROBOT-ROLLBACK")
|
|
sandbox.execute("INSERT INTO items (id, name) VALUES (2, 'rolled-back')")
|
|
# Transition to ACTIVE by executing, then rollback
|
|
sandbox.rollback()
|
|
sandbox.cleanup()
|
|
|
|
# Verify data NOT persisted
|
|
conn = sqlite3.connect(path)
|
|
rows = conn.execute("SELECT name FROM items WHERE id = 2").fetchall()
|
|
conn.close()
|
|
assert len(rows) == 0
|
|
print("tx-rollback-ok")
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
def _cred_mask() -> None:
|
|
"""Verify credential masking in validation messages."""
|
|
props = {"connection-string": "postgresql://admin:supersecret@db:5432/app"}
|
|
resolved = resolve_connection_args(props, "postgres")
|
|
_success, msg = validate_connection("postgres", resolved)
|
|
assert "supersecret" not in msg, f"Secret leaked: {msg}"
|
|
# Also test mask_database_url directly
|
|
masked = mask_database_url("postgresql://admin:supersecret@db:5432/app")
|
|
assert "supersecret" not in masked
|
|
print("cred-mask-ok")
|
|
|
|
|
|
def _factory_routing() -> None:
|
|
"""Verify SandboxFactory routes transaction_rollback correctly."""
|
|
factory = SandboxFactory()
|
|
sandbox = factory.create_sandbox(
|
|
resource_id="factory-test",
|
|
original_path=":memory:",
|
|
sandbox_strategy="transaction_rollback",
|
|
)
|
|
assert isinstance(sandbox, TransactionSandbox)
|
|
assert SandboxFactory.is_supported("transaction_rollback")
|
|
print("factory-routing-ok")
|
|
|
|
|
|
def _read_only() -> None:
|
|
"""Verify read-only sandbox prevents writes."""
|
|
fd, path = tempfile.mkstemp(suffix=".db")
|
|
os.close(fd)
|
|
try:
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
sandbox = TransactionSandbox(
|
|
resource_id="ro-test", original_path=path, read_only=True
|
|
)
|
|
sandbox.create(plan_id="PLAN-RO-ROBOT")
|
|
rejected = False
|
|
try:
|
|
sandbox.execute("INSERT INTO items (id, name) VALUES (1, 'forbidden')")
|
|
except Exception:
|
|
rejected = True
|
|
finally:
|
|
sandbox.cleanup()
|
|
assert rejected, "Write was not rejected in read-only mode"
|
|
print("read-only-ok")
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
|
|
_COMMANDS = {
|
|
"protocol-check": _protocol_check,
|
|
"type-defs": _type_defs,
|
|
"sqlite-validate": _sqlite_validate,
|
|
"tx-commit": _tx_commit,
|
|
"tx-rollback": _tx_rollback,
|
|
"cred-mask": _cred_mask,
|
|
"factory-routing": _factory_routing,
|
|
"read-only": _read_only,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
|
print(
|
|
f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
_COMMANDS[sys.argv[1]]()
|