ec397f2c65
v0.5 (Live Assist — on-the-job voice companion) milestone complete. 4 phases: P0 (pre-execution, v0.1.10) → P1 (assist core + guardrail, v0.1.11) → P2 (integration + tech-debt + NFR, v0.1.12) → P3 (final review + ship, v0.1.13 = milestone release). 16/16 REQs covered (3 ASSIST + 4 NFR + 9 IDEATE). 4 v0.6 backlog. 469 tests passed, 0 failed. 1 P0 fixed (guardrail processor safety). 8 P1+ flagged for v0.6. 8 v0.4 P1+ tech-debt addressed. G-049 + G-067 grill MUSTs resolved. ESCALATION-01 (PIPEDA) OPEN for human legal review before assist surface go-live. ---ci--- project: praxis phase: 3 milestone: v0.5 status: complete requirements: covered: [REQ-ASSIST-01, REQ-ASSIST-02, REQ-ASSIST-03, REQ-NFR-ASSIST-01, REQ-NFR-ASSIST-02, REQ-NFR-ASSIST-03, REQ-NFR-ASSIST-04, REQ-IDEATE-01, REQ-IDEATE-02, REQ-IDEATE-03, REQ-IDEATE-04, REQ-IDEATE-05, REQ-IDEATE-06, REQ-IDEATE-07, REQ-IDEATE-08, REQ-IDEATE-09] 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 |