"""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