bdcf793db2
Phase 2 (Integration + Tech-Debt + NFR Measurement) complete. 4 slices, 2 waves, 9 tasks. 4 REQs covered. 60 new tests (469 total). 8 v0.4 P1+ tech-debt findings addressed. Verify: APPROVE_WITH_NOTES. NFR measurement (p95 latency + guardrail FP/FN), cohort aggregation assist metrics (5 new metrics, no schema change), assist cost tracking + C-3 budget check, tech-debt wave (argon2id offload, cookie-secret validation, credential enum, f-string SQL, cache persistence, zoneinfo, audit log, 429 mock). ---ci--- project: praxis phase: 2 milestone: v0.5 status: complete requirements: covered: [REQ-NFR-ASSIST-01, REQ-IDEATE-04, REQ-IDEATE-06, REQ-IDEATE-07] partial: [] ---/ci---
106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""Mock-based tests for set_credential_status enum + f-string SQL fix
|
|
(TASK-12-03, P1+ #4/#8 from v0.4 REVIEW).
|
|
|
|
These tests do NOT require Postgres (they use a mock asyncpg pool). They
|
|
verify:
|
|
- 'revoked' uses a parameterized query with revoked_at=now() (no f-string).
|
|
- 'active' clears revoked_at=NULL (re-activation).
|
|
- Invalid status → ValueError (enum validation — P1+ #4).
|
|
- No f-string interpolation in the SQL (P1+ #8 code smell fix).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from db.pg_store import PgStore
|
|
|
|
|
|
def _mock_pool_with_conn():
|
|
"""Build a mock asyncpg pool + conn that records execute() calls."""
|
|
pool = MagicMock()
|
|
conn = MagicMock()
|
|
conn.execute = AsyncMock()
|
|
cm = MagicMock()
|
|
cm.__aenter__ = AsyncMock(return_value=conn)
|
|
cm.__aexit__ = AsyncMock(return_value=None)
|
|
pool.acquire = MagicMock(return_value=cm)
|
|
return pool, conn
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_credential_status_revoked_uses_parameterized_query():
|
|
"""TASK-12-03 (P1+ #8): 'revoked' uses a parameterized query (no f-string)."""
|
|
pool, conn = _mock_pool_with_conn()
|
|
store = PgStore(pool)
|
|
await store.set_credential_status("cred-1", "revoked")
|
|
# Exactly one execute call.
|
|
assert conn.execute.await_count == 1
|
|
sql, status_arg, cred_arg = conn.execute.await_args.args
|
|
# No f-string interpolation — the SQL is a literal with $1, $2.
|
|
assert "revoked_at = now()" in sql
|
|
assert "$1" in sql and "$2" in sql
|
|
assert status_arg == "revoked"
|
|
assert cred_arg == "cred-1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_credential_status_active_clears_revoked_at():
|
|
"""TASK-12-03: 'active' clears revoked_at=NULL (re-activation)."""
|
|
pool, conn = _mock_pool_with_conn()
|
|
store = PgStore(pool)
|
|
await store.set_credential_status("cred-1", "active")
|
|
assert conn.execute.await_count == 1
|
|
sql, status_arg, cred_arg = conn.execute.await_args.args
|
|
assert "revoked_at = NULL" in sql
|
|
assert status_arg == "active"
|
|
assert cred_arg == "cred-1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_credential_status_invalid_raises_value_error():
|
|
"""TASK-12-03 (P1+ #4): invalid status → ValueError (enum validation)."""
|
|
pool, conn = _mock_pool_with_conn()
|
|
store = PgStore(pool)
|
|
for bad_status in ("pending", "suspended", "deleted", "", "REVOKED", "active "):
|
|
with pytest.raises(ValueError, match="Invalid credential status"):
|
|
await store.set_credential_status("cred-1", bad_status)
|
|
# No execute call should have been made (validation happens before the query).
|
|
assert conn.execute.await_count == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_credential_status_no_fstring_in_sql():
|
|
"""TASK-12-03 (P1+ #8): no f-string interpolation in the SQL (code smell fix).
|
|
|
|
The SQL must be a literal string (no f-string {extra} interpolation). The
|
|
status + cred_id are bound parameters ($1, $2), not interpolated.
|
|
"""
|
|
pool, conn = _mock_pool_with_conn()
|
|
store = PgStore(pool)
|
|
await store.set_credential_status("cred-1", "revoked")
|
|
sql = conn.execute.await_args.args[0]
|
|
# The SQL must NOT contain an f-string-interpolated extra clause. The old
|
|
# code had f"UPDATE ... SET status = $1{extra} WHERE id = $2" where extra
|
|
# was ', revoked_at = now()' or ''. The new code has two explicit queries.
|
|
# Verify the SQL is a literal (no {extra}-style interpolation artifacts).
|
|
assert "{extra}" not in sql
|
|
assert "UPDATE issued_credentials SET status = $1, revoked_at = now()" in sql
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_credential_status_revoked_then_active():
|
|
"""TASK-12-03: revoke then re-activate (active clears revoked_at)."""
|
|
pool, conn = _mock_pool_with_conn()
|
|
store = PgStore(pool)
|
|
# Revoke.
|
|
await store.set_credential_status("cred-1", "revoked")
|
|
revoke_sql = conn.execute.await_args.args[0]
|
|
assert "revoked_at = now()" in revoke_sql
|
|
# Re-activate (active clears revoked_at).
|
|
conn.execute.reset_mock()
|
|
await store.set_credential_status("cred-1", "active")
|
|
active_sql = conn.execute.await_args.args[0]
|
|
assert "revoked_at = NULL" in active_sql |