fix(storage): address PR #798 round-3 review (SonarQube + pool sizing + RETURNING test)
Round-3 fixes. Two threads: - 9 OPEN SonarQube issues caused the "E Security Rating on New Code" gate failure. The bot's diagnosis (sa.text(text_sql) → SQL injection) was a wrong guess; the actual SQ rules firing were different. - Bot's substantive concerns: pool defaults too aggressive, delete_browser_session RETURNING path untested on Postgres, schema_version legacy table created on Postgres, stale module docstring. - User's underlying question on the pool: "isn't 1 connection enough?" Right-sized to 2+5 and documented the concurrency model in ADR-026 so the rationale is durable. SonarQube quality-gate fixes (clears all 9 OPEN issues) ------------------------------------------------------- - BLOCKER S6418: rename `SECRET` constant in test_storage_logging.py to `SENTINEL_PASSWORD_FRAGMENT` + NOSONAR with rationale. - CRITICAL S3776: extract `_build_postgres_engine()` from `initialize()` (was complexity 26 > 15); incidentally creates a clean unit-test seam for engine args. - CRITICAL S4423: `ssl.create_default_context(cafile=...)` is flagged as "weak protocol" — Python 3.10+ already negotiates the strongest available protocol. Explicitly pass `purpose=ssl.Purpose.SERVER_AUTH` and NOSONAR with the Python-version rationale. - MAJOR S3358: split the TLS-mode nested ternary in the engine factory into a `_describe_ssl_arg()` helper. - MAJOR S2068 ×3: bind test app-password literals to local vars and put `# NOSONAR S2068` on the same line as the literal (anchoring requirement) instead of on the closing paren. - MINOR S7503 ×2: `# NOSONAR S7503` on `_Cursor.__aenter__/__aexit__` — they MUST be `async` per the context-manager protocol. Pool sizing right-sized (answers "why so many connections?") ------------------------------------------------------------ - `DATABASE_POOL_SIZE` default 10 → **2**. - `DATABASE_MAX_OVERFLOW` default 20 → **5**. - Per-pod max drops from 30 to 7. With 3 replicas, total = 21 connections (was 90) — well under managed-Postgres `max_connections=100`. - New INFO log at startup: `Postgres engine ready: pool_size=N max_overflow=M (per-pod max K connections)`. Surfaces the active sizing without grepping config. - New ADR-026 § "Concurrency model and pool sizing" explains asyncpg's single-flight connection semantics, the MCP workload shape (read-mostly point lookups), why-not-1 (multi-user serialization), and the tune-up/tune-down recipe. - `docs/configuration.md` table updated with new defaults + homelab-vs-prod tuning guidance, linking the ADR. RETURNING path covered on Postgres ---------------------------------- - New `test_browser_session_delete_returning` exercises the `DELETE … RETURNING user_id` path — the only RETURNING clause in the storage layer and the most dialect-sensitive SQL in this PR. Asserts both present-row (returns True, row gone) and absent-row (returns False) branches. Schema portability polish ------------------------- - `alembic 001`: gate `schema_version` table creation on `op.get_bind().dialect.name == "sqlite"`. The table exists purely to match the fingerprint of pre-Alembic SQLite databases; fresh Postgres installs no longer carry the dead legacy table. Misc polish ----------- - Module docstring: "SQLite-based" → "SQL-backed", with a sentence on the DATABASE_URL opt-in and an ADR-026 link. - Comment on `_wrap_row` noting `row._mapping` is the documented RowMapping accessor in SQLAlchemy 2.x despite the underscore. Skipped (rationale in PR reply) ------------------------------- - `_qmark_to_named` SQL-comment handling: docstring already notes the limitation; no `?` in storage SQL comments today. - Module-level `anyio.Lock()`: established precedent confirmed by the bot itself. - `get_audit_logs` `SELECT *`: pre-existing pattern, out of scope. Verification ------------ - `uv run pytest tests/unit/` — 1025 passed. - `TEST_DATABASE_URL=… uv run pytest tests/integration/test_storage_postgres.py -m postgres` — 7 passed. - `ruff check && ruff format --check && ty check` — clean. - Confirmed `schema_version` absent on fresh Postgres, still present on fresh SQLite. Tracked on Astrolabe Cloud POC board, card #99. --- _This PR was generated with the help of AI, and reviewed by a Human_ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
f2b7bf132f
commit
51419329b0
@@ -111,13 +111,23 @@ async def test_refresh_token_roundtrip(storage: RefreshTokenStorage):
|
||||
|
||||
|
||||
async def test_app_password_roundtrip(storage: RefreshTokenStorage):
|
||||
"""Store + retrieve + replace + delete a scoped app password."""
|
||||
await storage.store_app_password(user_id="bob", app_password="pw-1")
|
||||
assert await storage.get_app_password("bob") == "pw-1"
|
||||
"""Store + retrieve + replace + delete a scoped app password.
|
||||
|
||||
The ``app_password=`` keyword-arg literals below trigger SonarQube's
|
||||
hard-coded-credential heuristic (``S2068``) even though these are
|
||||
obvious test fixtures with no production reach. The literals are
|
||||
bound to local variables so the NOSONAR marker can anchor to the
|
||||
same line as the literal — SQ doesn't pick up the marker if it
|
||||
sits on a different physical line.
|
||||
"""
|
||||
bob_pw_v1 = "pw-1" # NOSONAR S2068 — localhost test fixture, never deployed
|
||||
await storage.store_app_password(user_id="bob", app_password=bob_pw_v1)
|
||||
assert await storage.get_app_password("bob") == bob_pw_v1
|
||||
|
||||
# Replace path exercises the ON CONFLICT DO UPDATE on the singleton row.
|
||||
await storage.store_app_password(user_id="bob", app_password="pw-2")
|
||||
assert await storage.get_app_password("bob") == "pw-2"
|
||||
bob_pw_v2 = "pw-2" # NOSONAR S2068 — localhost test fixture, never deployed
|
||||
await storage.store_app_password(user_id="bob", app_password=bob_pw_v2)
|
||||
assert await storage.get_app_password("bob") == bob_pw_v2
|
||||
|
||||
assert await storage.delete_app_password("bob") is True
|
||||
assert await storage.get_app_password("bob") is None
|
||||
@@ -159,7 +169,8 @@ async def test_webhook_tracking(storage: RefreshTokenStorage):
|
||||
|
||||
async def test_audit_log_capture(storage: RefreshTokenStorage):
|
||||
"""Audit events from upstream methods land in audit_logs."""
|
||||
await storage.store_app_password(user_id="carol", app_password="x")
|
||||
carol_pw = "x" # NOSONAR S2068 — localhost test fixture, never deployed
|
||||
await storage.store_app_password(user_id="carol", app_password=carol_pw)
|
||||
logs = await storage.get_audit_logs(user_id="carol", limit=10)
|
||||
assert any(entry["event"] == "store_app_password" for entry in logs)
|
||||
|
||||
@@ -218,3 +229,26 @@ async def test_cleanup_expired_roundtrip(storage: RefreshTokenStorage):
|
||||
assert await storage.get_refresh_token("expired-user") is None
|
||||
assert await storage.get_oauth_session("sess-fresh") is not None
|
||||
assert await storage.get_oauth_session("sess-stale") is None
|
||||
|
||||
|
||||
async def test_browser_session_delete_returning(storage: RefreshTokenStorage):
|
||||
"""Exercise the ``DELETE … RETURNING user_id`` path on Postgres.
|
||||
|
||||
``delete_browser_session`` is the only RETURNING clause in the
|
||||
storage layer and the most dialect-sensitive SQL in this PR — it
|
||||
needed SQLite ≥ 3.35 specifically because of RETURNING. Bot review
|
||||
on PR #798 round 2 flagged that the existing cleanup test didn't
|
||||
actually exercise this path. Asserts both the present and absent
|
||||
cases so the asyncpg result-handling for RETURNING is covered.
|
||||
"""
|
||||
await storage.create_browser_session(
|
||||
session_id="bs-returning", user_id="alice", ttl_seconds=600
|
||||
)
|
||||
assert await storage.get_browser_session_user("bs-returning") == "alice"
|
||||
|
||||
assert await storage.delete_browser_session("bs-returning") is True
|
||||
assert await storage.get_browser_session_user("bs-returning") is None
|
||||
|
||||
# Deleting a nonexistent session returns False (RETURNING yields no
|
||||
# row → rowcount path).
|
||||
assert await storage.delete_browser_session("never-existed") is False
|
||||
|
||||
Reference in New Issue
Block a user