refactor(tests): one definition each for the copied fixtures and fakes (#2825, milestone 296 area 1)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
The shape ledger showed the same test scaffolding defined over and over: _bind_user x12 (byte-identical), _dispose_engine x10 in three wordings, _no_supersession x3, _make_mock_session x7 in three subsets, a get-or-create User helper x2 (+3 inlined), and fifteen hand-rolled MagicMock note factories each re-explaining the same "an auto-MagicMock attribute is truthy" hazard (note 2109). Now: conftest.py carries _bind_user / _dispose_engine / _no_supersession as opt-in fixtures (pytestmark = usefixtures(...) per module, so unit tests pay nothing), and tests/helpers.py carries make_mock_session(), ensure_user() and fake_note(**attrs) — the hazard documented once, real values on every attribute the product reads. Call sites were rewritten by AST so titles with dashes and commas survived; the three SimpleNamespace _note stand-ins that only feed a single function stay local. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,16 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_mock_session():
|
||||
s = AsyncMock()
|
||||
s.__aenter__ = AsyncMock(return_value=s)
|
||||
s.__aexit__ = AsyncMock(return_value=False)
|
||||
s.add = MagicMock()
|
||||
s.commit = AsyncMock()
|
||||
s.refresh = AsyncMock()
|
||||
return s
|
||||
from tests.helpers import make_mock_session
|
||||
|
||||
|
||||
def _fake_rulebook(id=1, owner_user_id=7, title="FabledSword family", description=""):
|
||||
@@ -35,7 +26,7 @@ def _fake_rulebook(id=1, owner_user_id=7, title="FabledSword family", descriptio
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rulebook_stores_to_db():
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
with patch("scribe.services.rulebooks.async_session") as mock_cls:
|
||||
mock_cls.return_value = mock_session
|
||||
from scribe.services.rulebooks import create_rulebook
|
||||
@@ -49,7 +40,7 @@ async def test_create_rulebook_stores_to_db():
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_rulebooks_returns_owned_only():
|
||||
rb = _fake_rulebook(id=1)
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.all.return_value = [rb]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -63,7 +54,7 @@ async def test_list_rulebooks_returns_owned_only():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_rulebook_returns_none_when_not_owner():
|
||||
"""get_rulebook scopes by owner_user_id — wrong user gets None."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -77,7 +68,7 @@ async def test_get_rulebook_returns_none_when_not_owner():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_rulebook_only_sets_provided_fields():
|
||||
rb = _fake_rulebook(id=1, title="old")
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = rb
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -91,7 +82,7 @@ async def test_update_rulebook_only_sets_provided_fields():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_rulebook_calls_delete():
|
||||
rb = _fake_rulebook(id=1)
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = rb
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -124,7 +115,7 @@ def _fake_topic(id=1, rulebook_id=1, title="git-workflow", description="", order
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_topic_requires_owned_rulebook():
|
||||
"""create_topic raises ValueError if the rulebook isn't owned by user."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -143,7 +134,7 @@ async def test_list_topics_returns_topics_for_owned_rulebook():
|
||||
topic = _fake_topic(id=10, rulebook_id=1, title="git-workflow")
|
||||
|
||||
# Two execute calls: ownership check, then topic select.
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
rb_result = MagicMock()
|
||||
rb_result.scalar_one_or_none.return_value = rb
|
||||
topic_result = MagicMock()
|
||||
@@ -182,7 +173,7 @@ def _fake_rule(id=1, topic_id=10, title="dev is home",
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_rule_requires_owned_topic():
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None # topic not found
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -199,7 +190,7 @@ async def test_create_rule_requires_owned_topic():
|
||||
async def test_list_rules_filters_by_topic_id():
|
||||
"""list_rules(topic_id=X) returns rules in that topic, ownership-scoped."""
|
||||
rule = _fake_rule(id=1, topic_id=10)
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalars.return_value.all.return_value = [rule]
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -212,7 +203,7 @@ async def test_list_rules_filters_by_topic_id():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_rule_returns_none_when_not_owner():
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -228,7 +219,7 @@ async def test_get_rule_returns_none_when_not_owner():
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_project_requires_owned_rulebook():
|
||||
"""subscribe_project raises if user doesn't own the rulebook."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
mock_result = MagicMock()
|
||||
mock_result.scalar_one_or_none.return_value = None
|
||||
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||
@@ -253,7 +244,7 @@ def _empty():
|
||||
async def test_get_applicable_rules_returns_shape():
|
||||
"""get_applicable_rules returns the full projection — including the
|
||||
new suppression fields and rulebook/topic IDs on each rule."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
sub_result = MagicMock()
|
||||
sub_result.all.return_value = [(1, "FabledSword family")]
|
||||
rules_result = MagicMock()
|
||||
@@ -291,7 +282,7 @@ async def test_get_applicable_rules_returns_shape():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_applicable_rules_truncates_when_over_limit():
|
||||
"""When limit+1 rows are returned, truncated=True and only `limit` returned."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
sub_result = MagicMock()
|
||||
sub_result.all.return_value = []
|
||||
rules_result = MagicMock()
|
||||
@@ -314,7 +305,7 @@ async def test_get_applicable_rules_truncates_when_over_limit():
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_applicable_rules_includes_project_scoped_rules():
|
||||
"""Project-scoped rules surface in the project_rules field."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
proj_rules_result = MagicMock()
|
||||
proj_rules_result.all.return_value = [
|
||||
(100, "Use alembic", "Always run migrations via alembic, never raw SQL."),
|
||||
@@ -338,7 +329,7 @@ async def test_get_applicable_rules_includes_project_scoped_rules():
|
||||
async def test_get_applicable_rules_surfaces_suppressed_with_context():
|
||||
"""Suppressed rules and topics come back with full title + rulebook context
|
||||
so the UI can render them without an extra round-trip."""
|
||||
mock_session = _make_mock_session()
|
||||
mock_session = make_mock_session()
|
||||
suppressed_rules_result = MagicMock()
|
||||
suppressed_rules_result.all.return_value = [
|
||||
# (rule_id, title, topic_id, topic_title, rulebook_id, rulebook_title)
|
||||
|
||||
Reference in New Issue
Block a user