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:
Chris Coutinho
2026-05-16 19:33:23 +02:00
co-authored by Claude Opus 4.7
parent f2b7bf132f
commit 51419329b0
7 changed files with 229 additions and 78 deletions
+40 -6
View File
@@ -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